<?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: Faiz Ahmed Farooqui</title>
    <description>The latest articles on DEV Community by Faiz Ahmed Farooqui (@faizahmedfarooqui).</description>
    <link>https://dev.to/faizahmedfarooqui</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%2F3982774%2F6372f4e0-2c68-4dd6-a56d-658d8dbb2a1c.png</url>
      <title>DEV Community: Faiz Ahmed Farooqui</title>
      <link>https://dev.to/faizahmedfarooqui</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/faizahmedfarooqui"/>
    <language>en</language>
    <item>
      <title>How I Actually Use AI to Ship Code: Context Engineering Over Clever Prompts</title>
      <dc:creator>Faiz Ahmed Farooqui</dc:creator>
      <pubDate>Thu, 02 Jul 2026 04:54:26 +0000</pubDate>
      <link>https://dev.to/faizahmedfarooqui/how-i-actually-use-ai-to-ship-code-context-engineering-over-clever-prompts-il8</link>
      <guid>https://dev.to/faizahmedfarooqui/how-i-actually-use-ai-to-ship-code-context-engineering-over-clever-prompts-il8</guid>
      <description>&lt;p&gt;The last post in this series was about what &lt;em&gt;not&lt;/em&gt; to feed an AI: &lt;a href="https://faizahmed.in/using-ai-without-leaking-secrets" rel="noopener noreferrer"&gt;keep your secrets out of the prompt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This one is the other half of the same coin. Once you're using AI safely, how do you use it so it actually multiplies your output instead of generating confident, plausible garbage you spend the afternoon unwinding?&lt;/p&gt;

&lt;p&gt;After a lot of shipping with these tools, my conclusion is boring but load-bearing: the gap between "AI helps" and "AI hurts" is almost never the model. It's the &lt;em&gt;context&lt;/em&gt; you give it. The skill that matters isn't writing a clever prompt. It's engineering the&lt;br&gt;
context so the obvious prompt produces the right answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode: vibes-based prompting
&lt;/h2&gt;

&lt;p&gt;Here's the loop most people are stuck in. You ask for something in a sentence, you get back code that looks right, and then you notice it used a library you don't use, named things against your conventions, and re-introduced a bug you fixed last week. So you rewrite it, and the next task starts from zero again.&lt;/p&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%2Fedyen9caawpgikypsdgh.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%2Fedyen9caawpgikypsdgh.png" alt=" " width="797" height="107"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An AI with no context is a sharp junior who has never read your codebase and remembers nothing from yesterday. You wouldn't hand that person a one-line ticket and expect a mergeable PR. The fix isn't a smarter junior. It's an onboarding doc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context engineering: a project memory file
&lt;/h2&gt;

&lt;p&gt;The single highest-leverage thing I do is keep a project memory file in the repo, the kind most agentic tools now read automatically. It is the onboarding doc, except the new hire reads it perfectly every single time.&lt;/p&gt;

&lt;p&gt;What goes in it isn't generic style advice. It's the hard-won, project-specific stuff that an outsider could not guess. A few representative entries from this very site, abbreviated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-negotiables.&lt;/strong&gt; Canonical URLs always point at the production host; post slugs must match the live URLs exactly, because changing one breaks a ranked page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Computed, never hardcoded.&lt;/strong&gt; Years of experience is derived from a start date in one config, never written as a literal number anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The gotchas that cost an afternoon.&lt;/strong&gt; One edge setting at the CDN must stay off or it silently breaks the JavaScript; the content config has to live at one exact path or the framework rejects it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every line in that file is a mistake that happened once and is now structurally prevented from happening again. That's the real point: the file is a &lt;em&gt;compounding asset&lt;/em&gt;. The repo teaches the assistant, so I stop re-explaining the same five things, and the quality of the first draft climbs over time instead of resetting each session.&lt;/p&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%2Fmswi1yvob6v28ipdxrml.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%2Fmswi1yvob6v28ipdxrml.png" alt=" " width="798" height="158"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The loop that matters is the dotted-back edge: when the model gets something wrong in a way that will recur, the fix isn't just correcting this output. It's adding a line to the context file so the whole class of mistake is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meta-prompting: point the AI at its own instructions
&lt;/h2&gt;

&lt;p&gt;The second habit is using the AI on its &lt;em&gt;own&lt;/em&gt; setup. A few moves I lean on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bootstrap the context file from the code.&lt;/strong&gt; Ask the model to read the project and draft the memory file, then edit it down. It's faster at inventorying conventions than I am, and I keep the judgment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make it restate the task first.&lt;/strong&gt; For anything non-trivial, I ask it to summarize what it's about to do before it does it. Half the bad outputs die right there, in the restatement, before any code is written.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask it to improve the prompt.&lt;/strong&gt; "What's ambiguous about what I just asked?" is often more useful than the answer to the original question.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is tied to a particular product. I move between an agentic CLI in the terminal, a browser chat for one-off questions and rubber-ducking, and inline editor completions, depending on the task. The tools change every few months. The principle, which is that you engineer context and make the implicit explicit, does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it helps, and where it doesn't
&lt;/h2&gt;

&lt;p&gt;Being honest about the boundary is what keeps this from becoming hype. The tools are a force multiplier on some work and a liability on the rest.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reach for AI&lt;/th&gt;
&lt;th&gt;Keep it human&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Boilerplate and scaffolding&lt;/td&gt;
&lt;td&gt;Novel architecture decisions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pattern-following refactors&lt;/td&gt;
&lt;td&gt;Taste and product calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explaining unfamiliar code&lt;/td&gt;
&lt;td&gt;Anything where confidently wrong is expensive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mechanical sweeps across a repo&lt;/td&gt;
&lt;td&gt;Security-critical design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test and fixture scaffolding&lt;/td&gt;
&lt;td&gt;The final review before it ships&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mechanical-sweep row is not hypothetical. The cleanup that removed a particular punctuation mark from every page of this site, touched across a dozen files in minutes, is exactly the kind of tedious, well-specified, low-judgment work these tools are built&lt;br&gt;
for. The architecture of the site is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow loop
&lt;/h2&gt;

&lt;p&gt;The discipline that ties it together is small and unglamorous:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scope the task small.&lt;/strong&gt; One change with a clear definition of done, not "build the feature."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give it the context up front&lt;/strong&gt;, or trust the memory file to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify every output.&lt;/strong&gt; Run the build, open the preview, read the diff. The four-second answer still has to pass the same bar your own code does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review it like a junior's PR&lt;/strong&gt;, because that's what it is. This is the same habit from the &lt;a href="https://faizahmed.in/using-ai-without-leaking-secrets" rel="noopener noreferrer"&gt;secrets post&lt;/a&gt;: nothing the model produces lands in a commit, a log, or a PR until a human has read it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feed lessons back&lt;/strong&gt; into the context file so the next draft starts higher.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wrote about the &lt;a href="https://faizahmed.in/building-this-blog-astro-cloudflare-umami" rel="noopener noreferrer"&gt;whole build of this site&lt;/a&gt; elsewhere; a good chunk of it ran through exactly this loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The payoff
&lt;/h2&gt;

