<?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>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>
    <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, 22 Aug 2026 09:50:32 +0000</pubDate>
      <link>https://dev.to/thejayedge/iso-4217-currency-codes-the-edge-cases-that-break-naive-multi-currency-forms-4pha</link>
      <guid>https://dev.to/thejayedge/iso-4217-currency-codes-the-edge-cases-that-break-naive-multi-currency-forms-4pha</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>Branded QR codes without a design tool: setting foreground/background color via query params</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Thu, 20 Aug 2026 09:53:08 +0000</pubDate>
      <link>https://dev.to/thejayedge/branded-qr-codes-without-a-design-tool-setting-foregroundbackground-color-via-query-params-2mbd</link>
      <guid>https://dev.to/thejayedge/branded-qr-codes-without-a-design-tool-setting-foregroundbackground-color-via-query-params-2mbd</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>Phone number validation is harder than you think (a libphonenumber crash course)</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Tue, 18 Aug 2026 09:47:17 +0000</pubDate>
      <link>https://dev.to/thejayedge/phone-number-validation-is-harder-than-you-think-a-libphonenumber-crash-course-1bf8</link>
      <guid>https://dev.to/thejayedge/phone-number-validation-is-harder-than-you-think-a-libphonenumber-crash-course-1bf8</guid>
      <description>&lt;p&gt;A regex will not validate phone numbers correctly. I promise. Numbering plans vary by country, mobile vs landline prefixes differ, and some countries have variable-length numbers. Google's libphonenumber (the library that powers Android's dialer) is the only sane way to do this client- or server-side.&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="nx"&gt;parsePhoneNumberFromString&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;libphonenumber-js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parsePhoneNumberFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+49 170 1234567&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;          &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// "DE"&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formatInternational&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// "+49 170 1234567"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two gotchas that catch people out: numbers without a country code need a &lt;code&gt;defaultCountry&lt;/code&gt; hint or parsing will silently fail, and "valid" only means "matches the numbering plan" — it says nothing about whether the number is currently assigned to someone.&lt;/p&gt;

&lt;p&gt;For a stateless HTTP version of this (useful if your stack isn't JS, or you don't want the dependency), I built it as one of the endpoints on &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; — same libphonenumber-js under the hood, just over JSON. Same account also covers &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; and &lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>api</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, 15 Aug 2026 09:49:40 +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-5flm</link>
      <guid>https://dev.to/thejayedge/looking-up-a-historical-exchange-rate-correctly-and-why-the-rate-on-that-date-is-fuzzier-than-it-5flm</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>Batch-generating QR codes for events, inventory, or ticketing without hammering an API</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Thu, 13 Aug 2026 10:31:12 +0000</pubDate>
      <link>https://dev.to/thejayedge/batch-generating-qr-codes-for-events-inventory-or-ticketing-without-hammering-an-api-3h7</link>
      <guid>https://dev.to/thejayedge/batch-generating-qr-codes-for-events-inventory-or-ticketing-without-hammering-an-api-3h7</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>Stop rolling your own password breach checker — here's how k-anonymity makes it safe</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Tue, 11 Aug 2026 10:09:20 +0000</pubDate>
      <link>https://dev.to/thejayedge/stop-rolling-your-own-password-breach-checker-heres-how-k-anonymity-makes-it-safe-285e</link>
      <guid>https://dev.to/thejayedge/stop-rolling-your-own-password-breach-checker-heres-how-k-anonymity-makes-it-safe-285e</guid>
      <description>&lt;p&gt;If you're checking user passwords against HaveIBeenPwned's breach database, please don't send the full password (or even the full hash) over the wire. Here's why, and the actual protocol that fixes it.&lt;/p&gt;

&lt;p&gt;HIBP's Pwned Passwords API uses k-anonymity: you SHA-1 hash the password locally, send only the first 5 characters of the hash, and the API returns every suffix that starts with that prefix (usually 300-900 of them). You compare locally. The full password never leaves your server, and HIBP never sees enough to reconstruct it.&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isBreached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;suffix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.pwnedpasswords.com/range/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's genuinely all it takes — no library needed. I wrapped this (plus password strength scoring) into an endpoint on &lt;a href="https://rapidapi.com/jonashaemecommerce/api/validate7" rel="noopener noreferrer"&gt;Validate&lt;/a&gt; if you'd rather not maintain the hashing/comparison logic yourself, but honestly, the snippet above will get most people there. Sibling APIs on the same account: &lt;a href="https://rapidapi.com/jonashaemecommerce/api/qr-api19" rel="noopener noreferrer"&gt;QR API&lt;/a&gt; and &lt;a href="https://rapidapi.com/jonashaemecommerce/api/currency-api15" rel="noopener noreferrer"&gt;Currency API&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>api</category>
      <category>javascript</category>
    </item>
    <item>
      <title>ISO 4217 currency codes: the edge cases that break naive multi-currency forms</title>
      <dc:creator>Jonas Hämmerle</dc:creator>
      <pubDate>Sat, 08 Aug 2026 10:02:11 +0000</pubDate>
      <link>https://dev.to/thejayedge/iso-4217-currency-codes-the-edge-cases-that-break-naive-multi-currency-forms-3hen</link>
      <guid>https://dev.to/thejayedge/iso-4217-currency-codes-the-edge-cases-that-break-naive-multi-currency-forms-3hen</guid>
      <description>&lt;p&gt;Most currency handling code assumes every currency has 2 decimal places and a straightforward numeric amount. Neither assumption holds universally, and both break in ways that are easy to miss until a real transaction hits the edge case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not every currency has 2 minor units.&lt;/strong&gt; JPY has zero — ¥100 is a whole number, there's no ¥100.00. KWD (Kuwaiti Dinar) has three. If your form/database column hardcodes 2 decimal places "because that's how currency works," you'll either round JPY amounts into nonsense or truncate KWD precision.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MINOR_UNITS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;JPY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;KWD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// default 2 for anything not listed&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatAmount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MINOR_UNITS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Currency codes aren't a stable, closed set forever&lt;/strong&gt; — they get added, and in rare cases retired (old Eurozone currencies like DEM, FRF were withdrawn when the Euro launched). A hardcoded currency list baked into your app years ago can silently drift from reality. Pull the supported-currency list from a source that's actually kept current, rather than a list you typed once.&lt;/p&gt;

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

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

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

&lt;p&gt;Here's the short version: move the first four characters to the end, convert letters to numbers (A=10, B=11...), then check if the resulting number mod 97 equals 1. That's it — no database lookup needed to catch most typos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// simplified version of the check&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ibanChecksumValid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iban&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rearranged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;iban&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;iban&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;numeric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rearranged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Z&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digit&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;numeric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digit&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fine for a side project. Once you're validating IBANs from real users, you also want country-specific length checks, BBAN structure validation, and ideally a formatted, human-readable output — which is more code than you want to own for something this boilerplate.&lt;/p&gt;

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

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

&lt;p&gt;The right cache key isn't "this specific conversion," it's "the rate table for this base currency, as of today's date":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rateCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// key: `${base}:${isoDate}`&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getRatesForDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rateCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rateCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://currency-api.p.rapidapi.com/v1/rates?base=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-RapidAPI-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key_env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-RapidAPI-Host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;currency-api.p.rapidapi.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;rateCache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One call per base currency per day gets you every target currency's rate in that response — &lt;code&gt;/v1/rates&lt;/code&gt; returns the full table, not just one pair — so converting to 8 currencies from a cached table is 8 in-memory lookups, not 8 network round-trips.&lt;/p&gt;

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

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

</description>
      <category>webdev</category>
      <category>api</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
