<?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>Stop rolling your own password breach checker — here's how k-anonymity makes it safe</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Tue, 11 Aug 2026 10:09:20 +0000</pubDate>
      <link>https://dev.to/thejayedge/stop-rolling-your-own-password-breach-checker-heres-how-k-anonymity-makes-it-safe-285e</link>
      <guid>https://dev.to/thejayedge/stop-rolling-your-own-password-breach-checker-heres-how-k-anonymity-makes-it-safe-285e</guid>
      <description>&lt;p&gt;If you're checking user passwords against HaveIBeenPwned's breach database, please don't send the full password (or even the full hash) over the wire. Here's why, and the actual protocol that fixes it.&lt;/p&gt;

&lt;p&gt;HIBP's Pwned Passwords API uses k-anonymity: you SHA-1 hash the password locally, send only the first 5 characters of the hash, and the API returns every suffix that starts with that prefix (usually 300-900 of them). You compare locally. The full password never leaves your server, and HIBP never sees enough to reconstruct it.&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;createHash&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:crypto&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;isBreached&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;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&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="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hash&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;5&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;suffix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hash&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;5&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.pwnedpasswords.com/range/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&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;text&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="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suffix&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 genuinely all it takes — no library needed. I wrapped this (plus password strength scoring) 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 maintain the hashing/comparison logic yourself, but honestly, the snippet above will get most people there. 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>security</category>
      <category>webdev</category>
      <category>api</category>
      <category>javascript</category>
    </item>
    <item>
      <title>ISO 4217 currency codes: the edge cases that break naive multi-currency forms</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sat, 08 Aug 2026 10:02:11 +0000</pubDate>
      <link>https://dev.to/thejayedge/iso-4217-currency-codes-the-edge-cases-that-break-naive-multi-currency-forms-3hen</link>
      <guid>https://dev.to/thejayedge/iso-4217-currency-codes-the-edge-cases-that-break-naive-multi-currency-forms-3hen</guid>
      <description>&lt;p&gt;Most currency handling code assumes every currency has 2 decimal places and a straightforward numeric amount. Neither assumption holds universally, and both break in ways that are easy to miss until a real transaction hits the edge case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not every currency has 2 minor units.&lt;/strong&gt; JPY has zero — ¥100 is a whole number, there's no ¥100.00. KWD (Kuwaiti Dinar) has three. If your form/database column hardcodes 2 decimal places "because that's how currency works," you'll either round JPY amounts into nonsense or truncate KWD precision.&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;const&lt;/span&gt; &lt;span class="nx"&gt;MINOR_UNITS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;JPY&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="na"&gt;KWD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// default 2 for anything not listed&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatAmount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&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;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MINOR_UNITS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;2&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;amount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digits&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;Currency codes aren't a stable, closed set forever&lt;/strong&gt; — they get added, and in rare cases retired (old Eurozone currencies like DEM, FRF were withdrawn when the Euro launched). A hardcoded currency list baked into your app years ago can silently drift from reality. Pull the supported-currency list from a source that's actually kept current, rather than a list you typed once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symbol collisions are a UI bug waiting to happen.&lt;/strong&gt; &lt;code&gt;$&lt;/code&gt; alone is ambiguous across USD, CAD, AUD, and several others — showing just the symbol without the ISO code next to it (or as a tooltip) is a real source of "wait, is that dollars or my dollars" confusion in any multi-currency UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt;'s &lt;code&gt;/v1/currencies&lt;/code&gt; endpoint always reflects the actual current supported set (ECB scope, ~30 major currencies) rather than a list that can go stale — check it instead of hardcoding one, especially if you're validating currency-code input from a form. Sibling API &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; is useful if that same form flow ever needs a QR code for a payment link, and &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; covers the rest of the form's IBAN/email/phone checks.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>finance</category>
      <category>api</category>
    </item>
    <item>
      <title>The IBAN mod-97 checksum, explained (and how to validate one in one line)</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Tue, 04 Aug 2026 16:28:32 +0000</pubDate>
      <link>https://dev.to/thejayedge/the-iban-mod-97-checksum-explained-and-how-to-validate-one-in-one-line-3abo</link>
      <guid>https://dev.to/thejayedge/the-iban-mod-97-checksum-explained-and-how-to-validate-one-in-one-line-3abo</guid>
      <description>&lt;p&gt;Ever wondered how your bank instantly knows you fat-fingered an IBAN before it even checks if the account exists? It's a checksum: ISO 13616's mod-97 algorithm.&lt;/p&gt;