&lt;p&gt;The leverage was never in the prompt I typed. It's in the context I maintain. A well-instrumented repo, with its conventions and scars written down where the tool can read them, turns an AI from a generator of plausible code into something that ships work that actually fits.&lt;/p&gt;

&lt;p&gt;And notice that the secure workflow from the last post and the high-output workflow from this one are the same workflow. Scoping tightly, keeping secrets out, reviewing every output, writing down what matters: that discipline is what makes AI both safe and worth using.&lt;/p&gt;

&lt;p&gt;Play nice, play safe, ship more.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>developerproductivity</category>
      <category>node</category>
    </item>
    <item>
      <title>The HTTP QUERY Method: GET With a Body, in Node.js and NestJS</title>
      <dc:creator>Faiz Ahmed Farooqui</dc:creator>
      <pubDate>Tue, 23 Jun 2026 17:29:43 +0000</pubDate>
      <link>https://dev.to/faizahmedfarooqui/the-http-query-method-get-with-a-body-in-nodejs-and-nestjs-62d</link>
      <guid>https://dev.to/faizahmedfarooqui/the-http-query-method-get-with-a-body-in-nodejs-and-nestjs-62d</guid>
      <description>&lt;p&gt;If you have ever built a search or filter endpoint, you have made this uncomfortable choice. Use GET, and your complex filter object has to be crammed into the URL. Use POST, and you are using a method that means "create or change something" for a request that changes nothing.&lt;/p&gt;

&lt;p&gt;That dilemma is over a decade old. In June 2026 the IETF finally closed it by publishing &lt;a href="https://www.rfc-editor.org/info/rfc10008/" rel="noopener noreferrer"&gt;RFC 10008&lt;/a&gt;, which standardizes a new HTTP method: &lt;code&gt;QUERY&lt;/code&gt;. In one line, it is GET with a body, done properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What QUERY actually is
&lt;/h2&gt;

&lt;p&gt;Per RFC 10008, a &lt;code&gt;QUERY&lt;/code&gt; request asks the target resource to process the enclosed content in a safe and idempotent manner and respond with the result. You send a request body describing what you want, and the server returns matching results without changing any state.&lt;/p&gt;

&lt;p&gt;That last part is the whole point. &lt;code&gt;QUERY&lt;/code&gt; is the only HTTP method that combines a request body with safe, idempotent, and cacheable semantics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;QUERY&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Safe&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Does not modify server state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idempotent&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Can be retried safely after a failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cacheable&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Responses can be cached, unlike POST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Has a body&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;The query definition lives in the request body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-Type&lt;/td&gt;
&lt;td&gt;Required&lt;/td&gt;
&lt;td&gt;The server MUST reject the request if it is missing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why it had to exist
&lt;/h2&gt;

&lt;p&gt;The gap it fills is precise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GET&lt;/strong&gt; is safe and idempotent, but has no defined body semantics. Sending a body with GET is undefined behaviour, and many clients and proxies actively strip or reject it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;POST&lt;/strong&gt; carries a body, but is neither safe nor idempotent, so caches and intermediaries cannot optimize it and clients cannot safely retry it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex queries&lt;/strong&gt; (GraphQL, JSONPath, structured search filters, large payloads) do not fit in a URL. You need a body. But you also want the request to be cacheable and retriable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;QUERY&lt;/code&gt; sits exactly in that gap. Here is how it compares to the methods you already use:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Has body&lt;/th&gt;
&lt;th&gt;Safe&lt;/th&gt;
&lt;th&gt;Idempotent&lt;/th&gt;
&lt;th&gt;Cacheable&lt;/th&gt;
&lt;th&gt;Primary intent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;No (undefined)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Retrieve by URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QUERY&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Retrieve with a complex body&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Create or trigger an action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUT&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Replace a resource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PATCH&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Partial update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Remove a resource&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you have been weighing &lt;a href="https://faizahmed.in/grpc-vs-rest" rel="noopener noreferrer"&gt;REST against gRPC&lt;/a&gt; for read-heavy services, this narrows the case where REST felt clumsy: the complex read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compatibility, as it really stands
&lt;/h2&gt;

&lt;p&gt;This is where the AI-generated explainers floating around get sloppy, so be precise.&lt;/p&gt;

&lt;p&gt;Native &lt;code&gt;QUERY&lt;/code&gt; parsing first shipped in &lt;strong&gt;Node.js 21.7.2&lt;/strong&gt; via the llhttp 9.2 update. The very early 21.7.x releases had HTTP-parser rough edges, so treat the &lt;strong&gt;22.x LTS line and newer&lt;/strong&gt; as the reliable baseline. I confirmed it directly: on current Node a &lt;code&gt;QUERY&lt;/code&gt; request arrives with &lt;code&gt;req.method === 'QUERY'&lt;/code&gt; and the body intact, no flags or shims.&lt;/p&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;Status&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node.js 22 LTS and newer&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;QUERY&lt;/code&gt; recognized natively; &lt;code&gt;req.method&lt;/code&gt; is &lt;code&gt;'QUERY'&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js 21.7.2 to 21.x&lt;/td&gt;
&lt;td&gt;First support&lt;/td&gt;
&lt;td&gt;Arrived with llhttp 9.2; early parser fixes followed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js before 21.7.2&lt;/td&gt;
&lt;td&gt;Not recognized&lt;/td&gt;
&lt;td&gt;The parser predates QUERY and rejects it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Express&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;No &lt;code&gt;app.query()&lt;/code&gt; shorthand; use an &lt;code&gt;app.all()&lt;/code&gt; guard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NestJS&lt;/td&gt;
&lt;td&gt;No native support&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@Query()&lt;/code&gt; already means URL params; a custom decorator is needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fetch / axios&lt;/td&gt;
&lt;td&gt;Works&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;QUERY&lt;/code&gt; is not a forbidden method, so both accept it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CORS&lt;/td&gt;
&lt;td&gt;Preflight&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;QUERY&lt;/code&gt; is not on the safelist (GET/HEAD/POST), so cross-origin calls preflight&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Check your runtime with &lt;code&gt;node -v&lt;/code&gt; before you write a line of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: plain Node.js
&lt;/h2&gt;

