<?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>ISO 4217 currency codes: the edge cases that break naive multi-currency forms</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sat, 26 Sep 2026 13:48:04 +0000</pubDate>
      <link>https://dev.to/thejayedge/iso-4217-currency-codes-the-edge-cases-that-break-naive-multi-currency-forms-15db</link>
      <guid>https://dev.to/thejayedge/iso-4217-currency-codes-the-edge-cases-that-break-naive-multi-currency-forms-15db</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>Batch-generating QR codes for events, inventory, or ticketing without hammering an API</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Thu, 24 Sep 2026 14:15:10 +0000</pubDate>
      <link>https://dev.to/thejayedge/batch-generating-qr-codes-for-events-inventory-or-ticketing-without-hammering-an-api-3ak1</link>
      <guid>https://dev.to/thejayedge/batch-generating-qr-codes-for-events-inventory-or-ticketing-without-hammering-an-api-3ak1</guid>
      <description>&lt;p&gt;Generating one QR code per attendee badge, warehouse bin, or ticket sounds like a simple loop until you're doing it for a few thousand items and either your own rate limiter or the API's kicks in halfway through a batch job.&lt;/p&gt;

&lt;p&gt;A few things that make batch QR generation less painful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrency, not sequential loops.&lt;/strong&gt; A single QR code generation call is fast (no external network dependency if the service computes the matrix in-process rather than calling out to render it), so the bottleneck is almost always your own request-issuing pattern, not the API. A small worker pool (5-10 concurrent requests) clears a few thousand codes in well under a minute instead of one-at-a-time sequential awaiting.&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;generateBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;concurrency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&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;results&lt;/span&gt; &lt;span class="o"&gt;=&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;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;items&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;concurrency&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;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&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;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;concurrency&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;responses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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://qr-api.p.rapidapi.com/v1/qr?data=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RAPIDAPI_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;qr-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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&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="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;responses&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;results&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;Respect the tier's rate limit rather than discovering it via 429s.&lt;/strong&gt; If you know you're generating a few thousand codes in one job, check your plan's requests-per-month ceiling ahead of time rather than mid-batch — free tiers are usually sized for "generate codes as users request them," not "bulk-generate an entire event's worth in one run."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache the output if the input doesn't change.&lt;/strong&gt; A QR code for a fixed URL/payload is deterministic — regenerating the same code repeatedly across job re-runs (a common failure mode when a batch job is re-triggered after a partial failure) wastes calls for zero benefit. Key your cache on the exact &lt;code&gt;data&lt;/code&gt;+&lt;code&gt;ecc&lt;/code&gt;+size params.&lt;/p&gt;

&lt;p&gt;Stateless generation (no per-request server state, no database) is what makes this kind of batch job cheap to run in the first place — that's the whole design of &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt;: every call is a pure function of its query params, nothing persisted, nothing logged beyond the request path. Same account also runs &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; (IBAN/email/phone/etc. format checks) and &lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt; (exchange rates) if useful for the rest of an onboarding/checkout pipeline.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to validate email addresses properly in 2026</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Tue, 22 Sep 2026 14:01:11 +0000</pubDate>
      <link>https://dev.to/thejayedge/how-to-validate-email-addresses-properly-in-2026-3pj3</link>
      <guid>https://dev.to/thejayedge/how-to-validate-email-addresses-properly-in-2026-3pj3</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. Same account also runs &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>Caching exchange rates correctly: per-day TTL, not per-request memoization</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sat, 19 Sep 2026 13:16:30 +0000</pubDate>
      <link>https://dev.to/thejayedge/caching-exchange-rates-correctly-per-day-ttl-not-per-request-memoization-512</link>
      <guid>https://dev.to/thejayedge/caching-exchange-rates-correctly-per-day-ttl-not-per-request-memoization-512</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, 17 Sep 2026 14:19:51 +0000</pubDate>
      <link>https://dev.to/thejayedge/the-qr-code-capacity-cliff-why-a-shortened-url-scans-more-reliably-than-a-long-one-4ll0</link>
      <guid>https://dev.to/thejayedge/the-qr-code-capacity-cliff-why-a-shortened-url-scans-more-reliably-than-a-long-one-4ll0</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>MX record checks: the cheap email validation step everyone skips</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Tue, 15 Sep 2026 14:19:14 +0000</pubDate>
      <link>https://dev.to/thejayedge/mx-record-checks-the-cheap-email-validation-step-everyone-skips-25d6</link>
      <guid>https://dev.to/thejayedge/mx-record-checks-the-cheap-email-validation-step-everyone-skips-25d6</guid>
      <description>&lt;p&gt;Regex-validating an email address only tells you the &lt;em&gt;syntax&lt;/em&gt; is plausible. It says nothing about whether &lt;code&gt;user@totallyfakedomain12345.com&lt;/code&gt; can receive mail. An MX record lookup closes that gap for basically zero extra latency.&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;domainCanReceiveMail&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;records&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;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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;records&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;This won't catch every typo (an MX record existing doesn't mean the specific mailbox exists — that needs an actual SMTP handshake, which is slow, unreliable, and often blocked by mail servers as spam-probing behavior). But it's a strong, cheap signal that catches a meaningful chunk of junk signups before you send a verification email into the void.&lt;/p&gt;

