<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jonas Hämmerle</title>
    <description>The latest articles on DEV Community by Jonas Hämmerle (@thejayedge).</description>
    <link>https://dev.to/thejayedge</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4021406%2F4c94fefc-cf75-4125-94b3-3c63cec7adfa.png</url>
      <title>DEV Community: Jonas Hämmerle</title>
      <link>https://dev.to/thejayedge</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thejayedge"/>
    <language>en</language>
    <item>
      <title>The complete guide to disposable email detection</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Tue, 14 Jul 2026 12:55:24 +0000</pubDate>
      <link>https://dev.to/thejayedge/the-complete-guide-to-disposable-email-detection-12d5</link>
      <guid>https://dev.to/thejayedge/the-complete-guide-to-disposable-email-detection-12d5</guid>
      <description>&lt;p&gt;Syntax-valid, MX-record-valid, and still worthless for your product: that's a disposable email address from 10minutemail, guerrillamail, mailinator, or one of several hundred similar throwaway-inbox services. If you're not filtering these, a meaningful slice of your "verified" signups are addresses nobody will ever check twice.&lt;/p&gt;

&lt;p&gt;Here's why this is a genuinely different problem from syntax or MX validation, and how to actually solve it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why there's no algorithmic tell.&lt;/strong&gt; A disposable-mail domain looks completely normal — valid syntax, real MX records, sometimes even a legitimate-looking domain name. There's no structural pattern that distinguishes &lt;code&gt;mailinator.com&lt;/code&gt; from &lt;code&gt;gmail.com&lt;/code&gt; at the protocol level. The only reliable signal is a maintained list of known disposable domains, checked against the domain part of the address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isDisposable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;disposableDomainSet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;disposableDomainSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The catch: the list has to stay current.&lt;/strong&gt; New disposable-mail domains spin up constantly — some services rotate domains specifically to dodge blocklists. A list you snapshot once and never update degrades within weeks. The community-run &lt;a href="https://github.com/disposable-email-domains/disposable-email-domains" rel="noopener noreferrer"&gt;disposable-email-domains&lt;/a&gt; project is a solid free starting point and is actively maintained, but you need a process to re-sync it, not a one-time import.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this fits in the validation pipeline.&lt;/strong&gt; Order matters, because each check is progressively more expensive and should only run if the previous one passed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Syntax&lt;/strong&gt; — near-instant, always run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MX record lookup&lt;/strong&gt; — one DNS call, catches typo'd/dead domains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disposable-domain check&lt;/strong&gt; — a set lookup against your list, catches throwaway-but-technically-valid addresses&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running them in that order means you only pay for the disposable-domain check on addresses that already passed the cheaper filters — no wasted work on obviously broken input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't catch:&lt;/strong&gt; a user who signs up with a real Gmail address they simply never check again. Disposable-domain detection targets &lt;em&gt;services designed for throwaway use&lt;/em&gt;, not general inbox abandonment — that's a retention problem, not a validation one.&lt;/p&gt;

&lt;p&gt;I run this exact pipeline (syntax → MX → disposable-domain, each opt-in past the first) as an endpoint on &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; with a synced domain list, if you'd rather not own the re-sync process yourself. Sibling APIs on the same account: &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; and &lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The complete guide to disposable email detection</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sun, 12 Jul 2026 09:48:12 +0000</pubDate>
      <link>https://dev.to/thejayedge/the-complete-guide-to-disposable-email-detection-2lbh</link>
      <guid>https://dev.to/thejayedge/the-complete-guide-to-disposable-email-detection-2lbh</guid>
      <description>&lt;p&gt;Syntax-valid, MX-record-valid, and still worthless for your product: that's a disposable email address from 10minutemail, guerrillamail, mailinator, or one of several hundred similar throwaway-inbox services. If you're not filtering these, a meaningful slice of your "verified" signups are addresses nobody will ever check twice.&lt;/p&gt;