&lt;p&gt;Because &lt;code&gt;QUERY&lt;/code&gt; is a recognized method, the server side needs no tricks. &lt;code&gt;req.method&lt;/code&gt; is simply &lt;code&gt;'QUERY'&lt;/code&gt;, and body handling is identical to POST: collect chunks, concatenate, parse.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:http&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;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QUERY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data&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="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rawBody&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;end&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// RFC 10008: the server MUST reject a QUERY with no Content-Type&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type is required for QUERY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;queryBody&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;queryBody&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;parse&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid JSON body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;performSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryBody&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// QUERY is safe and idempotent, so caching the response is valid&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;max-age=60&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept-Query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&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;results&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeHead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Method Not Allowed&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="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;performSearch&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="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;filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;item1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;item2&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="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Server on http://localhost:3000&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;The client side is just as plain. &lt;code&gt;fetch&lt;/code&gt; accepts any method string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// client.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3000/search&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QUERY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// required per RFC 10008&lt;/span&gt;
  &lt;span class="na"&gt;body&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="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electronics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;inStock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-price&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Axios has no &lt;code&gt;.query()&lt;/code&gt; shorthand yet, so use the generic config form: &lt;code&gt;axios({ method: 'QUERY', url, headers, data })&lt;/code&gt;. And to test from the shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; QUERY http://localhost:3000/search &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;'{"filter": {"category": "electronics"}, "limit": 5}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Part 2: Express
&lt;/h2&gt;

&lt;p&gt;Express has no &lt;code&gt;app.query()&lt;/code&gt; because that name is already taken for reading URL query params. The workaround is &lt;code&gt;app.all()&lt;/code&gt; with a method guard inside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// express-server.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/search&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QUERY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Method Not Allowed&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;max-age=60&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept-Query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;item1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;item2&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="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Express on port 3000&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;If you have several &lt;code&gt;QUERY&lt;/code&gt; routes, wrap the guard in a small helper so the noise lives in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;queryRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QUERY&lt;/span&gt;&lt;span class="dl"&gt;'&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;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Part 3: NestJS, wired up properly
&lt;/h2&gt;

&lt;p&gt;NestJS is the interesting one. There is no &lt;code&gt;@QueryMethod()&lt;/code&gt; decorator, and there cannot be a &lt;code&gt;@Query()&lt;/code&gt; one, because &lt;code&gt;@Query()&lt;/code&gt; already reads URL query params. So you build it: a custom route decorator plus a guard that enforces the method and validates&lt;br&gt;
&lt;code&gt;Content-Type&lt;/code&gt;. This is the same kind of plumbing I leaned on building the &lt;a href="https://faizahmed.in/microservices-in-nestjs-with-rabbitmq-postgresql" rel="noopener noreferrer"&gt;NestJS microservice with RabbitMQ and PostgreSQL&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The decorator&lt;/strong&gt; marks a route as QUERY-only and registers it for all methods, leaving the filtering to the guard:&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="c1"&gt;// decorators/query-method.decorator.ts&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;All&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;applyDecorators&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SetMetadata&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="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IS_QUERY_ROUTE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IS_QUERY_ROUTE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;QueryMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&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;MethodDecorator&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;applyDecorators&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;SetMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;IS_QUERY_ROUTE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;All&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The guard&lt;/strong&gt; rejects anything that is not &lt;code&gt;QUERY&lt;/code&gt;, and enforces the Content-Type rule from the spec:&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="c1"&gt;// guards/query-method.guard.ts&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;CanActivate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ExecutionContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;MethodNotAllowedException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BadRequestException&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="s1"&gt;@nestjs/common&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;Reflector&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="s1"&gt;@nestjs/core&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;IS_QUERY_ROUTE&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="s1"&gt;../decorators/query-method.decorator&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="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QueryMethodGuard&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;CanActivate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;reflector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Reflector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;canActivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ExecutionContext&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isQueryRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reflector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;IS_QUERY_ROUTE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHandler&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isQueryRoute&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&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;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;switchToHttp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;getRequest&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QUERY&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MethodNotAllowedException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This endpoint only accepts QUERY&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BadRequestException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type is required for QUERY&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A validated DTO&lt;/strong&gt;, so the body is type-checked like any other NestJS payload:&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="c1"&gt;// search/dto/search-query.dto.ts&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;IsObject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IsOptional&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IsNumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;IsString&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="s1"&gt;class-validator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SearchQueryDto&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;IsObject&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;IsOptional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;?:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;IsString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;IsOptional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;sort&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="nd"&gt;IsNumber&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;IsOptional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The controller&lt;/strong&gt; ties it together. Notice &lt;code&gt;@Body()&lt;/code&gt; works exactly as it does for POST:&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="c1"&gt;// search/search.controller.ts&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;Controller&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="nx"&gt;Res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UseGuards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HttpCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HttpStatus&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="s1"&gt;@nestjs/common&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;Response&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="s1"&gt;express&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;QueryMethod&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="s1"&gt;../decorators/query-method.decorator&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;QueryMethodGuard&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="s1"&gt;../guards/query-method.guard&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;SearchService&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="s1"&gt;./search.service&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;SearchQueryDto&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="s1"&gt;./dto/search-query.dto&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="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search&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="nd"&gt;UseGuards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;QueryMethodGuard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SearchController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;searchService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SearchService&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="nd"&gt;QueryMethod&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// handles QUERY /search&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;HttpCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HttpStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;queryDto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SearchQueryDto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Res&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;passthrough&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryDto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;max-age=60&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept-Query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service and module are ordinary NestJS. Two things not to forget. &lt;br&gt;
First, that DTO only validates if a &lt;code&gt;ValidationPipe&lt;/code&gt; is active&lt;br&gt;
(&lt;code&gt;app.useGlobalPipes(new ValidationPipe())&lt;/code&gt;); without it, the decorators are inert.&lt;/p&gt;

&lt;p&gt;Second, add &lt;code&gt;QUERY&lt;/code&gt; to your CORS config, or every cross-origin call will fail its preflight:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enableCors&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;QUERY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PATCH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DELETE&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The full request path
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  client["Client: fetch / axios / curl&amp;lt;br/&amp;gt;QUERY /search + Content-Type + body"] --&amp;gt; parser["Node.js HTTP parser (21.7.2+)&amp;lt;br/&amp;gt;req.method === 'QUERY'"]
  parser --&amp;gt; router{Framework}
  router -- Express --&amp;gt; ex["app.all() + method check"]
  router -- NestJS --&amp;gt; nest["@QueryMethod() + QueryMethodGuard&amp;lt;br/&amp;gt;validate method + Content-Type"]
  ex --&amp;gt; resp["Body parsed like POST&amp;lt;br/&amp;gt;Cache-Control valid, QUERY is safe"]
  nest --&amp;gt; resp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Known gaps today
&lt;/h2&gt;

