<?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: Alejandro Rodriguez</title>
    <description>The latest articles on DEV Community by Alejandro Rodriguez (@alejandrorodrom).</description>
    <link>https://dev.to/alejandrorodrom</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%2F4069354%2F73659989-e112-47ab-a895-9623bd1b7971.jpg</url>
      <title>DEV Community: Alejandro Rodriguez</title>
      <link>https://dev.to/alejandrorodrom</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alejandrorodrom"/>
    <language>en</language>
    <item>
      <title>Your bundle’s raw KB lie: measure gzip and brotli before you merge</title>
      <dc:creator>Alejandro Rodriguez</dc:creator>
      <pubDate>Fri, 06 Nov 2026 08:46:02 +0000</pubDate>
      <link>https://dev.to/alejandrorodrom/your-bundles-raw-kb-lie-measure-gzip-and-brotli-before-you-merge-487c</link>
      <guid>https://dev.to/alejandrorodrom/your-bundles-raw-kb-lie-measure-gzip-and-brotli-before-you-merge-487c</guid>
      <description>&lt;p&gt;You open &lt;code&gt;dist/&lt;/code&gt;, add up the JS, and relax: “the bundle is 420 KB.” That number is how much the file takes on disk. It is not what the person who opens your app downloads.&lt;/p&gt;

&lt;p&gt;Most CDNs and browsers deliver JavaScript compressed. Chrome, Cloudflare, and almost any modern host prefer Brotli and fall back to gzip. Transfer size — what travels over the network and pushes LCP, hydration, and time-to-interactive — is usually much smaller than raw. Sometimes the opposite happens: a change that barely moves disk size makes the file compress worse, and the user pays more than you thought.&lt;/p&gt;

&lt;p&gt;The useful first habit is not “look at the file weight.” It is measuring the three numbers together: raw, gzip, and brotli. &lt;a href="https://www.npmjs.com/package/bundlelens" rel="noopener noreferrer"&gt;Bundlelens&lt;/a&gt; (CLI on npm, version 2.1.0, Node 22 or later) does exactly that: it runs your build or analyzes a folder that already exists, indexes the artifacts, and writes an HTML and JSON report with those three columns.&lt;/p&gt;

&lt;h2&gt;
  
  
  In five minutes
&lt;/h2&gt;

&lt;p&gt;At the project root (where &lt;code&gt;package.json&lt;/code&gt; lives):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx bundlelens init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That creates &lt;code&gt;bundlelens.config.json&lt;/code&gt; with defaults. If a &lt;code&gt;.gitignore&lt;/code&gt; exists, it adds the report folder (default &lt;code&gt;bundlelens/&lt;/code&gt;), unless you pass &lt;code&gt;--skip-gitignore&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Open the config and check two fields that have to match your stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;buildCommand&lt;/code&gt;: the same command you would run in the terminal to produce the build (&lt;code&gt;npm run build&lt;/code&gt;, &lt;code&gt;pnpm run build&lt;/code&gt;, and so on).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;buildDir&lt;/code&gt;: the folder, relative to the config file, whose artifacts you want to measure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical Vite or webpack setup:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buildCommand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buildDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bundlelens"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gzip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"brotli"&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;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;&lt;code&gt;compression.gzip&lt;/code&gt; and &lt;code&gt;compression.brotli&lt;/code&gt; are what turn on the columns that matter. Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx bundlelens run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;run&lt;/code&gt; executes the build and analyzes &lt;code&gt;buildDir&lt;/code&gt;. If the folder is already there (for example in CI, after &lt;code&gt;npm run build&lt;/code&gt;), you do not need to compile again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx bundlelens analyze dist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When it finishes, open &lt;code&gt;./bundlelens/index.html&lt;/code&gt;. The ranking and &lt;code&gt;files.html&lt;/code&gt; show raw, gzip, and brotli per file; &lt;code&gt;treemap.html&lt;/code&gt; maps size by path. The same data is written to &lt;code&gt;report.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you do not want a config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx bundlelens run &lt;span class="s2"&gt;"npm run build"&lt;/span&gt; &lt;span class="nt"&gt;--build-dir&lt;/span&gt; dist &lt;span class="nt"&gt;--output&lt;/span&gt; bundlelens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a key exists in the JSON (&lt;code&gt;buildDir&lt;/code&gt;, &lt;code&gt;outputDir&lt;/code&gt;, &lt;code&gt;buildCommand&lt;/code&gt;), the file wins and the flag does not override it. So CI always measures the right folder, set it in the config or pass it only as a flag — not both in conflict.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are actually measuring
&lt;/h2&gt;

&lt;p&gt;Raw is how large the artifact is uncompressed. It is useful to see which file grew, not to estimate the download.&lt;/p&gt;

&lt;p&gt;Gzip is the historical floor: almost every server speaks it. Brotli usually compresses text better (JS, CSS, HTML, JSON) and is what most CDNs deliver today. If you only watch raw, you can fight a “huge chunk” that travels fine under brotli, or miss a vendor file that barely changes on disk but compresses worse after a refactor.&lt;/p&gt;

&lt;p&gt;Bundlelens computes gzip and brotli per file while it indexes. The report lets you compare the three columns: that is where you see which file actually moves transfer size. That is also why &lt;code&gt;buildDir&lt;/code&gt; matters so much: the tool measures what lives under that folder, not “what the user will request in a single visit.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick the right folder for your framework
&lt;/h2&gt;