&lt;p&gt;Here's why this is a genuinely different problem from syntax or MX validation, and how to actually solve it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why there's no algorithmic tell.&lt;/strong&gt; A disposable-mail domain looks completely normal — valid syntax, real MX records, sometimes even a legitimate-looking domain name. There's no structural pattern that distinguishes &lt;code&gt;mailinator.com&lt;/code&gt; from &lt;code&gt;gmail.com&lt;/code&gt; at the protocol level. The only reliable signal is a maintained list of known disposable domains, checked against the domain part of the address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isDisposable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;disposableDomainSet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;disposableDomainSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The catch: the list has to stay current.&lt;/strong&gt; New disposable-mail domains spin up constantly — some services rotate domains specifically to dodge blocklists. A list you snapshot once and never update degrades within weeks. The community-run &lt;a href="https://github.com/disposable-email-domains/disposable-email-domains" rel="noopener noreferrer"&gt;disposable-email-domains&lt;/a&gt; project is a solid free starting point and is actively maintained, but you need a process to re-sync it, not a one-time import.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this fits in the validation pipeline.&lt;/strong&gt; Order matters, because each check is progressively more expensive and should only run if the previous one passed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Syntax&lt;/strong&gt; — near-instant, always run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MX record lookup&lt;/strong&gt; — one DNS call, catches typo'd/dead domains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disposable-domain check&lt;/strong&gt; — a set lookup against your list, catches throwaway-but-technically-valid addresses&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running them in that order means you only pay for the disposable-domain check on addresses that already passed the cheaper filters — no wasted work on obviously broken input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it doesn't catch:&lt;/strong&gt; a user who signs up with a real Gmail address they simply never check again. Disposable-domain detection targets &lt;em&gt;services designed for throwaway use&lt;/em&gt;, not general inbox abandonment — that's a retention problem, not a validation one.&lt;/p&gt;

&lt;p&gt;I run this exact pipeline (syntax → MX → disposable-domain, each opt-in past the first) as an endpoint on &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; with a synced domain list, if you'd rather not own the re-sync process yourself. Sibling APIs on the same account: &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; and &lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why your regex for IBAN validation is probably wrong</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sun, 12 Jul 2026 09:47:29 +0000</pubDate>
      <link>https://dev.to/thejayedge/why-your-regex-for-iban-validation-is-probably-wrong-am7</link>
      <guid>https://dev.to/thejayedge/why-your-regex-for-iban-validation-is-probably-wrong-am7</guid>
      <description>&lt;p&gt;A regex like &lt;code&gt;/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/&lt;/code&gt; will pass through plenty of IBANs that are complete garbage. It checks the shape — two letters, two digits, then alphanumerics — but shape isn't validity, and this is the mistake nearly every from-scratch IBAN validator makes.&lt;/p&gt;

&lt;p&gt;Here's what a shape-only regex misses, in order of how often it bites people:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Country-specific length.&lt;/strong&gt; IBAN length isn't a range, it's a fixed number per country: Germany is always 22 characters, the Netherlands 18, Malta 31. A regex with &lt;code&gt;{11,30}&lt;/code&gt; for the BBAN portion accepts a "German" IBAN that's the wrong length for Germany, because it's only checking the global range across &lt;em&gt;all&lt;/em&gt; countries, not the specific one this IBAN claims to be from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The checksum.&lt;/strong&gt; This is the part a regex structurally cannot do — regular expressions can't compute a mod-97 checksum, because that requires arithmetic over the whole string, not pattern matching. Every IBAN has two check digits (positions 3-4) that are the result of ISO 7064's mod-97-10 algorithm applied to the rearranged, letter-to-number-converted account number. A regex will happily accept an IBAN with those two digits set to anything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isValidIBAN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iban&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;iban&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;]{2}\d{2}[&lt;/span&gt;&lt;span class="sr"&gt;A-Z0-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// shape check — necessary but nowhere near sufficient&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rearranged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rearranged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remainder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;remainder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;remainder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remainder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;97&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the chunked-modulo loop — IBANs convert to numbers with 30+ digits, well past what JS's &lt;code&gt;Number&lt;/code&gt; type can hold precisely, so you can't just do &lt;code&gt;bigNumber % 97&lt;/code&gt; in one step without either a BigInt polyfill or this kind of iterative reduction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Per-country BBAN structure.&lt;/strong&gt; Beyond length, each country also defines which positions must be digits vs letters within the BBAN. A regex checking "alphanumeric" for the whole remainder will accept a UK IBAN with digits where the bank code letters should be.&lt;/p&gt;