&lt;p&gt;The spec is final, but the ecosystem is still catching up:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node before 21.7.2&lt;/td&gt;
&lt;td&gt;Upgrade. There is no parser-level workaround&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NestJS has no decorator&lt;/td&gt;
&lt;td&gt;Use the &lt;code&gt;@QueryMethod()&lt;/code&gt; + guard pattern above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Express has no shorthand&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;app.all()&lt;/code&gt; with a method check, or the &lt;code&gt;queryRoute()&lt;/code&gt; helper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CORS preflight&lt;/td&gt;
&lt;td&gt;Add &lt;code&gt;QUERY&lt;/code&gt; to your &lt;code&gt;allowedMethods&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swagger / OpenAPI&lt;/td&gt;
&lt;td&gt;No standard tooling yet; document it manually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Axios shorthand&lt;/td&gt;
&lt;td&gt;None; use &lt;code&gt;axios({ method: 'QUERY', ... })&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Should you use it
&lt;/h2&gt;

&lt;p&gt;If you have a search or filter endpoint that has been awkwardly living as a POST, &lt;code&gt;QUERY&lt;/code&gt; is the upgrade it was waiting for: a real body for complex filters, plus safe, idempotent, cacheable semantics that POST could never give you. It pairs especially well with cursor-based listing, so it is worth revisiting &lt;a href="https://faizahmed.in/offset-vs-cursor-vs-keyset-pagination" rel="noopener noreferrer"&gt;how you paginate those results&lt;/a&gt; at the same time.&lt;/p&gt;

&lt;p&gt;One honest caveat on the caching point. RFC 10008 requires a cache to key on the request body, and most shared caches and CDNs do not do that yet. So treat cacheability as something the method now &lt;em&gt;permits&lt;/em&gt;, not something your existing CDN will do for you on&lt;br&gt;
day one.&lt;/p&gt;

&lt;p&gt;It is not a reason to rewrite every endpoint. GET by URL is still right for simple reads.&lt;br&gt;
But for the complex read, the decade-long GET-versus-POST compromise finally has a clean answer.&lt;/p&gt;

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

&lt;p&gt;Everything above was checked against primary sources, and the Node.js behaviour was confirmed by running a real QUERY request, not taken on faith from a generated draft.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc10008.html" rel="noopener noreferrer"&gt;RFC 10008: The HTTP QUERY Method&lt;/a&gt;, and its &lt;a href="https://www.rfc-editor.org/info/rfc10008/" rel="noopener noreferrer"&gt;summary page&lt;/a&gt; at the RFC Editor&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/rfc10008/" rel="noopener noreferrer"&gt;RFC 10008 on the IETF datatracker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nodejs/node/issues/51562" rel="noopener noreferrer"&gt;Node.js issue #51562: Support for the QUERY method&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nodejs.org/en/blog/release/v21.7.2" rel="noopener noreferrer"&gt;Node.js 21.7.2 release notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/expressjs/express/issues/5615" rel="noopener noreferrer"&gt;Express issue #5615: Support HTTP QUERY method&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fetch.spec.whatwg.org/" rel="noopener noreferrer"&gt;Fetch Standard: forbidden methods&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS" rel="noopener noreferrer"&gt;MDN: Cross-Origin Resource Sharing (CORS)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>nestjs</category>
      <category>http</category>
      <category>httpquery</category>
    </item>
    <item>
      <title>Using AI Without Leaking Your Secrets: A Threat Model for AI-Assisted Development</title>
      <dc:creator>Faiz Ahmed Farooqui</dc:creator>
      <pubDate>Tue, 23 Jun 2026 05:47:50 +0000</pubDate>
      <link>https://dev.to/faizahmedfarooqui/using-ai-without-leaking-your-secrets-a-threat-model-for-ai-assisted-development-2l57</link>
      <guid>https://dev.to/faizahmedfarooqui/using-ai-without-leaking-your-secrets-a-threat-model-for-ai-assisted-development-2l57</guid>
      <description>&lt;p&gt;Someone hits an error, copies the whole stack trace into a chat window, and asks the model to "just figure this out fast." Buried three lines into that trace is a &lt;code&gt;DATABASE_URL&lt;/code&gt; with a live password in it. The answer comes back in four seconds. The secret is now somewhere you can't reach.&lt;/p&gt;

&lt;p&gt;Pasting secrets into an LLM prompt is the new paste-to-Pastebin, except you can't delete it from a request log or a training set after the fact. This post is not about avoiding AI. I use it every day to ship code. It's about using it the way you'd use any system that crosses a trust boundary: with a threat model, not a vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where your prompt actually goes
&lt;/h2&gt;

&lt;p&gt;Most people picture a prompt as a private conversation. It isn't. It's an outbound request that fans out into several places, and which places depend entirely on what you're paying for.&lt;/p&gt;

&lt;p&gt;On free and consumer tiers, your inputs are often retained and may be used to improve the model. On paid Pro, Team, and Enterprise tiers, the provider typically contracts &lt;em&gt;not&lt;/em&gt; to train on your data and to keep shorter or zero retention windows. That distinction is real, and it matters: paid seats are genuinely safer. But "not trained on" is not the same as "never stored." Request logs, abuse-detection systems, human-review exceptions, and sub-processors all still exist.&lt;/p&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%2Fmglcad3wad6l1pgt0w1f.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%2Fmglcad3wad6l1pgt0w1f.png" alt=" " width="800" height="112"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are three ways data leaks across that boundary, and only the first is obvious:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What you paste&lt;/strong&gt;, like the stack trace, the config, the snippet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the tool auto-attaches&lt;/strong&gt;, including open files, the surrounding repo, and terminal output. Modern coding assistants pull in context you never explicitly handed them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the model emits&lt;/strong&gt;, such as a secret you fed in earlier, echoed back into a commit, a PR description, or a log line.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The threat model
&lt;/h2&gt;

&lt;p&gt;Here's the framing that keeps this honest: the provider is not an &lt;em&gt;adversary&lt;/em&gt;. They're a &lt;em&gt;trusted-but-unverifiable third party&lt;/em&gt;. The risk isn't that they're lying about training. It's that you can't audit their internal pipeline, and policies, sub-processors, and breach exposure all change over time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assets at risk:&lt;/strong&gt; API keys and tokens, database connection strings, private keys and certificates, customer PII sitting in test fixtures, and proprietary business logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure modes:&lt;/strong&gt; provider retention beyond what you assumed, an over-broad tool context window, a compromised editor extension, or your own future self pasting the model's output somewhere public.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat the prompt channel the way you'd treat any outbound network call from production: as untrusted egress. You wouldn't ship plaintext customer data to a third-party endpoint just because their docs promised they'd be careful with it. The prompt box is that endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  What never goes in a prompt
&lt;/h2&gt;