&lt;p&gt;Not every framework leaves a &lt;code&gt;dist/&lt;/code&gt; you can compare apples to apples. Some write a working directory (cache, server output, manifests) next to what actually travels to the browser. If you point at the whole tree, raw, gzip, and brotli inflate, and you stop estimating the download.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vite, webpack, React with Vite, Vue, or a Svelte SPA:&lt;/strong&gt; usually &lt;code&gt;dist/&lt;/code&gt;. That is the client package, and it is a good &lt;code&gt;buildDir&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create React App:&lt;/strong&gt; typically &lt;code&gt;build/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Angular (17+):&lt;/strong&gt; the web client usually lands in &lt;code&gt;dist/my-app/browser&lt;/code&gt;. Use the path from your &lt;code&gt;angular.json&lt;/code&gt;, not a generic &lt;code&gt;dist/&lt;/code&gt; if you have more than one project or output.&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buildCommand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buildDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/my-app/browser"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bundlelens"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gzip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"brotli"&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;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;&lt;strong&gt;Ionic (web build):&lt;/strong&gt; usually &lt;code&gt;www/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next.js:&lt;/strong&gt; &lt;code&gt;next build&lt;/code&gt; produces a working directory, not a package you should sum from end to end. If you static-export (&lt;code&gt;output: "export"&lt;/code&gt;), measure &lt;code&gt;out/&lt;/code&gt;. If you do not export and you want what the browser will request (hashed JS, CSS, and assets), point at &lt;code&gt;.next/static&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;npx bundlelens analyze .next/static &lt;span class="nt"&gt;--output&lt;/span&gt; bundlelens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can analyze all of &lt;code&gt;.next&lt;/code&gt; (&lt;code&gt;npx bundlelens analyze .next&lt;/code&gt;). Use that to inspect build artifacts, not to estimate a page download: it includes cache, server output, and internal files the user never requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SvelteKit:&lt;/strong&gt; do not measure &lt;code&gt;.svelte-kit&lt;/code&gt;; that is the internal build directory. With &lt;code&gt;adapter-static&lt;/code&gt;, pages and client assets default to &lt;code&gt;build/&lt;/code&gt;. With &lt;code&gt;adapter-node&lt;/code&gt;, &lt;code&gt;build/&lt;/code&gt; mixes the server (&lt;code&gt;index.js&lt;/code&gt;, server code) and the client; if you care about what the browser requests, point at &lt;code&gt;build/client&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;npx bundlelens analyze build/client &lt;span class="nt"&gt;--output&lt;/span&gt; bundlelens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Nuxt:&lt;/strong&gt; do not measure &lt;code&gt;.nuxt&lt;/code&gt; (working directory) or all of &lt;code&gt;.output&lt;/code&gt; (it includes the Nitro server). What is served as static files to the browser lives in &lt;code&gt;.output/public&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;npx bundlelens analyze .output/public &lt;span class="nt"&gt;--output&lt;/span&gt; bundlelens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule is the same on any stack: choose as &lt;code&gt;buildDir&lt;/code&gt; the folder that represents what you want to measure, not “everything the framework wrote while compiling.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Fit it into the PR, not only your laptop
&lt;/h2&gt;

&lt;p&gt;The measurement is useful when someone else can repeat the three numbers. A minimal GitHub Actions job, after the build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx bundlelens@latest analyze dist --output bundlelens&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundlelens-report&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bundlelens/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap &lt;code&gt;dist&lt;/code&gt; for your real &lt;code&gt;buildDir&lt;/code&gt;: &lt;code&gt;dist/my-app/browser&lt;/code&gt; on Angular, &lt;code&gt;out&lt;/code&gt; or &lt;code&gt;.next/static&lt;/code&gt; on Next.js, &lt;code&gt;build&lt;/code&gt; or &lt;code&gt;build/client&lt;/code&gt; on SvelteKit, &lt;code&gt;.output/public&lt;/code&gt; on Nuxt, &lt;code&gt;www&lt;/code&gt; on Ionic, &lt;code&gt;build&lt;/code&gt; on CRA. There is no TTY in CI: set &lt;code&gt;buildCommand&lt;/code&gt; and &lt;code&gt;buildDir&lt;/code&gt;. If you want the job to fail when the build fails, use &lt;code&gt;--fail-on-build&lt;/code&gt; or &lt;code&gt;failOnBuild&lt;/code&gt; in the config.&lt;/p&gt;

&lt;p&gt;The report is stored as a PR artifact. There you see raw, gzip, and brotli per file, measured on the folder you chose.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>performance</category>
      <category>node</category>
    </item>
    <item>
      <title>HMAC over JSON in TypeScript — why verify fails in production</title>
      <dc:creator>Alejandro Rodriguez</dc:creator>
      <pubDate>Sun, 23 Aug 2026 03:01:14 +0000</pubDate>
      <link>https://dev.to/alejandrorodrom/hmac-over-json-in-typescript-why-verify-fails-in-production-457k</link>
      <guid>https://dev.to/alejandrorodrom/hmac-over-json-in-typescript-why-verify-fails-in-production-457k</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/alejandrorodrom/buffer-vs-uint8array-in-typescript-crypto-and-why-it-hurts-289l"&gt;previous post&lt;/a&gt; we treated bytes as &lt;code&gt;Uint8Array&lt;/code&gt; and skimmed HMAC for a JSON body. The one-liner looked fine. Then production hit: client signs, server verifies, &lt;code&gt;ok === false&lt;/code&gt; with the “same” payload.&lt;/p&gt;

