<?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>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>
    <item>
      <title>Looking up a historical exchange rate correctly (and why 'the rate on that date' is fuzzier than it sounds)</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sat, 29 Aug 2026 14:37:05 +0000</pubDate>
      <link>https://dev.to/thejayedge/looking-up-a-historical-exchange-rate-correctly-and-why-the-rate-on-that-date-is-fuzzier-than-it-2b98</link>
      <guid>https://dev.to/thejayedge/looking-up-a-historical-exchange-rate-correctly-and-why-the-rate-on-that-date-is-fuzzier-than-it-2b98</guid>
      <description>&lt;p&gt;A common ask in invoicing/reconciliation code: "what was the USD/EUR rate on 2024-01-15?" The naive approach — pick a rate source, hit it with a date, trust the number — glosses over one detail that matters: ECB reference rates only publish on business days, so "the rate on" a weekend or bank-holiday date doesn't exist as a distinct value.&lt;/p&gt;

&lt;p&gt;The honest way to handle this is to resolve the requested date to the nearest actual trading date and &lt;em&gt;say so in the response&lt;/em&gt;, rather than silently picking one:&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;getHistoricalRate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&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/historical?date=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;from=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;from&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;to&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;// { from, to, rate, date } — "date" is the resolved trading date, not necessarily the one you asked for&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two failure modes worth guarding against explicitly, not just the happy path: a future date (there's no "historical" rate for a date that hasn't happened) and a date before your data source's coverage starts (ECB/Frankfurter data begins 1999-01-04 — pre-Euro-era rates aren't there). Both should be a clean 400, not a confusing 502 or a silently wrong 200.&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 new &lt;code&gt;/v1/historical&lt;/code&gt; endpoint does exactly this: validates the date up front (rejects future/out-of-range/malformed dates with a 400), resolves weekend/holiday dates to the prior business day, and returns which date it actually used — same "fail honestly, don't fabricate" principle as the rest of the API. 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>finance</category>
    </item>
    <item>
      <title>Branded QR codes without a design tool: setting foreground/background color via query params</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Thu, 27 Aug 2026 19:52:52 +0000</pubDate>
      <link>https://dev.to/thejayedge/branded-qr-codes-without-a-design-tool-setting-foregroundbackground-color-via-query-params-3gka</link>
      <guid>https://dev.to/thejayedge/branded-qr-codes-without-a-design-tool-setting-foregroundbackground-color-via-query-params-3gka</guid>
      <description>&lt;p&gt;The default black-on-white QR code works fine functionally, but it clashes with pretty much every brand style guide the moment you drop it into a poster, product package, or app screen. Most "just generate a QR code" APIs don't expose color at all, which pushes color-matching into a separate image-editing step after the fact — recoloring an SVG's fill by hand, or worse, round-tripping through a design tool for something that should be a one-line request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; takes &lt;code&gt;color&lt;/code&gt; (module/foreground) and &lt;code&gt;bgColor&lt;/code&gt; (background) as plain hex query params on &lt;code&gt;GET /v1/qr&lt;/code&gt;, no leading &lt;code&gt;#&lt;/code&gt;:&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;color=1a2b3c&amp;amp;bgColor=f4f1ea"&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;Both default to black-on-white (&lt;code&gt;000000&lt;/code&gt;/&lt;code&gt;ffffff&lt;/code&gt;) if omitted, so existing integrations don't need to change anything to keep their current output.&lt;/p&gt;

&lt;p&gt;Two things worth knowing if you're picking colors instead of just accepting the defaults:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contrast still has to survive a camera, not just a screen.&lt;/strong&gt; A QR decoder needs a clear light/dark distinction between modules and background — a pastel-on-pastel combination that looks fine in a design mockup can fail to scan reliably once printed or viewed under bad lighting. Keep real contrast between the two hex values, not just a stylistic difference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate hex input server-side if you ever accept it from a user.&lt;/strong&gt; These values get embedded directly into SVG &lt;code&gt;fill&lt;/code&gt; attributes, so strict 3/6-digit hex validation isn't optional — it's the difference between a color param and a markup-injection vector.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same &lt;code&gt;/v1/qr/matrix&lt;/code&gt; raw-grid endpoint is unaffected by color params since it returns boolean modules, not rendered output — colors only apply to the SVG path. 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>tutorial</category>
    </item>
    <item>
      <title>EU VAT number validation: format check vs VIES lookup, and when you need which</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Tue, 25 Aug 2026 09:49:58 +0000</pubDate>
      <link>https://dev.to/thejayedge/eu-vat-number-validation-format-check-vs-vies-lookup-and-when-you-need-which-2eaa</link>
      <guid>https://dev.to/thejayedge/eu-vat-number-validation-format-check-vs-vies-lookup-and-when-you-need-which-2eaa</guid>
      <description>&lt;p&gt;If you sell B2B in the EU, you'll eventually need to validate a customer's VAT number. There are two very different levels of "validation" here and it's worth knowing which one you actually need:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Format validation&lt;/strong&gt; — does the string match the country's VAT number pattern (length, structure)? This catches typos instantly, works offline, and is free forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VIES lookup&lt;/strong&gt; — is this VAT number actually registered with that EU country's tax authority right now? This requires a live call to the European Commission's VIES service, can be slow (VIES has outages more often than you'd like), and is the only way to know if the number is real vs just correctly-formatted.&lt;/p&gt;

&lt;p&gt;For most signup flows, format validation is enough to catch mistakes, with an optional VIES check before you actually apply a reverse-charge VAT exemption (since that's the part with tax liability implications).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/v&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/validate/vat&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="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;"IE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"vatNumber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6388047V"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"checkExistence"&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="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;That's the shape I settled on for &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; — format check always runs, VIES lookup is opt-in per request so you're not waiting on a flaky government API when you don't need to. 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>business</category>
    </item>
  </channel>
</rss>