&lt;p&gt;A short do-not-send list does most of the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live credentials, tokens, or API keys&lt;/li&gt;
&lt;li&gt;The contents of &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.env.local&lt;/code&gt;, &lt;code&gt;.env.production&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Private keys, certificates, or key material&lt;/li&gt;
&lt;li&gt;Real customer PII such as names, emails, or payment data&lt;/li&gt;
&lt;li&gt;Full dumps of proprietary source you wouldn't open-source&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important nuance is that this is about &lt;em&gt;masking&lt;/em&gt;, not abstinence. Placeholders are completely fine, and the model reasons about them just as well: &lt;code&gt;[API_KEY]&lt;/code&gt;, &lt;code&gt;[DB_PASSWORD]&lt;/code&gt;, &lt;code&gt;postgres://user:****@host/db&lt;/code&gt;. You almost never need the real value to get the real answer. If you already have a &lt;a href="https://dev.to/secrets-sprawl-in-nodejs-projects-detection-prevention-and-secure-deployment-2025"&gt;secrets-sprawl problem&lt;/a&gt;, AI assistance just adds one more exfiltration path on top of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context hygiene: make leaks structurally impossible
&lt;/h2&gt;

&lt;p&gt;Relying on "remember to be careful" fails the first time you're tired. Build the discipline into the workflow instead, as a loop that runs before anything leaves your machine.&lt;/p&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%2Fq8meueeap4wluz40fjm8.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%2Fq8meueeap4wluz40fjm8.png" alt=" " width="798" height="215"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three concrete habits carry it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An ignore file for AI tools&lt;/strong&gt;, which is the AI-era &lt;code&gt;.gitignore&lt;/code&gt;. Keep &lt;code&gt;.env&lt;/code&gt;, secrets directories, and key material out of the context your assistant auto-attaches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan before you send.&lt;/strong&gt; Run secret detection as a pre-prompt habit, not only at pre-commit. The cheapest leak to fix is the one that never left the editor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep plaintext from existing at all.&lt;/strong&gt; This is where the deeper principle lives. If secrets are stored as ciphertext and only decrypted into memory at runtime, there is no pasteable plaintext to leak in the first place. That's exactly what &lt;a href="https://dev.to/what-is-envelope-encryption"&gt;envelope encryption&lt;/a&gt; and a &lt;a href="https://dev.to/nodejs-secrets-threat-model-aws-kms"&gt;KMS-backed secrets threat model&lt;/a&gt; buy you, and it's why I &lt;a href="https://dev.to/encrypted-env-aws-kms-nodejs-complete-guide"&gt;keep encrypted env files out of the codebase entirely&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cloud vs paid tier vs local: picking the right home for the data
&lt;/h2&gt;

&lt;p&gt;The honest answer isn't "self-host everything," which throws away most of the value.&lt;br&gt;
It's matching the tier to the sensitivity of what you're sending.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;No-training guarantee&lt;/th&gt;
&lt;th&gt;Verifiable?&lt;/th&gt;
&lt;th&gt;Use it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free / consumer chat&lt;/td&gt;
&lt;td&gt;Often no, may train&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Throwaway snippets, public docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paid Pro / Team / Enterprise (with DPA)&lt;/td&gt;
&lt;td&gt;Yes, contractually&lt;/td&gt;
&lt;td&gt;Contractually, not technically&lt;/td&gt;
&lt;td&gt;Most proprietary engineering work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local / self-hosted model&lt;/td&gt;
&lt;td&gt;Not applicable, never leaves&lt;/td&gt;
&lt;td&gt;Yes, by construction&lt;/td&gt;
&lt;td&gt;Regulated data you must &lt;em&gt;prove&lt;/em&gt; stayed home&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the decision, as a flow:&lt;/p&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%2Fr7eeot721zo5hil5rfjf.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%2Fr7eeot721zo5hil5rfjf.png" alt=" " width="762" height="1162"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For most real engineering work, a paid tier with a no-training agreement is the pragmatic default. Say it plainly, because it's true. You step down to local or self-hosted models only when the data is regulated and you need to &lt;em&gt;prove&lt;/em&gt; it never transited a third party, not merely be promised so. Payment data under PCI, health&lt;br&gt;
data under HIPAA: that should never touch a consumer chat tier, full stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero-trust, applied to AI
&lt;/h2&gt;

&lt;p&gt;This is the part that ties it together. We use the paid tiers. We trust the no-training promise enough to do real work on them. And we &lt;em&gt;still&lt;/em&gt; don't paste the crown jewels.&lt;br&gt;
Not because we think the provider is lying, but because the cheapest secret to protect is the one that never left the machine.&lt;/p&gt;

&lt;p&gt;A no-training guarantee is a &lt;em&gt;contractual&lt;/em&gt; control, not a &lt;em&gt;technical&lt;/em&gt; one. It lowers risk; it doesn't eliminate it. So we treat it as one layer and assume the prompt channel could be hostile anyway, the same&lt;br&gt;
&lt;a href="https://dev.to/zero-trust-encryption-a-security-first-approach"&gt;zero-trust posture&lt;/a&gt; you'd apply to any boundary: verify, scope, minimize. Least-context, not just least-privilege. This isn't distrust. It's the same reason you encrypt data at rest even though your cloud&lt;br&gt;
provider already promised the disks are safe. Play nice, play safe, defense in depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Before AI touches your code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add an AI-tool ignore file; exclude &lt;code&gt;.env&lt;/code&gt;, secrets directories, and key material from auto-attached context.&lt;/li&gt;
&lt;li&gt;Run a secret scan on anything you're about to paste.&lt;/li&gt;
&lt;li&gt;Mask credentials and PII with placeholders, never live values.&lt;/li&gt;
&lt;li&gt;Use a paid tier with a no-training agreement for proprietary work; reserve local/self-hosted for regulated data you must prove never left.&lt;/li&gt;
&lt;li&gt;Keep real secrets in a KMS or keystore so plaintext never exists to paste.&lt;/li&gt;
&lt;li&gt;Review the AI's &lt;em&gt;output&lt;/em&gt; before it lands in logs, PRs, or commits. Models echo back what you fed them.&lt;/li&gt;
&lt;li&gt;Treat the prompt channel as untrusted egress: log and minimize what crosses it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this slows you down once it's a habit. The four-second answer is still four seconds. It just doesn't cost you a secret you can never take back.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>aws</category>
      <category>secrets</category>
    </item>
    <item>
      <title>The Build Log: Rebuilding My Blog on Astro, Cloudflare Pages &amp; Umami</title>
      <dc:creator>Faiz Ahmed Farooqui</dc:creator>
      <pubDate>Tue, 23 Jun 2026 05:39:02 +0000</pubDate>
      <link>https://dev.to/faizahmedfarooqui/the-build-log-rebuilding-my-blog-on-astro-cloudflare-pages-umami-5718</link>
      <guid>https://dev.to/faizahmedfarooqui/the-build-log-rebuilding-my-blog-on-astro-cloudflare-pages-umami-5718</guid>
      <description>&lt;p&gt;I already wrote about &lt;a href="https://dev.to/moving-off-hashnode-self-hosting-everything"&gt;&lt;em&gt;why&lt;/em&gt; I left Hashnode&lt;/a&gt;.&lt;br&gt;