&lt;p&gt;Here's the short version: move the first four characters to the end, convert letters to numbers (A=10, B=11...), then check if the resulting number mod 97 equals 1. That's it — no database lookup needed to catch most typos.&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="c1"&gt;// simplified version of the check&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ibanChecksumValid&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;rearranged&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;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;iban&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="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="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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &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;digit&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;numeric&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="nx"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digit&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;remainder&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;Fine for a side project. Once you're validating IBANs from real users, you also want country-specific length checks, BBAN structure validation, and ideally a formatted, human-readable output — which is more code than you want to own for something this boilerplate.&lt;/p&gt;

&lt;p&gt;If you'd rather not maintain that: &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; is a small API I built that handles this (plus VAT/email/phone/credit-card format checks) as a single JSON call. Free tier is 100 requests/month, no card required. Same account also runs &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; (QR code generation) and &lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt; (exchange rates) if useful.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Caching exchange rates correctly: per-day TTL, not per-request memoization</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sat, 01 Aug 2026 12:41:30 +0000</pubDate>
      <link>https://dev.to/thejayedge/caching-exchange-rates-correctly-per-day-ttl-not-per-request-memoization-48bf</link>
      <guid>https://dev.to/thejayedge/caching-exchange-rates-correctly-per-day-ttl-not-per-request-memoization-48bf</guid>
      <description>&lt;p&gt;If you're converting to several currencies at once — a pricing page showing a product in 8 currencies, say — the naive approach is one &lt;code&gt;/convert&lt;/code&gt; call per currency pair. That works, but it's leaving an easy optimization on the table: ECB-sourced rates update once per business day, so there's no reason to be making fresh network calls more often than that.&lt;/p&gt;

&lt;p&gt;The right cache key isn't "this specific conversion," it's "the rate table for this base currency, as of today's date":&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;const&lt;/span&gt; &lt;span class="nx"&gt;rateCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// key: `${base}:${isoDate}`&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;getRatesForDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&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;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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;10&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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="nx"&gt;rateCache&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;key&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;rateCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://currency-api.p.rapidapi.com/v1/rates?base=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-RapidAPI-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key_env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-RapidAPI-Host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;currency-api.p.rapidapi.com&lt;/span&gt;&lt;span class="dl"&gt;"&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;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;rateCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&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;data&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;One call per base currency per day gets you every target currency's rate in that response — &lt;code&gt;/v1/rates&lt;/code&gt; returns the full table, not just one pair — so converting to 8 currencies from a cached table is 8 in-memory lookups, not 8 network round-trips.&lt;/p&gt;