&lt;p&gt;None of this means "validate against a live bank lookup" — that's a different, heavier problem (does the account exist right now), and format validation deliberately doesn't answer it. It means: regex for shape, then the real mod-97 checksum, then country-specific length/structure — in that order, each one catching what the previous step can't.&lt;/p&gt;

&lt;p&gt;I wrapped all three checks into one endpoint on &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; if you'd rather not own the mod-97 edge cases and the per-country BBAN table yourself. Same account also covers &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; and &lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt; if useful.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>banking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Real-time currency conversion API - no signup required</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sun, 12 Jul 2026 09:46:38 +0000</pubDate>
      <link>https://dev.to/thejayedge/real-time-currency-conversion-api-no-signup-required-1a5i</link>
      <guid>https://dev.to/thejayedge/real-time-currency-conversion-api-no-signup-required-1a5i</guid>
      <description>&lt;p&gt;Most currency-conversion APIs make you register for a separate vendor account, verify an email, and sometimes wait for manual approval before you get a key — for something that should be a two-line integration. Here's a route that skips the vendor-specific signup entirely.&lt;/p&gt;

&lt;p&gt;RapidAPI's marketplace lets you subscribe with the RapidAPI key you already have (or create once, for free) rather than a new account per data provider. For &lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt;, that means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://currency-api.p.rapidapi.com/v1/convert?from=EUR&amp;amp;to=USD&amp;amp;amount=100"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Key: &amp;lt;your-rapidapi-key&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Host: currency-api.p.rapidapi.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;1.0821&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;108.21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"2026-07-11"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One RapidAPI key, and you're subscribed to every API on the platform you want to try — including this one, &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; for input validation, and &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; for QR generation.&lt;/p&gt;

&lt;p&gt;A couple of implementation details worth knowing if you're building this into a checkout or pricing display:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rates come from the European Central Bank&lt;/strong&gt; (via Frankfurter, a free keyless proxy over ECB reference rates), updated once per ECB business day — not a live tick-by-tick feed. Fine for pricing pages and invoicing; not a substitute for a trading-grade feed if you're doing FX execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail loud, not silent.&lt;/strong&gt; If the upstream rate source is unreachable, the API returns a clean &lt;code&gt;502&lt;/code&gt; instead of quietly serving a stale cached rate. For anything touching money, knowing the number is wrong beats not knowing it's stale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/v1/rates&lt;/code&gt; vs &lt;code&gt;/v1/convert&lt;/code&gt;&lt;/strong&gt; — pull the whole rate table once per base currency if you're converting to several currencies at once (one call, cache it client-side for the day), and use &lt;code&gt;/v1/convert&lt;/code&gt; for one-off single conversions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Free tier is 500,000 requests/month, no card required to start.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>tutorial</category>
      <category>finance</category>
    </item>
    <item>
      <title>How to generate QR codes for free with an API</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sun, 12 Jul 2026 09:41:32 +0000</pubDate>
      <link>https://dev.to/thejayedge/how-to-generate-qr-codes-for-free-with-an-api-43md</link>
      <guid>https://dev.to/thejayedge/how-to-generate-qr-codes-for-free-with-an-api-43md</guid>
      <description>&lt;p&gt;Generating a QR code server-side usually means pulling in a heavyweight image library, or shelling out to a third-party service that watermarks the output or rate-limits you into a paid plan. Here's the simplest version that actually works in production.&lt;/p&gt;