This is the other half: &lt;em&gt;how&lt;/em&gt; the replacement actually works. Every meaningful&lt;br&gt;
decision, the code behind it, and the bugs that cost me an afternoon — so future-me&lt;br&gt;
(and anyone rebuilding their own site) doesn't relearn them.&lt;/p&gt;

&lt;p&gt;The whole thing is an &lt;a href="https://astro.build" rel="noopener noreferrer"&gt;Astro&lt;/a&gt; site in a Git repo: profile and&lt;br&gt;
blog on one domain, built to static HTML, served from Cloudflare Pages, with cookieless&lt;br&gt;
analytics I host myself. No platform lock-in, no CDN I don't control.&lt;/p&gt;
&lt;h2&gt;
  
  
  The principles
&lt;/h2&gt;

&lt;p&gt;Everything below falls out of four rules I set up front:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't break the SEO.&lt;/strong&gt; My posts have ranked for years. The migration had to be lossless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Own the assets.&lt;/strong&gt; No hot-linking images or fonts from someone else's CDN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship almost no JavaScript.&lt;/strong&gt; Static pages, a few small scripts, nothing more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stay free and private.&lt;/strong&gt; No per-seat SaaS, no cookies, no consent banner.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Content model
&lt;/h2&gt;

&lt;p&gt;Posts are Markdown files, one folder per post, images colocated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/content/blog/&amp;lt;slug&amp;gt;/index.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The collection uses Astro's Content Layer with a Zod schema. The important detail is&lt;br&gt;
the public URL: it comes from a &lt;code&gt;slug&lt;/code&gt; frontmatter field, &lt;strong&gt;not&lt;/strong&gt; the folder name —&lt;br&gt;
because the slug has to match the old Hashnode URL exactly.&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="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;..."&lt;/span&gt;
&lt;span class="na"&gt;datePublished&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mon Jun 13 2022 09:14:14 GMT+0000 (...)&lt;/span&gt;
&lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-post-slug&lt;/span&gt;      &lt;span class="c1"&gt;# becomes /my-post-slug — the ranked URL&lt;/span&gt;
&lt;span class="na"&gt;cover&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./cover.jpg&lt;/span&gt;      &lt;span class="c1"&gt;# local, optional&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;a, b, c&lt;/span&gt;
&lt;span class="na"&gt;series&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;encryption&lt;/span&gt;      &lt;span class="c1"&gt;# or null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migration was a script. Hashnode's export gave me Markdown with their CDN image URLs;&lt;br&gt;
I wrote &lt;code&gt;import-posts.mjs&lt;/code&gt; to pull every post in, strip the repeated "About Me"&lt;br&gt;
footer, ensure a &lt;code&gt;series:&lt;/code&gt; field, and &lt;strong&gt;download every remote image into the post&lt;br&gt;
folder&lt;/strong&gt; so nothing points at &lt;code&gt;cdn.hashnode.com&lt;/code&gt; anymore. A second pass,&lt;br&gt;
&lt;code&gt;optimize-images.mjs&lt;/code&gt;, downsizes them with &lt;code&gt;sharp&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Preserving SEO on the cutover
&lt;/h2&gt;

&lt;p&gt;This was the part I refused to get wrong. Two pieces:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Canonical URLs.&lt;/strong&gt; Every page declares its canonical on the apex host, driven by one constant:&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;const&lt;/span&gt; &lt;span class="nx"&gt;canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canonicalPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CANONICAL_ORIGIN&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// https://faizahmed.in/...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Path-preserved redirects.&lt;/strong&gt; The old &lt;code&gt;blog.faizahmed.in/&amp;lt;slug&amp;gt;&lt;/code&gt; URLs now &lt;code&gt;301&lt;/code&gt; to&lt;br&gt;
&lt;code&gt;faizahmed.in/&amp;lt;slug&amp;gt;&lt;/code&gt; via a Cloudflare Redirect Rule — path preserved, so every ranked&lt;br&gt;
slug maps 1:1. Because I kept posts at the root (no &lt;code&gt;/blog/&lt;/code&gt; prefix) and kept the exact&lt;br&gt;
slugs, the redirect is lossless and the ranking signal consolidates onto one host.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;sitemap.xml&lt;/code&gt;, per-topic RSS feeds, JSON-LD (&lt;code&gt;BlogPosting&lt;/code&gt;, &lt;code&gt;Person&lt;/code&gt;, &lt;code&gt;WebSite&lt;/code&gt;),&lt;br&gt;
and an allow-all &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bug that haunted returning visitors
&lt;/h2&gt;

&lt;p&gt;My old Gatsby site had registered a service worker (&lt;code&gt;gatsby-plugin-offline&lt;/code&gt;). That&lt;br&gt;
worker was &lt;em&gt;still installed in every returning visitor's browser&lt;/em&gt;, happily serving the&lt;br&gt;
old cached site — even after I'd changed everything. A hard refresh doesn't help,&lt;br&gt;
because the worker sits in front of the network.&lt;/p&gt;

&lt;p&gt;The fix is a kill-switch service worker at the same path that unregisters itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;install&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;skipWaiting&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;activate&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unregister&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;clients&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchAll&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;window&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="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;Served with &lt;code&gt;Cache-Control: no-cache&lt;/code&gt; so browsers always re-fetch it. On a stuck&lt;br&gt;
visitor's next visit, it wipes the old caches, unregisters, and reloads them onto the&lt;br&gt;
live site — no action on their end.&lt;/p&gt;
&lt;h2&gt;
  
  
  Search with no backend
&lt;/h2&gt;

&lt;p&gt;⌘K search is a static JSON index (&lt;code&gt;/search.json&lt;/code&gt;) built at deploy time, filtered&lt;br&gt;
client-side over title, tags, series, and an excerpt. No server, works in dev and prod.&lt;/p&gt;

&lt;p&gt;The gotcha: I inject result rows with &lt;code&gt;innerHTML&lt;/code&gt;, and &lt;strong&gt;Astro's scoped styles don't&lt;br&gt;
apply to nodes created that way&lt;/strong&gt; — they don't get the scoping attribute. The fix was&lt;br&gt;
moving the result-row CSS into a &lt;code&gt;&amp;lt;style is:global&amp;gt;&lt;/code&gt; block. (This bites you again with&lt;br&gt;
any JS-generated DOM — keep it in mind.)&lt;/p&gt;
&lt;h2&gt;
  
  
  Diagrams: Mermaid, and a render bug