&lt;p&gt;This piece is only about that failure mode. HMAC is message authentication (integrity + authenticity with a shared secret). It is not encryption. It does not replace TLS. The examples use &lt;a href="https://www.npmjs.com/package/encloom" rel="noopener noreferrer"&gt;encloom&lt;/a&gt;; the habits apply with Web Crypto or &lt;code&gt;@noble/hashes&lt;/code&gt; too.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Not a compliance guide. About bytes on the wire.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What HMAC actually signs
&lt;/h2&gt;

&lt;p&gt;HMAC-SHA256 takes a &lt;strong&gt;key&lt;/strong&gt; and a &lt;strong&gt;message&lt;/strong&gt;, both as bytes. The tag is deterministic for that pair. If either side feeds different bytes—different UTF-8 of the secret, different JSON serialization, hex vs Base64 of the tag—verification fails even when humans “see the same JSON.”&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;hmacSha256SignSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;hmacSha256VerifySync&lt;/span&gt;&lt;span class="p"&gt;,&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;encloom/hmac&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bufferToBase64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base64ToBuffer&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;encloom/helpers/encoding&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api-shared-secret&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;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{"action":"ping","id":1}&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;mac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmacSha256SignSync&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;msg&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;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmacSha256VerifySync&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;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mac&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="na"&gt;macB64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bufferToBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mac&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;hmacSha256VerifySync&lt;/code&gt; returns a boolean and compares the tag as bytes (&lt;code&gt;equalConstTime&lt;/code&gt;). Decode Base64 or hex first and use that API. Do not invent a &lt;code&gt;===&lt;/code&gt; on encoded strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  The production bug: re-stringify after parse
&lt;/h2&gt;

&lt;p&gt;A convenience helper signs &lt;code&gt;JSON.stringify(data)&lt;/code&gt; for you. That is safe &lt;strong&gt;only if&lt;/strong&gt; the verifier uses the &lt;strong&gt;same&lt;/strong&gt; byte sequence.&lt;/p&gt;

&lt;p&gt;This is the trap:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client builds an object, signs &lt;code&gt;JSON.stringify(object)&lt;/code&gt; (compact).&lt;/li&gt;
&lt;li&gt;The HTTP client, a logger, or the server serializes &lt;strong&gt;again&lt;/strong&gt;—pretty-print, a DTO with an extra field, a different key order.&lt;/li&gt;
&lt;li&gt;Verify runs over those new bytes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;JSON.parse&lt;/code&gt; of the wire string and &lt;code&gt;JSON.stringify&lt;/code&gt; of &lt;strong&gt;that&lt;/strong&gt; object often keeps key order in modern JS. The usual break is a &lt;em&gt;second&lt;/em&gt; serialization that is not the wire: &lt;code&gt;JSON.stringify(obj, null, 2)&lt;/code&gt;, a mapped DTO (&lt;code&gt;receivedAt&lt;/code&gt;, stripped &lt;code&gt;undefined&lt;/code&gt;), or Unicode escapes (&lt;code&gt;\u0061&lt;/code&gt; vs &lt;code&gt;a&lt;/code&gt;). Key order can still bite if something rebuilds the object from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before — same payload, different bytes → &lt;code&gt;ok === false&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;hmacSha256SignJsonUtf8KeyBase64Sync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;hmacSha256VerifySync&lt;/span&gt;&lt;span class="p"&gt;,&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;encloom/hmac&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base64ToBuffer&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;encloom/helpers/encoding&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;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api-shared-secret&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ping&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Client signs compact JSON.stringify(body)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;macB64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmacSha256SignJsonUtf8KeyBase64Sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// MAC over: {"action":"ping","id":1}&lt;/span&gt;

&lt;span class="c1"&gt;// Server (or axios interceptor, or a DTO mapper) serializes again&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pretty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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;extraField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;receivedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-08-22T00:00:00.000Z&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;okPretty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmacSha256VerifySync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pretty&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;base64ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;macB64&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;okDto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmacSha256VerifySync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extraField&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nf"&gt;base64ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;macB64&lt;/span&gt;&lt;span class="p"&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;okPretty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;okDto&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// false, false — same “JSON”, different UTF-8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  After — sign and verify the wire (preferred)
&lt;/h3&gt;

&lt;p&gt;Agree that the MAC covers the &lt;strong&gt;exact request body bytes&lt;/strong&gt;, not a re-built object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;hmacSha256SignUtf8KeyBase64Sync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;hmacSha256VerifySync&lt;/span&gt;&lt;span class="p"&gt;,&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;encloom/hmac&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base64ToBuffer&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;encloom/helpers/encoding&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;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api-shared-secret&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Client: MAC over the exact string that will be POSTed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ping&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;macB64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmacSha256SignUtf8KeyBase64Sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// POST rawBody with header X-Signature: macB64&lt;/span&gt;

&lt;span class="c1"&gt;// Server: verify the raw body from the framework (before JSON.parse)&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;macB64&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;hmacSha256VerifySync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;base64ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;macB64&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;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="nf"&gt;verifyRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;macB64&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Express/Fastify/etc., read the raw body (or a buffered string) &lt;strong&gt;before&lt;/strong&gt; &lt;code&gt;JSON.parse&lt;/code&gt; for the MAC path. Parse for business logic after verify succeeds.&lt;/p&gt;