&lt;p&gt;The core idea: a QR code is just a grid of black/white modules encoding your data with Reed-Solomon error correction, then rendered as an image. You don't need to implement the encoding yourself — you need an endpoint that returns either a ready-to-embed SVG or the raw module matrix if you want to render it yourself (canvas, terminal, whatever):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://qr-api.p.rapidapi.com/v1/qr?data=https://example.com&amp;amp;ecc=M&amp;amp;cellSize=8"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Key: &amp;lt;your-key&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Host: qr-api.p.rapidapi.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; qr.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's an SVG you can embed directly with an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag or inline it for crisp scaling at any size — no rasterization artifacts. A few things worth knowing before you ship this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error correction level matters more than people think.&lt;/strong&gt; &lt;code&gt;H&lt;/code&gt; (30% recovery) survives a logo overlay or a slightly damaged print; &lt;code&gt;L&lt;/code&gt; (7%) is fine for a clean digital display. Don't default to the highest level everywhere — it makes the code visually denser for no benefit if nothing's ever going to obscure it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap your input length.&lt;/strong&gt; QR codes get exponentially denser as the encoded string grows. If you're encoding a URL, shorten it first; don't just dump JSON payloads into a QR code and expect a scannable result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVG over PNG when you can.&lt;/strong&gt; Vector output means no blurring when someone screenshots and enlarges it, which happens more than you'd expect with QR codes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; as a free-to-start, stateless endpoint for exactly this — no data logged, no watermark, SVG or raw matrix output. Same provider also runs &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; (IBAN/email/phone/etc. validation) and &lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt; (exchange rates) if you need those too.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Stop using regex for phone validation - use this instead</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sat, 11 Jul 2026 10:19:49 +0000</pubDate>
      <link>https://dev.to/thejayedge/stop-using-regex-for-phone-validation-use-this-instead-29e9</link>
      <guid>https://dev.to/thejayedge/stop-using-regex-for-phone-validation-use-this-instead-29e9</guid>
      <description>&lt;p&gt;A regex for "valid phone number" is a trap. International phone numbering isn't regular: number length, valid area codes, and mobile-vs-landline prefixes differ per country and change over time as ranges get reassigned. A regex that's correct today starts silently misclassifying numbers months later, and you won't notice until a support ticket comes in.&lt;/p&gt;

&lt;p&gt;The actual fix is using the numbering-plan data Google publishes and maintains for Android/Chrome: &lt;a href="https://github.com/google/libphonenumber" rel="noopener noreferrer"&gt;libphonenumber&lt;/a&gt;, or its much smaller JS port &lt;a href="https://github.com/catamphetamine/libphonenumber-js" rel="noopener noreferrer"&gt;libphonenumber-js&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;parsePhoneNumberFromString&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;libphonenumber-js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parsePhoneNumberFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+49 30 1234567&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;              &lt;span class="c1"&gt;// true - checks length + pattern for that country&lt;/span&gt;
&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                &lt;span class="c1"&gt;// "DE"&lt;/span&gt;
&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getType&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;               &lt;span class="c1"&gt;// "FIXED_LINE" | "MOBILE" | undefined (not always determinable)&lt;/span&gt;
&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;formatInternational&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// "+49 30 1234567", normalized&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things regex can't give you that this does for free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Country-aware validity.&lt;/strong&gt; "Valid length" isn't one number — it varies by country and sometimes by number type within a country. The library knows the real ranges, and gets updated when they change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalization.&lt;/strong&gt; Users type phone numbers a dozen different ways (spaces, dashes, parens, with or without country code). &lt;code&gt;formatInternational()&lt;/code&gt; gives you one canonical form to store and compare against, instead of writing your own normalization pass.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One gotcha: without a country hint, a local-format number is ambiguous. &lt;code&gt;030 1234567&lt;/code&gt; is valid in Germany and might also just be a truncated something-else from another country. If your form is scoped to one country, pass it as the default country rather than relying purely on a &lt;code&gt;+&lt;/code&gt; prefix being present.&lt;/p&gt;