&lt;/h2&gt;

&lt;p&gt;Posts render Mermaid diagrams client-side. My first version used Mermaid's&lt;br&gt;
&lt;code&gt;startOnLoad&lt;/code&gt;, and diagrams silently refused to render. The cause: I import Mermaid&lt;br&gt;
asynchronously, so by the time it initializes the &lt;code&gt;load&lt;/code&gt; event has already fired and&lt;br&gt;
&lt;code&gt;startOnLoad&lt;/code&gt; never triggers. The fix is to render explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;mermaid&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;mermaidUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;mermaid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;startOnLoad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;mermaid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pre.mermaid:not([data-processed])&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;I also added a full-screen &lt;strong&gt;click-to-zoom&lt;/strong&gt; viewer (scroll/pinch to zoom, drag to&lt;br&gt;
pan) because a wide sequence diagram crammed into a text column is unreadable.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tag hygiene
&lt;/h2&gt;

&lt;p&gt;The import dragged in &lt;strong&gt;220 tags across ~90 posts&lt;/strong&gt; — mostly single-use, plus&lt;br&gt;
spelling duplicates (&lt;code&gt;ci-cd&lt;/code&gt; vs &lt;code&gt;cicd&lt;/code&gt;, &lt;code&gt;pci-dss&lt;/code&gt; vs &lt;code&gt;pcidss&lt;/code&gt;). That's a pile of thin,&lt;br&gt;
near-duplicate pages search engines hate. Two fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;consolidate-tags.mjs&lt;/code&gt; script with a &lt;code&gt;MERGE&lt;/code&gt; map rewrites variant spellings to one
canonical tag, and removed slugs &lt;code&gt;301&lt;/code&gt; to their canonical.&lt;/li&gt;
&lt;li&gt;Tag pages with fewer than 2 posts get &lt;code&gt;noindex, follow&lt;/code&gt; and are dropped from the
sitemap — still navigable, no thin-content drag.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Analytics that actually counts
&lt;/h2&gt;

&lt;p&gt;I run &lt;a href="https://umami.is" rel="noopener noreferrer"&gt;Umami&lt;/a&gt; — cookieless, no consent banner. But there's a catch&lt;br&gt;
for a developer audience: ad/tracker blockers (and Brave Shields) block known&lt;br&gt;
analytics hosts by name, so a big chunk of &lt;em&gt;my&lt;/em&gt; readers never get counted.&lt;/p&gt;

&lt;p&gt;The fix is a &lt;strong&gt;first-party proxy&lt;/strong&gt;. A Cloudflare Pages Function serves the tracker and&lt;br&gt;
forwards the beacon under my own domain, so to the browser it's all &lt;code&gt;faizahmed.in&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// functions/u/[[path]].js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SCRIPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://cloud.umami.is/script.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COLLECT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://gateway.umami.is/api/send&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onRequest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&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;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;u/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/script.js&lt;/span&gt;&lt;span class="dl"&gt;"&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SCRIPT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// (cached, JS content-type)&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;path&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/send&lt;/span&gt;&lt;span class="dl"&gt;"&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;COLLECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* forward IP + UA */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the browser only ever talks to my domain, I could also tighten the CSP back to&lt;br&gt;
&lt;code&gt;connect-src 'self'&lt;/code&gt;. And since I wanted to know what people click, a single delegated&lt;br&gt;
handler fires a Umami event for every link — &lt;code&gt;internal-link&lt;/code&gt;, &lt;code&gt;outbound-link&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;email-link&lt;/code&gt;, &lt;code&gt;tel-link&lt;/code&gt; — with the URL and where it was clicked.&lt;/p&gt;
&lt;h2&gt;
  
  
  View Transitions (and making scripts survive them)
&lt;/h2&gt;

&lt;p&gt;I enabled Astro's View Transitions so navigation feels instant and I could show a&lt;br&gt;
small loader on slow loads. The trap: with client-side navigation, scripts that attach&lt;br&gt;
listeners &lt;em&gt;on load&lt;/em&gt; stop working after the first navigation, because the DOM gets&lt;br&gt;
swapped. The pattern that fixes it — re-initialize on every navigation, bind&lt;br&gt;
document-level listeners only once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;astro:page-load&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// bind listeners to the fresh elements here&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;transition:persist&lt;/code&gt; looked tempting for keeping the header's listeners alive, but it&lt;br&gt;
&lt;strong&gt;doesn't preserve script-attached listeners&lt;/strong&gt; on components — it cost me a round of&lt;br&gt;
"why is the theme toggle dead." The &lt;code&gt;astro:page-load&lt;/code&gt; re-init is the reliable way.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Safari afternoon: Rocket Loader
&lt;/h2&gt;

&lt;p&gt;Then everything broke in Safari — toggle, search, dropdown, all dead — while Chrome was&lt;br&gt;
fine. The culprit was &lt;strong&gt;Cloudflare Rocket Loader&lt;/strong&gt;, an "optimization" that rewrites and&lt;br&gt;
defers your &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags at the edge. On a modern ES-module + event setup it breaks&lt;br&gt;
things, and Safari most of all. It was &lt;em&gt;also&lt;/em&gt; the original reason Mermaid misbehaved.&lt;/p&gt;

&lt;p&gt;There's no in-repo fix — Cloudflare generates the rewrite after your build — so it's a&lt;br&gt;
dashboard toggle: &lt;strong&gt;Speed → Optimization → Rocket Loader → Off.&lt;/strong&gt; Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://faizahmed.in/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; rocket-loader   &lt;span class="c"&gt;# want 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a static site this lean, Rocket Loader does nothing useful and only breaks things. Off, permanently.&lt;/p&gt;

&lt;h2&gt;
  
  
  GEO: optimizing for the AI engines too
&lt;/h2&gt;

&lt;p&gt;SEO gets you ranked; GEO (Generative Engine Optimization) gets you &lt;em&gt;cited&lt;/em&gt; by AI answer&lt;br&gt;
engines. The cheapest high-leverage move is an &lt;a href="https://llmstxt.org" rel="noopener noreferrer"&gt;&lt;code&gt;llms.txt&lt;/code&gt;&lt;/a&gt; — a&lt;br&gt;
curated, plain-text map of the site for LLMs. Mine is generated at build: a short bio,&lt;br&gt;
the key pages, every series, and every post with a one-line description. Combined with&lt;br&gt;
the allow-all &lt;code&gt;robots.txt&lt;/code&gt;, clean JSON-LD, and semantic HTML, the site is easy for an&lt;br&gt;
assistant to read and quote.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it adds up to
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Static Astro, zero UI framework, self-hosted fonts, locally-optimized images.&lt;/li&gt;
&lt;li&gt;One domain, lossless 301s, canonical consolidation — SEO intact.&lt;/li&gt;
&lt;li&gt;Cookieless, blocker-proof, free analytics I own.&lt;/li&gt;
&lt;li&gt;Client-side search, rendered diagrams with zoom, View Transitions + a loader.&lt;/li&gt;
&lt;li&gt;A tight CSP (&lt;code&gt;connect-src 'self'&lt;/code&gt;), allow-all for crawlers and AI agents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest lesson? Most of my time didn't go into building features — it went into&lt;br&gt;
the edge: a stale service worker, an "optimization" that broke Safari, scripts that&lt;br&gt;
forgot to re-run after navigation. The platform you control is worth exactly these&lt;br&gt;
afternoons.&lt;/p&gt;