&lt;p&gt;I run this as an optional flag on the email endpoint of &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; — syntax check always runs, MX check is opt-in since it adds a DNS round-trip. 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>
    </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, 12 Sep 2026 12:59:36 +0000</pubDate>
      <link>https://dev.to/thejayedge/why-a-currency-api-should-return-502-not-a-stale-rate-when-the-upstream-is-down-50mf</link>
      <guid>https://dev.to/thejayedge/why-a-currency-api-should-return-502-not-a-stale-rate-when-the-upstream-is-down-50mf</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, 10 Sep 2026 13:40:11 +0000</pubDate>
      <link>https://dev.to/thejayedge/svg-output-for-qr-codes-why-vector-beats-raster-once-anyone-screenshots-or-resizes-it-1ak1</link>
      <guid>https://dev.to/thejayedge/svg-output-for-qr-codes-why-vector-beats-raster-once-anyone-screenshots-or-resizes-it-1ak1</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>Why I put input validation behind an API instead of a shared npm package</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Tue, 08 Sep 2026 13:36:28 +0000</pubDate>
      <link>https://dev.to/thejayedge/why-i-put-input-validation-behind-an-api-instead-of-a-shared-npm-package-ihc</link>
      <guid>https://dev.to/thejayedge/why-i-put-input-validation-behind-an-api-instead-of-a-shared-npm-package-ihc</guid>
      <description>&lt;p&gt;Most validation logic (IBAN, VAT, phone, email, credit card format) is language-agnostic — it's just string parsing and checksums. So why did I build it as an HTTP API instead of a JS library?&lt;/p&gt;

&lt;p&gt;A few reasons that mattered to me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Polyglot by default.&lt;/strong&gt; An HTTP API works from Python, Go, Ruby, whatever — a JS package only works in JS projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No dependency upgrades to babysit.&lt;/strong&gt; libphonenumber's metadata changes as countries update numbering plans; that's my problem to keep current, not every consumer's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The VIES/MX-record/breach-check lookups need a server anyway.&lt;/strong&gt; Those aren't pure functions — they're network calls. Once you need a server for those, you may as well put the offline checks behind the same API for consistency.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff is obviously latency — a network round-trip beats a local function call every time. For anything on a hot path doing thousands of validations per second, a local library is the right call. For "validate this form submission once," the API overhead is noise.&lt;/p&gt;

&lt;p&gt;If you want to see the actual shape of it: &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate on RapidAPI&lt;/a&gt; — free tier, no card required. Source structure is public too: &lt;a href="https://github.com/Jay-Ecommerce/validate-api" rel="noopener noreferrer"&gt;https://github.com/Jay-Ecommerce/validate-api&lt;/a&gt;. Same account also runs &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>opinion</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>Sat, 05 Sep 2026 12:54:28 +0000</pubDate>
      <link>https://dev.to/thejayedge/ecb-reference-rates-vs-live-fx-feeds-what-updated-once-a-day-actually-costs-you-19m5</link>
      <guid>https://dev.to/thejayedge/ecb-reference-rates-vs-live-fx-feeds-what-updated-once-a-day-actually-costs-you-19m5</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. Same account also runs &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; (IBAN/email/phone/etc. format checks) and &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; (QR code generation) if useful for the rest of a checkout/invoicing flow.&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>Thu, 03 Sep 2026 13:39:01 +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-3kb5</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-3kb5</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. Same account also runs &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; (IBAN/email/phone/etc. format checks) and &lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt; (exchange rates).&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Luhn's algorithm: the 70-year-old checksum still validating every card you own</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Tue, 01 Sep 2026 14:07:54 +0000</pubDate>
      <link>https://dev.to/thejayedge/luhns-algorithm-the-70-year-old-checksum-still-validating-every-card-you-own-g8m</link>
      <guid>https://dev.to/thejayedge/luhns-algorithm-the-70-year-old-checksum-still-validating-every-card-you-own-g8m</guid>
      <description>&lt;p&gt;Every credit card number has a built-in checksum digit, and the algorithm that checks it (Luhn's algorithm) predates modern computing — it was patented in 1954, before credit cards as we know them existed.&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;luhnValid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;number&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;number&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;\D&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;split&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;reverse&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&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;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;acc&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="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&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;span class="nx"&gt;digit&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digit&lt;/span&gt; &lt;span class="o"&gt;&amp;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;digit&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;9&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;acc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;digit&lt;/span&gt;&lt;span class="p"&gt;;&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;sum&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="mi"&gt;0&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 only checks that the number is &lt;em&gt;structurally&lt;/em&gt; valid — it says nothing about whether the card exists, is active, or has funds. That's a completely separate (and much more sensitive) concern that needs a real payment processor, not a format checker.&lt;/p&gt;

&lt;p&gt;Brand detection (Visa vs Mastercard vs Amex) is a second, separate step based on the IIN/BIN prefix ranges. I bundled both into one endpoint on &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; since they're almost always needed together. Same account also runs &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>javascript</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