&lt;p&gt;If your backend isn't JS (or you don't want the dependency in a small service), I wrapped libphonenumber-js as a hosted endpoint on &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; — same validation logic, plain JSON in and out.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>IBAN validation guide for developers</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sat, 11 Jul 2026 10:14:40 +0000</pubDate>
      <link>https://dev.to/thejayedge/iban-validation-guide-for-developers-pbn</link>
      <guid>https://dev.to/thejayedge/iban-validation-guide-for-developers-pbn</guid>
      <description>&lt;p&gt;IBAN validation trips people up because it looks like it should be a length check plus a regex, and it's neither. Here's the actual structure, in the order you should check it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Length and country format.&lt;/strong&gt; Every IBAN starts with a 2-letter ISO country code and 2 check digits, followed by a country-specific BBAN whose length is fixed &lt;em&gt;per country&lt;/em&gt; — Germany is always 22 characters, the Netherlands 18, Malta 31. A string can be the right shape (letters-then-digits) and still be the wrong length for the country it claims to be from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The mod-97 checksum (ISO 7064).&lt;/strong&gt; This is the part people get wrong or skip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isValidIBAN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iban&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;iban&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;]{2}\d{2}[&lt;/span&gt;&lt;span class="sr"&gt;A-Z0-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rearranged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rearranged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="c1"&gt;// IBANs convert to numbers far too large for JS's Number type (30+ digits),&lt;/span&gt;
  &lt;span class="c1"&gt;// so mod 97 has to be computed in chunks rather than in one BigInt/Number op.&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remainder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;remainder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;remainder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remainder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;97&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Move the first 4 characters to the end, convert every letter to its numeric value (A=10 ... Z=35), and the resulting number mod 97 must equal 1. The chunked-modulo loop is the detail that trips up most from-scratch implementations — you can't just do &lt;code&gt;BigInt(numeric) % 97n&lt;/code&gt; in older runtimes without a BigInt polyfill, and plain &lt;code&gt;Number&lt;/code&gt; silently loses precision past 2^53.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. What this does &lt;em&gt;not&lt;/em&gt; tell you.&lt;/strong&gt; A passing checksum means the IBAN is well-formed — not that the account exists, is open, or belongs to who the payer thinks it does. Confirming that needs a live lookup against the bank (or a service like the account-name-matching checks banks now run for fraud prevention), which is a separate, heavier operation than format validation.&lt;/p&gt;

&lt;p&gt;For most apps — checkout forms, payout setup, onboarding — steps 1 and 2 are exactly the right amount of validation: fast, no external calls, no third-party uptime dependency. I wrapped both (plus the per-country BBAN structure check) into an endpoint on &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; if you'd rather not own the mod-97 edge cases yourself.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>tutorial</category>
      <category>banking</category>
    </item>
    <item>
      <title>How to validate email addresses properly in 2026</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sat, 11 Jul 2026 10:09:48 +0000</pubDate>
      <link>https://dev.to/thejayedge/how-to-validate-email-addresses-properly-in-2026-4j0g</link>
      <guid>https://dev.to/thejayedge/how-to-validate-email-addresses-properly-in-2026-4j0g</guid>
      <description>&lt;p&gt;"Just use a regex" is the most common wrong answer to email validation. Here's what actually matters, in the order it actually matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Syntax — but a permissive one.&lt;/strong&gt; The RFC 5322 spec technically allows things like quoted strings and comments in the local part, which almost no real mail provider uses. Don't implement the full spec; use a pragmatic check instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;looksLikeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+@&lt;/span&gt;&lt;span class="se"&gt;[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\.[^\s&lt;/span&gt;&lt;span class="sr"&gt;@&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+$/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This deliberately under-validates. A regex that's &lt;em&gt;too&lt;/em&gt; strict will reject real addresses (plus-addressing, unusual-but-valid TLDs) more often than it catches typos — false rejections cost you signups, false acceptances just mean you fall through to the next check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. MX record lookup.&lt;/strong&gt; Syntax passing doesn't mean the domain can receive mail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolveMx&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:dns/promises&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canReceiveMail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;resolveMx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Catches typo'd domains (&lt;code&gt;gmial.com&lt;/code&gt;) and abandoned/fake domains for basically zero added latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Disposable/temp-mail detection.&lt;/strong&gt; Syntax-valid, MX-valid, and still worthless for your product: throwaway addresses from 10minutemail, guerrillamail, mailinator, and hundreds of similar services that spin up new domains constantly. There's no algorithmic tell — you need a maintained domain blocklist (the community-run &lt;a href="https://github.com/disposable-email-domains/disposable-email-domains" rel="noopener noreferrer"&gt;disposable-email-domains&lt;/a&gt; list is a solid free start, just budget time to keep it synced).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. What you're deliberately &lt;em&gt;not&lt;/em&gt; doing:&lt;/strong&gt; an SMTP handshake to check if the specific mailbox exists. It's slow, unreliable, and most mail servers now treat probing behavior as spam reconnaissance and silently no-op it. Don't build on a foundation that stopped being reliable.&lt;/p&gt;

&lt;p&gt;Put together, that's syntax (fast, always run) → MX (cheap, catches typos) → disposable-domain check (catches throwaway signups) — each step only worth running if the previous one passed. I built exactly this pipeline as endpoints on &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; if you'd rather not maintain the MX/disposable-list plumbing yourself.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Validate API changelog: disposable-email detection and postal-code validation</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Wed, 08 Jul 2026 20:35:43 +0000</pubDate>
      <link>https://dev.to/thejayedge/validate-api-changelog-disposable-email-detection-and-postal-code-validation-4e6e</link>
      <guid>https://dev.to/thejayedge/validate-api-changelog-disposable-email-detection-and-postal-code-validation-4e6e</guid>
      <description>&lt;p&gt;We just shipped two new endpoints on &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt;, the validation/utility API — both format-only checks, no external services called, same pattern as the existing IBAN/VAT checksum endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;POST /v1/validate/disposable-email&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Flags throwaway/temp-mail addresses — Mailinator, Guerrilla Mail, 10-minute-mail, YOPmail, and 80+ other known providers, including their subdomains (so &lt;code&gt;abc.mailinator.com&lt;/code&gt; still gets caught). Real email providers (Gmail, Outlook, your own domain, etc.) are never flagged.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://validate7.p.rapidapi.com/v1/validate/disposable-email"&lt;/span&gt;   &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Key: &amp;lt;your-key&amp;gt;"&lt;/span&gt;   &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Host: validate7.p.rapidapi.com"&lt;/span&gt;   &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt;   &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email": "test@mailinator.com"}'&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; {"syntaxValid":true,"domain":"mailinator.com","disposable":true,"errors":[]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful paired with the existing &lt;code&gt;/v1/validate/email&lt;/code&gt; endpoint (syntax + MX check) to reject fake signups without blocking real users.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;POST /v1/validate/postal-code&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Format validation for 50+ countries — US, CA, GB, DE, FR, and most of the EU/APAC/LatAm. Same idea as the IBAN mod-97 checksum: structure-only, no claim that the code is currently assigned by the postal service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://validate7.p.rapidapi.com/v1/validate/postal-code"&lt;/span&gt;   &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Key: &amp;lt;your-key&amp;gt;"&lt;/span&gt;   &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-RapidAPI-Host: validate7.p.rapidapi.com"&lt;/span&gt;   &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt;   &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"countryCode": "US", "postalCode": "94103"}'&lt;/span&gt;
&lt;span class="c"&gt;# =&amp;gt; {"valid":true,"countryCode":"US","postalCode":"94103","supported":true,"errors":[]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also &lt;code&gt;GET /v1/validate/postal-code/countries&lt;/code&gt; if you need the full list of supported country codes.&lt;/p&gt;

&lt;p&gt;Both are live now on the same free tier as everything else. If there's a specific validation check you keep re-implementing that isn't covered yet, I'd genuinely like to hear about it — that's exactly how these two got picked.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Entropy-based password strength scoring (and why 'has a symbol' rules are theater)</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Wed, 08 Jul 2026 13:42:01 +0000</pubDate>
      <link>https://dev.to/thejayedge/entropy-based-password-strength-scoring-and-why-has-a-symbol-rules-are-theater-2nph</link>
      <guid>https://dev.to/thejayedge/entropy-based-password-strength-scoring-and-why-has-a-symbol-rules-are-theater-2nph</guid>
      <description>&lt;p&gt;"Must contain one uppercase, one number, one symbol" rules produce passwords like &lt;code&gt;Password1!&lt;/code&gt; — technically compliant, trivially guessable. Entropy-based scoring is a better signal.&lt;/p&gt;

&lt;p&gt;The idea: estimate the search space an attacker would need to brute-force, based on character variety and length, then flag patterns that reduce real entropy below what the raw math suggests (dictionary words, keyboard walks, repeated characters).&lt;/p&gt;

&lt;p&gt;Rough approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;estimateEntropyBits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;poolSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;a-z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;poolSize&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;poolSize&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;0-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;poolSize&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;a-zA-Z0-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;poolSize&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poolSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the naive version — it overestimates for anything with patterns. A real implementation should also penalize repeated substrings and common dictionary words before trusting the raw bits figure.&lt;/p&gt;

&lt;p&gt;I built a fuller version of this (plus the breach-check endpoint from k-anonymity) as part of &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; if you want the batteries-included version.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