&lt;p&gt;It's all in a Git repo now. That was the whole point.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>cloudflare</category>
      <category>jamstack</category>
      <category>umami</category>
    </item>
    <item>
      <title>Encrypt your .env with AWS KMS: Secrets that never touch process.env</title>
      <dc:creator>Faiz Ahmed Farooqui</dc:creator>
      <pubDate>Sat, 13 Jun 2026 14:24:01 +0000</pubDate>
      <link>https://dev.to/faizahmedfarooqui/encrypt-your-env-with-aws-kms-secrets-that-never-touch-processenv-180m</link>
      <guid>https://dev.to/faizahmedfarooqui/encrypt-your-env-with-aws-kms-secrets-that-never-touch-processenv-180m</guid>
      <description>&lt;p&gt;A year ago I'd have told you a .env file was fine.&lt;/p&gt;

&lt;p&gt;Then we patched a &lt;strong&gt;CVSS 10.0 RCE&lt;/strong&gt; in Next.js (&lt;a href="https://nextjs.org/blog/CVE-2025-66478" rel="noopener noreferrer"&gt;CVE-2025-66478&lt;/a&gt;) and spent the next two days rotating every secret we owned — because we couldn't prove which ones an attacker could have read. They were all sitting in process.env. One env dump away from gone.&lt;/p&gt;

&lt;p&gt;That incident is why I built &lt;a href="https://www.npmjs.com/package/@faizahmed/secret-keystore" rel="noopener noreferrer"&gt;&lt;code&gt;@faizahmed/secret-keystore&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual problem isn't committing &lt;code&gt;.env&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Everyone knows not to commit secrets. The part that hurts you is what happens the moment your process is compromised. The default Node setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// every secret → process.env, at startup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attacker gets code execution (a dep RCE, an SSRF, a framework CVE). Their first move:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. Every DB password, API key, and JWT secret you own, in plaintext, in one place. That's your &lt;strong&gt;blast radius&lt;/strong&gt; — and then you're rotating everything and hoping, because you can't prove what leaked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: a KMS Key ID is not a secret
&lt;/h2&gt;

&lt;p&gt;The whole design rests on one decision: &lt;strong&gt;the only thing a developer ever handles is an AWS KMS Key ID&lt;/strong&gt; — which isn't sensitive. It's a pointer. The key material never leaves KMS, and access is gated by IAM. No private keys, no passphrases, nothing for anyone to leak.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;.env&lt;/code&gt; stores ciphertext:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_PASSWORD=ENC[AQICAHh2nZPq...]
API_KEY=ENC[AQICAHh2nZPq...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At runtime, values are decrypted &lt;strong&gt;on demand into an in-memory store&lt;/strong&gt; — and never put back into &lt;code&gt;process.env&lt;/code&gt;. So the next RCE leaks the handful of keys your code actually touched, not the entire vault.&lt;/p&gt;

&lt;h3&gt;
  
  
  In practice
&lt;/h3&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; @faizahmed/secret-keystore

&lt;span class="c"&gt;# encrypt the secrets in your .env (in place)&lt;/span&gt;
npx @faizahmed/secret-keystore encrypt &lt;span class="nt"&gt;--kms-key-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"alias/my-key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load them at runtime without ever touching &lt;code&gt;process.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@faizahmed/secret-keystore&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;secrets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;kmsKeyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alias/my-key&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;dbPassword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// decrypted, in memory only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, for an app you don't want to modify, inject into the child process and run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @faizahmed/secret-keystore run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--kms-key-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"alias/my-key"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  node server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also &lt;code&gt;rotate&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;keys&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, and &lt;code&gt;import&lt;/code&gt; — plus optional AWS Nitro Enclave attestation when you need to prove what's running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Being honest about what it does NOT do
&lt;/h2&gt;

&lt;p&gt;It's not magic, and the README says so. An attacker with full code execution &lt;strong&gt;inside&lt;/strong&gt; your process can still call the keystore or scrape memory. You still patch and rotate! &lt;/p&gt;

&lt;p&gt;What it removes is bulk exposure: no single &lt;strong&gt;env&lt;/strong&gt; dump that hands over everything, no plaintext in git, and secret access that's per-key and grep-able.&lt;/p&gt;

&lt;p&gt;It's also &lt;strong&gt;AWS-only by design&lt;/strong&gt;, that's the point. The moment you hand a human a private key or passphrase (age, PGP, password-based tools), you recreate the leak risk KMS exists to remove. &lt;/p&gt;

&lt;p&gt;If you need multi-cloud, SOPS is the better fit. If you're already on Secrets Manager/SSM, use those - same KMS underneath; this is for teams who want encrypted config files in their existing workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full write-up
&lt;/h2&gt;

&lt;p&gt;I wrote a 4-part series with the complete threat model, every CLI command, the runtime/&lt;code&gt;config()&lt;/code&gt; internals, and a comparison to dotenvx / SOPS / Secrets Manager:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.faizahmed.in/encrypted-env-aws-kms-nodejs-complete-guide" rel="noopener noreferrer"&gt;The Complete Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.faizahmed.in/nodejs-secrets-threat-model-aws-kms" rel="noopener noreferrer"&gt;Part 1 — Your .env Is a Loaded Gun (threat model)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.faizahmed.in/secret-keystore-cli-encrypt-env-aws-kms" rel="noopener noreferrer"&gt;Part 2 — The CLI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.faizahmed.in/secret-keystore-runtime-config-loader-nodejs" rel="noopener noreferrer"&gt;Part 3 — Runtime, rotation &amp;amp; attestation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo (with runnable Next.js + NestJS examples): &lt;a href="https://github.com/faizahmedfarooqui/secret-keystore" rel="noopener noreferrer"&gt;github.com/faizahmedfarooqui/secret-keystore&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you ship Node on AWS and have ever had to "rotate everything and hope," this is the pattern I wish I'd had before the incident. Feedback and hard questions welcome.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>backend</category>
      <category>security</category>
    </item>
  </channel>
</rss>