&lt;p&gt;Two things worth being careful about: cache the &lt;em&gt;date the rate was published&lt;/em&gt;, not the date you fetched it (a request right after midnight UTC might still be serving yesterday's ECB publish if today's hasn't landed yet), and don't cache across a process restart without also persisting the date key — a stale in-memory cache that outlives its day is exactly the silent-wrong-number problem you're trying to avoid.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt;'s &lt;code&gt;/v1/rates&lt;/code&gt; endpoint is built for this pattern — pull the whole table once, cache it client-side for the day, and reserve &lt;code&gt;/v1/convert&lt;/code&gt; for one-off single conversions where pulling the whole table would be overkill. Sibling APIs on the same account: &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; and &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>performance</category>
    </item>
    <item>
      <title>The QR code capacity cliff: why a shortened URL scans more reliably than a long one</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Thu, 30 Jul 2026 16:01:00 +0000</pubDate>
      <link>https://dev.to/thejayedge/the-qr-code-capacity-cliff-why-a-shortened-url-scans-more-reliably-than-a-long-one-1l34</link>
      <guid>https://dev.to/thejayedge/the-qr-code-capacity-cliff-why-a-shortened-url-scans-more-reliably-than-a-long-one-1l34</guid>
      <description>&lt;p&gt;QR codes don't degrade gracefully as you stuff more data into them — they hit a capacity ceiling per version/error-correction combination, and the module grid gets denser and denser as you approach it. Past a certain point you're not looking at a QR code you can scan from arm's length anymore, you're looking at one that needs to nearly touch the camera lens to resolve.&lt;/p&gt;

&lt;p&gt;The numbers, roughly: at the lowest error correction level (L), a QR code tops out around 2,900 alphanumeric characters or ~4,300 numeric-only characters. That sounds like a lot until you remember two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every character costs more space than you'd think&lt;/strong&gt;, because the encoding also has to carry the error-correction payload alongside your data — that's not overhead you can opt out of, it's the whole point of the format being scannable at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-world scanning distance matters more than theoretical capacity.&lt;/strong&gt; A QR code sized for a business card that's encoding near the character ceiling will produce a module grid so fine-grained that a phone camera needs to be uncomfortably close to resolve individual modules.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix is almost always: don't encode raw data, encode a short pointer to it. A URL-shortened link, a UUID that looks up a record server-side, a short numeric ID — anything that keeps the encoded payload well under a hundred characters keeps the module grid coarse enough to scan reliably from a normal distance, at a normal print size.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# fine — short, scans easily even printed small&lt;/span&gt;
curl &lt;span class="s2"&gt;"https://qr-api.p.rapidapi.com/v1/qr?data=https://short.link/x7f2"&lt;/span&gt; ...

&lt;span class="c"&gt;# avoid — encoding a full JSON payload directly makes the grid dense&lt;/span&gt;
&lt;span class="c"&gt;# and print-size-sensitive for no real benefit over a lookup pointer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're building this into a product, cap the accepted input length and fail with a clear error before hitting the encoder's own hard limit — silently truncating someone's data is worse than telling them up front. &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; caps &lt;code&gt;data&lt;/code&gt; at 2000 characters for exactly this reason: comfortably under the spec's own ~2900-character ceiling at the lowest ECC level, with a clear 4xx instead of an encoder crash past it. Same account also runs &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&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>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Validating a CSV of 500 emails or IBANs? Stop doing it one HTTP call at a time</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Tue, 28 Jul 2026 16:17:15 +0000</pubDate>
      <link>https://dev.to/thejayedge/validating-a-csv-of-500-emails-or-ibans-stop-doing-it-one-http-call-at-a-time-50j2</link>
      <guid>https://dev.to/thejayedge/validating-a-csv-of-500-emails-or-ibans-stop-doing-it-one-http-call-at-a-time-50j2</guid>
      <description>&lt;p&gt;Cleaning a signup list or a vendor-payout CSV against a single-item validation endpoint means one HTTP round-trip per row — 500 rows means 500 requests, most of which burn through a rate limit before you're a quarter of the way through the file.&lt;/p&gt;

&lt;p&gt;The fix isn't a faster loop, it's an endpoint that accepts the whole array and validates it server-side in one call:&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 https://validate-api.p.rapidapi.com/v1/batch/iban &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: validate-api.p.rapidapi.com"&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;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"ibans": ["DE89370400440532013000", "not-an-iban", "FR1420041010050500013M02606"]}'&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;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE89370400440532013000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"countryCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not-an-iban"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"countryCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"IBAN contains invalid characters"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FR1420041010050500013M02606"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"countryCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]}&lt;/span&gt;&lt;span class="w"&gt;
&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;Same checksum logic as the single-item endpoint, just run once per array item instead of once per request — order-preserving output, so row &lt;em&gt;n&lt;/em&gt; in the response always maps to row &lt;em&gt;n&lt;/em&gt; in your input.&lt;/p&gt;

&lt;p&gt;One design choice worth calling out if you're building something similar: batch email validation defaults MX-record checking to &lt;strong&gt;off&lt;/strong&gt;, unlike the single-item endpoint. A batch of 50 emails with MX checking on means 50 DNS lookups fired from one request — fine occasionally, not something you want as the default for a bulk endpoint. Opt in explicitly (&lt;code&gt;checkMx: true&lt;/code&gt;) when you actually need it.&lt;/p&gt;

&lt;p&gt;I added &lt;code&gt;/v1/batch/iban&lt;/code&gt; (up to 100 items) and &lt;code&gt;/v1/batch/email&lt;/code&gt; (up to 50 items) to &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; for exactly this list-cleaning use case — same free tier, no separate pricing for batch. 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>api</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Why a currency API should return 502, not a stale rate, when the upstream is down</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sat, 25 Jul 2026 12:14:15 +0000</pubDate>
      <link>https://dev.to/thejayedge/why-a-currency-api-should-return-502-not-a-stale-rate-when-the-upstream-is-down-1iab</link>
      <guid>https://dev.to/thejayedge/why-a-currency-api-should-return-502-not-a-stale-rate-when-the-upstream-is-down-1iab</guid>
      <description>&lt;p&gt;A tempting shortcut when your rate source is unreachable: serve the last cached rate and let the request succeed. For most APIs that's a reasonable degradation strategy. For anything touching money, it's the wrong default.&lt;/p&gt;

&lt;p&gt;The problem isn't that a stale rate is wrong — it's that a stale rate &lt;em&gt;looks&lt;/em&gt; identical to a correct one from the caller's side. A 200 response with a slightly-off number is far more dangerous than an honest failure, because nothing downstream knows to treat the value with suspicion. A checkout flow that silently converts at yesterday's rate doesn't fail loudly; it just quietly overcharges or undercharges, and nobody notices until a reconciliation problem shows up much later.&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRateOrFail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;upstreamUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`upstream returned &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Explicit failure, not a stale fallback — the caller needs to know&lt;/span&gt;
    &lt;span class="c1"&gt;// "we don't have a trustworthy number right now," not get a wrong one.&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;UpstreamError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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;The tradeoff this makes explicit: you're accepting worse availability (a 502 during an upstream outage) in exchange for a hard guarantee that every successful response is a real, current-as-of-its-timestamp rate — never a guess dressed up as data.&lt;/p&gt;

&lt;p&gt;This is the one place &lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt;'s "no fabricated data" principle gets tested in practice: if Frankfurter (the ECB rate proxy it uses) is unreachable, the API returns a clean &lt;code&gt;502 upstream_error&lt;/code&gt; instead of quietly serving whatever it last had cached. Same principle sibling API &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; applies to its VIES/MX lookups — fail honestly rather than silently degrade on anything where the caller can't tell the difference between fresh and stale. Third sibling API on the same account: &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>reliability</category>
    </item>
    <item>
      <title>SVG output for QR codes: why vector beats raster once anyone screenshots or resizes it</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Thu, 23 Jul 2026 16:03:27 +0000</pubDate>
      <link>https://dev.to/thejayedge/svg-output-for-qr-codes-why-vector-beats-raster-once-anyone-screenshots-or-resizes-it-3913</link>
      <guid>https://dev.to/thejayedge/svg-output-for-qr-codes-why-vector-beats-raster-once-anyone-screenshots-or-resizes-it-3913</guid>
      <description>&lt;p&gt;Raster QR codes (PNG/JPEG) look fine at the size they were generated for and fall apart the moment someone resizes, screenshots, or prints them larger — the module edges blur, and blurred module edges are exactly what breaks a decoder's ability to read the grid.&lt;/p&gt;

&lt;p&gt;SVG sidesteps this completely because a QR code is fundamentally vector data already — it's a grid of black/white squares, not a photograph. Rendering it as &lt;code&gt;&amp;lt;rect&amp;gt;&lt;/code&gt; elements instead of a pixel bitmap means it stays crisp at any zoom level, print size, or screen density:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"qr.svg"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"Scan to open"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"600"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a 600px display of a code that could've been generated at any size — no upscaling artifacts, because there was never a fixed pixel grid to begin with.&lt;/p&gt;

&lt;p&gt;A couple of practical notes if you're embedding these in a product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inline the SVG&lt;/strong&gt; (rather than referencing it as an &lt;code&gt;&amp;lt;img src&amp;gt;&lt;/code&gt;) if you need to style fill colors with CSS at render time instead of baking colors in server-side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict hex validation on any color params matters more than it looks&lt;/strong&gt; — if a QR generation endpoint lets you pass custom foreground/background colors and embeds them directly into SVG attributes, that's an injection surface if the values aren't validated as actual hex colors first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw matrix output is still useful&lt;/strong&gt; — for canvas rendering, terminal output, or anything that isn't "embed an image," getting the boolean module grid directly beats parsing an SVG back into pixels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; returns &lt;code&gt;image/svg+xml&lt;/code&gt; by default from &lt;code&gt;GET /v1/qr&lt;/code&gt;, with a &lt;code&gt;/v1/qr/matrix&lt;/code&gt; variant returning the raw grid for exactly that second use case. Sibling APIs on the same account: &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&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>api</category>
      <category>svg</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>ECB reference rates vs live FX feeds: what 'updated once a day' actually costs you</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Tue, 21 Jul 2026 13:34:59 +0000</pubDate>
      <link>https://dev.to/thejayedge/ecb-reference-rates-vs-live-fx-feeds-what-updated-once-a-day-actually-costs-you-1h66</link>
      <guid>https://dev.to/thejayedge/ecb-reference-rates-vs-live-fx-feeds-what-updated-once-a-day-actually-costs-you-1h66</guid>
      <description>&lt;p&gt;Most currency-conversion needs in a typical web app — pricing pages, invoicing, displaying a rough converted total — don't need a tick-by-tick FX feed, but it's worth being explicit about what you're trading away when you use ECB reference rates (published once per business day) instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What ECB rates are good for:&lt;/strong&gt; pricing display, invoicing, order totals shown to a customer, anything where "the rate as of this morning" is an acceptable, disclosed approximation. They're free, stable, and don't require an account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What they're not good for:&lt;/strong&gt; FX execution, arbitrage, anything where being a few hours stale has real financial consequences. If you're actually trading currency, not just displaying a converted number, you need a feed that updates far more often than once a day — and probably isn't free.&lt;/p&gt;

&lt;p&gt;The practical pattern that works for the first category:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://currency-api.p.rapidapi.com/v1/convert?from=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;to=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-RapidAPI-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-RapidAPI-Host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;currency-api.p.rapidapi.com&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// { rate, result, date } — "date" tells you exactly how fresh this is&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;date&lt;/code&gt; field matters more than people give it credit for — surfacing "rate as of {date}" next to a converted price, rather than presenting it as live, sets the right expectation and avoids a support ticket when someone notices the number doesn't match what their bank showed them an hour later.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt; is built on exactly this tradeoff: ECB reference rates via Frankfurter, free and keyless, explicit about being once-a-day data rather than pretending to be a live feed.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>finance</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>QR code error correction levels: L vs M vs Q vs H, and when the tradeoff is actually worth it</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Tue, 21 Jul 2026 13:34:55 +0000</pubDate>
      <link>https://dev.to/thejayedge/qr-code-error-correction-levels-l-vs-m-vs-q-vs-h-and-when-the-tradeoff-is-actually-worth-it-4h6d</link>
      <guid>https://dev.to/thejayedge/qr-code-error-correction-levels-l-vs-m-vs-q-vs-h-and-when-the-tradeoff-is-actually-worth-it-4h6d</guid>
      <description>&lt;p&gt;Every QR code has a Reed-Solomon error-correction level baked in — L (~7% recovery), M (~15%), Q (~25%), H (~30%) — and picking the wrong one either wastes visual density or leaves you with a code that stops scanning the moment it gets a little damaged.&lt;/p&gt;

&lt;p&gt;Here's the actual decision, not just the percentages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;L&lt;/strong&gt; — clean digital display only (a code shown on a screen, never printed, never overlaid with anything). Smallest, least dense module grid for a given payload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;M&lt;/strong&gt; — the safe default for most printed material. Handles minor print smudging or a slightly low-quality printer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q&lt;/strong&gt; — printed material that gets handled a lot (product packaging, shipping labels) or is at moderate risk of scuffing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;H&lt;/strong&gt; — anything with a logo or graphic overlaid on top of the code, since that overlay is itself "damage" the decoder has to recover from. Also the right call for outdoor/weather-exposed prints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mistake I see most often: defaulting to H everywhere "to be safe." Higher error correction means more modules for the same data, which means a denser, harder-to-scan-from-a-distance code with zero benefit if nothing's ever going to obscure it. Match the level to the actual damage risk, not to the theoretical maximum.&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=Q"&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;&lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; exposes &lt;code&gt;ecc&lt;/code&gt; as a plain query param (defaults to &lt;code&gt;M&lt;/code&gt;) so you can set it per use case instead of hardcoding one level everywhere.&lt;/p&gt;

</description>
      <category>webdev</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>Tue, 21 Jul 2026 13:25:45 +0000</pubDate>
      <link>https://dev.to/thejayedge/the-complete-guide-to-disposable-email-detection-131d</link>
      <guid>https://dev.to/thejayedge/the-complete-guide-to-disposable-email-detection-131d</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>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>
  </channel>
</rss>