&lt;p&gt;Signing only the body is enough to catch tampering of that body. A reused MAC can still be posted to another path. If that matters, put more than the body under the MAC—for example &lt;code&gt;METHOD + path + timestamp + rawBody&lt;/code&gt; as one UTF-8 string both sides build the same way—and reject old timestamps.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you must sign objects — canonical JSON
&lt;/h2&gt;

&lt;p&gt;If both sides only have objects, define a canonical form: sorted keys, no extra spaces, UTF-8, and the same number serialization. A minimal sketch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canonicalJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&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;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&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="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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="s2"&gt;`[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&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;canonicalJson&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&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;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;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;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`{&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;keys&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;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&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="nf"&gt;canonicalJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&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;Then HMAC over &lt;code&gt;utf8ToBuffer(canonicalJson(body))&lt;/code&gt; on both sides. Homegrown canonicalization has edge cases (dates, &lt;code&gt;undefined&lt;/code&gt;, bigint). Prefer signing the wire when you control the HTTP stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encoding checklist
&lt;/h2&gt;

&lt;p&gt;Before debugging crypto, check these mismatches:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Agree on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Secret&lt;/td&gt;
&lt;td&gt;Same UTF-8 bytes (trim, no accidental newline in env vars)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message&lt;/td&gt;
&lt;td&gt;Raw body &lt;strong&gt;or&lt;/strong&gt; one canonical serializer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tag transport&lt;/td&gt;
&lt;td&gt;Base64 &lt;strong&gt;or&lt;/strong&gt; hex—decode before verify&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Algorithm&lt;/td&gt;
&lt;td&gt;HMAC-SHA256 vs SHA512 (do not mix)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hex vs Base64 of the same tag are different strings; both sides must use the same encoding on the wire and decode to &lt;code&gt;Uint8Array&lt;/code&gt; before &lt;code&gt;verify&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What HMAC does not give you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confidentiality&lt;/strong&gt; — anyone who sees the body still reads it. Use TLS; add AES-GCM (or similar) if you need ciphertext at rest or beyond transport.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-repudiation&lt;/strong&gt; — shared secret means either party can forge tags. For “only this private key signed,” use ECDSA / similar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay protection&lt;/strong&gt; — a valid MAC can be resent. Bind method, path, and a timestamp (or nonce) into the signed bytes, as above, if that matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When this pattern fits
&lt;/h2&gt;

&lt;p&gt;Use shared-secret HMAC when client and server (or two backends) already share a secret, you need tamper detection on a payload, and you can pin the exact bytes under the MAC.&lt;/p&gt;

&lt;p&gt;Skip it for public clients that cannot hold a secret (use user auth + server-side checks), or when you need asymmetric proof of origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/encloom" rel="noopener noreferrer"&gt;encloom on npm&lt;/a&gt; · &lt;a href="https://github.com/alejandrorodrom/encloom" rel="noopener noreferrer"&gt;repo&lt;/a&gt; · &lt;code&gt;encloom/hmac&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Previous: &lt;a href="https://dev.to/alejandrorodrom/buffer-vs-uint8array-in-typescript-crypto-and-why-it-hurts-289l"&gt;Buffer vs Uint8Array in TypeScript crypto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Related: Web Crypto &lt;code&gt;HMAC&lt;/code&gt;, RFC 2104&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>cryptography</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Buffer vs Uint8Array in TypeScript crypto (and why it hurts)</title>
      <dc:creator>Alejandro Rodriguez</dc:creator>
      <pubDate>Mon, 17 Aug 2026 06:34:15 +0000</pubDate>
      <link>https://dev.to/alejandrorodrom/buffer-vs-uint8array-in-typescript-crypto-and-why-it-hurts-289l</link>
      <guid>https://dev.to/alejandrorodrom/buffer-vs-uint8array-in-typescript-crypto-and-why-it-hurts-289l</guid>
      <description>&lt;p&gt;In Node, &lt;code&gt;Buffer&lt;/code&gt; shows up everywhere. In the browser and on the edge, the real contract is typed arrays (&lt;code&gt;Uint8Array&lt;/code&gt;)—and often Web Crypto.&lt;/p&gt;

&lt;p&gt;When you mix those worlds—an API on Node, a client in the browser, a Worker in between—crypto stops being “call SHA-256” and turns into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;converting types at every boundary&lt;/li&gt;
&lt;li&gt;guessing whether a helper accepts &lt;code&gt;string | Buffer | Uint8Array&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;pulling half a library for a single primitive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post follows that thread (&lt;code&gt;Uint8Array&lt;/code&gt; end to end) through four flows: hashing, HMAC, AES-GCM, and ECDSA over a digest. The snippets use &lt;a href="https://www.npmjs.com/package/encloom" rel="noopener noreferrer"&gt;encloom&lt;/a&gt;; the same ideas apply if you prefer &lt;code&gt;@noble/*&lt;/code&gt; plus Web Crypto by hand.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is not a threat-modeling or compliance guide. It is about bytes and APIs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The contract: explicit bytes
&lt;/h2&gt;

&lt;p&gt;A string is not a cryptographic message until you define an encoding. UTF-8 is the usual choice for JSON APIs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;sha256Sync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sha256Utf8HexSync&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;encloom/sha2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bufferToHex&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;encloom/helpers/encoding&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;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256Sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&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="nf"&gt;bufferToHex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Shortcut when you only need hex of a UTF-8 string&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="nf"&gt;sha256Utf8HexSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&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;code&gt;Uint8Array&lt;/code&gt; in the public API avoids “sometimes Buffer, sometimes not” across runtimes. The cost is one conversion line; the win is predictable typing.&lt;/p&gt;

&lt;p&gt;Naming note: helpers like &lt;code&gt;utf8ToBuffer&lt;/code&gt; return &lt;code&gt;Uint8Array&lt;/code&gt;. The &lt;code&gt;Buffer&lt;/code&gt; suffix is an enc-utils-style name, not Node’s &lt;code&gt;Buffer&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Per-module imports
&lt;/h2&gt;

&lt;p&gt;If your bundler tree-shakes and the package sets &lt;code&gt;sideEffects: false&lt;/code&gt;, import the subpath you actually use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;sha256Sync&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;encloom/sha2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;aesGcmEncryptSync&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;encloom/aes-gcm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same idea as preferring &lt;code&gt;lodash/get&lt;/code&gt; over &lt;code&gt;import _ from "lodash"&lt;/code&gt; when you need one function. On the frontend and in Workers, size shows up.&lt;/p&gt;

&lt;h2&gt;
  
  
  HMAC: integrity for a JSON body
&lt;/h2&gt;

&lt;p&gt;Same byte contract: the MAC is computed over &lt;code&gt;Uint8Array&lt;/code&gt;, not a loose string or a Node &lt;code&gt;Buffer&lt;/code&gt;. Scenario: client and server share a secret and want to detect payload tampering. No encryption here—message authentication only.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;hmacSha256SignJsonUtf8KeyBase64Sync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;hmacSha256VerifySync&lt;/span&gt;&lt;span class="p"&gt;,&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;encloom/hmac&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base64ToBuffer&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;encloom/helpers/encoding&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;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api-shared-secret&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ping&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;macB64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmacSha256SignJsonUtf8KeyBase64Sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Same serialization the helper used when signing&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&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;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmacSha256VerifySync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;base64ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;macB64&lt;/span&gt;&lt;span class="p"&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;macB64&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A detail that breaks verification in production: if one side signs the object and the other re-parses JSON from the wire and runs &lt;code&gt;JSON.stringify&lt;/code&gt; again, key order can change. Agree on canonical bytes (or sign an already-stable string) between client and server.&lt;/p&gt;

&lt;h2&gt;
  
  
  AES-GCM: JSON in an envelope
&lt;/h2&gt;

&lt;p&gt;Same thread: plaintext and key as &lt;code&gt;Uint8Array&lt;/code&gt;; the helper only serializes the result to strings for transport. GCM gives confidentiality plus integrity (the tag). &lt;code&gt;encryptJsonAes256GcmSync&lt;/code&gt; generates a random key and IV and returns an envelope (&lt;code&gt;iv&lt;/code&gt;, &lt;code&gt;key&lt;/code&gt;, &lt;code&gt;stream&lt;/code&gt;). Decryption returns &lt;code&gt;unknown&lt;/code&gt;—narrow it at the call site.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;encryptJsonAes256GcmSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;decryptJsonAes256GcmSync&lt;/span&gt;&lt;span class="p"&gt;,&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;encloom/aes-gcm&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;wire&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encryptJsonAes256GcmSync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ana&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// wire: { iv, key, stream } — the key travels inside the envelope&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;again&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decryptJsonAes256GcmSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wire&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&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;again&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important so the example is not misread: &lt;strong&gt;anyone who has the full &lt;code&gt;wire&lt;/code&gt; can decrypt&lt;/strong&gt;, because the key is in the object. That fits a local sealed blob, or a layer you later protect with another key / KMS / ECIES. It is not “I encrypted with a shared secret and only send the ciphertext.”&lt;/p&gt;

&lt;p&gt;If you manage the key yourself, the low-level API is &lt;code&gt;aesGcmEncryptSync(nonce, key, plaintext)&lt;/code&gt;. There the operational rule is: do not reuse the same &lt;code&gt;(key, nonce)&lt;/code&gt; pair; 12-byte nonces are the usual size.&lt;/p&gt;

&lt;h2&gt;
  
  
  ECDSA (secp256k1): sign the digest
&lt;/h2&gt;

&lt;p&gt;Explicit bytes again: UTF-8 → &lt;code&gt;Uint8Array&lt;/code&gt;, then the digest, then the signature. Many examples sign raw plaintext; in this API &lt;code&gt;sign&lt;/code&gt; / &lt;code&gt;verify&lt;/code&gt; expect the message &lt;strong&gt;hash&lt;/strong&gt; (e.g. SHA-256). &lt;code&gt;verify&lt;/code&gt; does not return a &lt;code&gt;boolean&lt;/code&gt;: if the signature is invalid, it &lt;strong&gt;throws&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;generateKeyPair&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verify&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;encloom/ecdsa&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sha256Sync&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;encloom/sha2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;utf8ToBuffer&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;encloom/helpers/encoding&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateKeyPair&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;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256Sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;utf8ToBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transfer:42&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;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;privateKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ok; on failure → throw&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Ethereum-style pipelines, &lt;code&gt;keccak256&lt;/code&gt; lives in &lt;code&gt;encloom/sha3&lt;/code&gt;. Keccak-256 ≠ SHA3-256 (FIPS); they are different domains—do not mix them up.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this approach helps (and when it doesn’t)
&lt;/h2&gt;

&lt;p&gt;It makes sense if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;share the same byte-oriented code across Node / browser / edge&lt;/li&gt;
&lt;li&gt;use a few primitives and care what lands in the bundle&lt;/li&gt;
&lt;li&gt;prefer types that do not lie (&lt;code&gt;Uint8Array&lt;/code&gt; vs “stringly bytes”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It adds little if you already live inside a stack that covers ~80% of your case (e.g. a full wallet SDK) and you do not care about size or buffer types.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Package and module docs: &lt;a href="https://www.npmjs.com/package/encloom" rel="noopener noreferrer"&gt;encloom on npm&lt;/a&gt; · &lt;a href="https://github.com/alejandrorodrom/encloom" rel="noopener noreferrer"&gt;repo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Related ideas: Web Crypto (&lt;code&gt;SubtleCrypto&lt;/code&gt;), the &lt;code&gt;@noble&lt;/code&gt; family for pure-JS implementations&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>cryptography</category>
      <category>node</category>
    </item>
    <item>
      <title>Switch Success and Error Without Touching Your Frontend</title>
      <dc:creator>Alejandro Rodriguez</dc:creator>
      <pubDate>Sun, 16 Aug 2026 21:06:25 +0000</pubDate>
      <link>https://dev.to/alejandrorodrom/switch-success-and-error-without-touching-your-frontend-1dlp</link>
      <guid>https://dev.to/alejandrorodrom/switch-success-and-error-without-touching-your-frontend-1dlp</guid>
      <description>&lt;p&gt;Your app already calls the API. What you need now is the other half: the 404, the 500, the empty list — without asking anyone to break staging, and without &lt;code&gt;if (mock)&lt;/code&gt; in the client.&lt;/p&gt;

&lt;p&gt;In this article you’ll:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define several named responses on the same endpoint&lt;/li&gt;
&lt;li&gt;Pick which one is active with a single JSON key&lt;/li&gt;
&lt;li&gt;Hit the same URL with &lt;code&gt;curl&lt;/code&gt; and get 200, 404, or 500&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frontend keeps the same base URL (&lt;code&gt;http://localhost:3001&lt;/code&gt;). You only edit the mock file.&lt;/p&gt;

&lt;p&gt;This is a CLI (&lt;code&gt;mock-server&lt;/code&gt;), not an embeddable SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js &amp;gt;= 22.12&lt;/li&gt;
&lt;li&gt;A project with a &lt;code&gt;package.json&lt;/code&gt; (recommended)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;http-mock-json &lt;span class="nt"&gt;--save-dev&lt;/span&gt;
npx mock-server init
npx mock-server start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; creates &lt;code&gt;mocks/&lt;/code&gt;, adds a &lt;code&gt;mock:start&lt;/code&gt; script, and can scaffold a first file. The server listens on &lt;strong&gt;port 3001&lt;/strong&gt;. Watch mode is always on: save a mock and it reloads.&lt;/p&gt;

&lt;p&gt;If you already have the server running, skip this section and open your JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  One endpoint, several scenarios
&lt;/h2&gt;

&lt;p&gt;Each HTTP method has two pieces:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;responses&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Named scenarios (&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;statusCode&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nameResponse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which scenario is returned&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The URL does not change. Only &lt;code&gt;nameResponse&lt;/code&gt; does.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;mocks/animals.json&lt;/code&gt; (or create it) and use this:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data/animals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"nameResponse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"responses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"200"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"animals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lion"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tiger"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not-found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"404"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No animals found"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"server-error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"500"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Internal server error"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;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;&lt;code&gt;nameResponse&lt;/code&gt; must match one of the &lt;code&gt;name&lt;/code&gt; values. Here the default is &lt;code&gt;"success"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Call it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://localhost:3001/data/animals
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json; charset=utf-8&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"animals"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Lion"&lt;/span&gt;&lt;span class="p"&gt;},{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Tiger"&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;Point your app at &lt;code&gt;http://localhost:3001&lt;/code&gt;. Same GET as production — this body is the happy path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flip to 404
&lt;/h2&gt;

&lt;p&gt;Change only this line:&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="nl"&gt;"nameResponse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not-found"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save. Watch reloads. Run the same &lt;code&gt;curl&lt;/code&gt; again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;404&lt;/span&gt; &lt;span class="ne"&gt;Not Found&lt;/span&gt;

{"message":"No animals found"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No new route. No frontend change. Same &lt;code&gt;GET /data/animals&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flip to 500
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"nameResponse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"server-error"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and curl once more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt; &lt;span class="ne"&gt;Internal Server Error&lt;/span&gt;

{"message":"Internal server error"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole feature: &lt;strong&gt;same URL, different backend behavior, one string in JSON&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;nameResponse&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Body&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;success&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;list of animals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;not-found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;error message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server-error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;error message&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use the 404 for empty/not-found UI. Use the 500 for the generic failure banner. Switch back to &lt;code&gt;success&lt;/code&gt; when you need the list again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same idea on POST
&lt;/h2&gt;

&lt;p&gt;You can attach another &lt;code&gt;responses&lt;/code&gt; array to &lt;code&gt;POST&lt;/code&gt; on the same path. &lt;code&gt;nameResponse&lt;/code&gt; is per method:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data/animals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"nameResponse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"responses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"200"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"animals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lion"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"server-error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"500"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Internal server error"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"nameResponse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"responses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"201"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Eagle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"created"&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;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"server-error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"500"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Could not save animal"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3001/data/animals &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"Eagle"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;"nameResponse": "created"&lt;/code&gt; you get &lt;strong&gt;201&lt;/strong&gt;. Set it to &lt;code&gt;"server-error"&lt;/code&gt; and the same POST returns &lt;strong&gt;500&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GET and POST do not share &lt;code&gt;nameResponse&lt;/code&gt;. You can have a healthy list and a failing create at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you just got
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Several scenarios on one endpoint, in a file the team can review&lt;/li&gt;
&lt;li&gt;200 / 404 / 500 (and 201 on POST) without touching the client&lt;/li&gt;
&lt;li&gt;A switch that is one key: &lt;code&gt;nameResponse&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/http-mock-json" rel="noopener noreferrer"&gt;http-mock-json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/alejandrorodrom/http-mock-json" rel="noopener noreferrer"&gt;alejandrorodrom/http-mock-json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://rodriguezrom.com/libraries/http-mock-json" rel="noopener noreferrer"&gt;rodriguezrom.com/libraries/http-mock-json&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this helps, drop a ⭐ on the repo or comment which status you mock first.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>frontend</category>
      <category>api</category>
    </item>
    <item>
      <title>Mock Your API in 5 Minutes with JSON (No Backend Required)</title>
      <dc:creator>Alejandro Rodriguez</dc:creator>
      <pubDate>Sun, 09 Aug 2026 16:47:50 +0000</pubDate>
      <link>https://dev.to/alejandrorodrom/mock-your-api-in-5-minutes-with-json-no-backend-required-54c8</link>
      <guid>https://dev.to/alejandrorodrom/mock-your-api-in-5-minutes-with-json-no-backend-required-54c8</guid>
      <description>&lt;p&gt;Your frontend needs the API now. The backend isn’t ready, staging is down, or you want to trigger a 500 without asking anyone to break the server.&lt;/p&gt;

&lt;p&gt;In this article you’ll:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install a local mock server&lt;/li&gt;
&lt;li&gt;Define an endpoint in a JSON file&lt;/li&gt;
&lt;li&gt;Call it with &lt;code&gt;curl&lt;/code&gt; (or from your app) as if it were the real API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This isn’t an embeddable SDK — it’s a &lt;strong&gt;CLI&lt;/strong&gt; that boots HTTP on your machine. Your app talks to &lt;code&gt;http://localhost:3001&lt;/code&gt; the same way it would talk to staging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Node.js &amp;gt;= 22.12&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;A project with a &lt;code&gt;package.json&lt;/code&gt; (recommended)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;http-mock-json &lt;span class="nt"&gt;--save-dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI binary is &lt;code&gt;mock-server&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Initialize
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx mock-server init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default this will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a &lt;code&gt;mocks/&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;Add a &lt;code&gt;mock:start&lt;/code&gt; script to your &lt;code&gt;package.json&lt;/code&gt; (&lt;code&gt;mock-server start -p 3001&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Prompt you to create a first mock&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the prompts, you can use something like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON file name&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;animals&lt;/code&gt; → &lt;code&gt;mocks/animals.json&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Endpoint&lt;/td&gt;
&lt;td&gt;&lt;code&gt;data/animals&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Methods&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The scaffold sets &lt;code&gt;nameResponse&lt;/code&gt; to &lt;code&gt;"success"&lt;/code&gt; and includes two empty responses: &lt;code&gt;success&lt;/code&gt; (&lt;code&gt;200&lt;/code&gt;) and &lt;code&gt;error&lt;/code&gt; (&lt;code&gt;404&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Fill in the response bodies
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;mocks/animals.json&lt;/code&gt; and make it look like this:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data/animals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"nameResponse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"responses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"200"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"animals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lion"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tiger"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"404"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No animals found"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;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;What you need on day one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Top-level key&lt;/td&gt;
&lt;td&gt;Endpoint path (&lt;code&gt;data/animals&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uppercase method&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt;, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nameResponse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Which scenario is returned by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;responses[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Named scenarios with &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;statusCode&lt;/code&gt;, and &lt;code&gt;body&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  4. Start the server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx mock-server start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if &lt;code&gt;init&lt;/code&gt; added the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run mock:start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both listen on port 3001 by default (&lt;a href="http://localhost:3001" rel="noopener noreferrer"&gt;http://localhost:3001&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;On start, every mock file is validated. Errors block startup — fix them and run again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch mode is always on:&lt;/strong&gt; save a mock file and the server reloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://localhost:3001/data/animals
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Use port &lt;code&gt;3001&lt;/code&gt; if you started via the &lt;code&gt;init&lt;/code&gt; script.)&lt;/p&gt;

&lt;p&gt;You should see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json; charset=utf-8&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"animals"&lt;/span&gt;&lt;span class="p"&gt;:[{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Lion"&lt;/span&gt;&lt;span class="p"&gt;},{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Tiger"&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;In your frontend, point the API base URL at that local host. Same HTTP calls — different “backend.”&lt;/p&gt;

&lt;h2&gt;
  
  
  10-second bonus: flip to the error scenario
&lt;/h2&gt;

&lt;p&gt;Change this in the JSON:&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="nl"&gt;"nameResponse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the file (watch reloads) and run &lt;code&gt;curl&lt;/code&gt; again. You now get the 404 with the error message — &lt;strong&gt;without touching the frontend&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That’s the core idea: same URL, different backend behavior, controlled from JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you just got
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A local mock server in minutes&lt;/li&gt;
&lt;li&gt;An endpoint defined in a file the team can read and version&lt;/li&gt;
&lt;li&gt;Success ↔ error by editing a single key&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/http-mock-json" rel="noopener noreferrer"&gt;http-mock-json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/alejandrorodrom/http-mock-json" rel="noopener noreferrer"&gt;alejandrorodrom/http-mock-json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://www.rodriguezrom.com/libraries/http-mock-json" rel="noopener noreferrer"&gt;rodriguezrom.com/libraries/http-mock-json&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this helps, drop a ⭐ on the repo or comment with which API you’re mocking.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>frontend</category>
      <category>testing</category>
    </item>
    <item>
      <title>I Built a Free Planning Poker App with SvelteKit and Cloudflare Durable Objects</title>
      <dc:creator>Alejandro Rodriguez</dc:creator>
      <pubDate>Sun, 09 Aug 2026 05:03:49 +0000</pubDate>
      <link>https://dev.to/alejandrorodrom/i-built-a-free-planning-poker-app-with-sveltekit-and-cloudflare-durable-objects-43b4</link>
      <guid>https://dev.to/alejandrorodrom/i-built-a-free-planning-poker-app-with-sveltekit-and-cloudflare-durable-objects-43b4</guid>
      <description>&lt;p&gt;Scrum teams need Planning Poker often — and they rarely want another account, another SaaS login, or a shared spreadsheet.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://planningpoker.free/" rel="noopener noreferrer"&gt;Planning Poker Free&lt;/a&gt;&lt;/strong&gt;: open-source, no signup, create a room, share the link, and estimate in real time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live app: &lt;a href="https://planningpoker.free/" rel="noopener noreferrer"&gt;planningpoker.free&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/alejandrorodrom/planning-poker-free" rel="noopener noreferrer"&gt;github.com/alejandrorodrom/planning-poker-free&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffnkk09dnces1vxb6qbg8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffnkk09dnces1vxb6qbg8.png" alt="Planning Poker Free board" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Most estimation tools ask for accounts, orgs, or paid seats before the team can vote. For a 15-minute refinement, that friction is too high.&lt;/p&gt;

&lt;p&gt;I wanted something closer to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a room&lt;/li&gt;
&lt;li&gt;Share &lt;code&gt;/room/{id}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Vote&lt;/li&gt;
&lt;li&gt;Reveal and discuss&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No accounts. Optional password. Done.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rooms:&lt;/strong&gt; public or password-protected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Roles:&lt;/strong&gt; moderator, voter, observer (with handoff if the moderator drops)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decks:&lt;/strong&gt; modified Fibonacci, Fibonacci, powers of two, T-shirt, sequential&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregation:&lt;/strong&gt; consensus, mode, median, or mean&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voting UX:&lt;/strong&gt; hidden/revealed cards, optional auto-reveal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teams:&lt;/strong&gt; overall + per-team results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export:&lt;/strong&gt; Markdown, CSV, or a photo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6a8u0mwesoyfkupedtul.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6a8u0mwesoyfkupedtul.png" alt="Results view" width="585" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UI&lt;/td&gt;
&lt;td&gt;Svelte 5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App framework&lt;/td&gt;
&lt;td&gt;SvelteKit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Cloudflare Workers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Realtime state&lt;/td&gt;
&lt;td&gt;Durable Objects + WebSockets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each room is its own &lt;strong&gt;Durable Object&lt;/strong&gt;: one in-memory state machine plus WebSocket fan-out. No Redis, no always-on Node process — the room wakes when people join and stays consistent while they’re connected.&lt;/p&gt;

&lt;p&gt;That combo is the interesting part for me: realtime multiplayer UX without babysitting infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design choices that mattered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  No accounts on purpose
&lt;/h3&gt;

&lt;p&gt;Session rejoin uses &lt;code&gt;localStorage&lt;/code&gt; so people can refresh and come back. Last active tab wins. Simple, good enough for estimation sessions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Moderators can hand off
&lt;/h3&gt;

&lt;p&gt;If the moderator goes offline, someone else can claim moderation. Real meetings are messy; the tool should survive that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Export is not optional
&lt;/h3&gt;

&lt;p&gt;After the call, teams want notes. Markdown/CSV export makes the session useful beyond the live board.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://planningpoker.free/" rel="noopener noreferrer"&gt;planningpoker.free&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Create a room&lt;/li&gt;
&lt;li&gt;Share the link with your team&lt;/li&gt;
&lt;li&gt;Vote and reveal&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you find a bug or want a feature, open an issue or PR on &lt;a href="https://github.com/alejandrorodrom/planning-poker-free" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s next
&lt;/h2&gt;

&lt;p&gt;If you use it with your team, tell me what broke — or what you wished existed.&lt;/p&gt;




&lt;p&gt;Built and maintained by &lt;a href="https://www.rodriguezrom.com" rel="noopener noreferrer"&gt;Alejandro Rodriguez Romero&lt;/a&gt; / RODRIGUEZROM.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>sveltekit</category>
      <category>cloudflare</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
