<?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: Allen Jones</title>
    <description>The latest articles on DEV Community by Allen Jones (@allenarduino).</description>
    <link>https://dev.to/allenarduino</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%2F679555%2F89b03aae-198c-48b1-acb5-9637c4f5ef61.jpeg</url>
      <title>DEV Community: Allen Jones</title>
      <link>https://dev.to/allenarduino</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/allenarduino"/>
    <language>en</language>
    <item>
      <title>JavaScript and TypeScript Interview Questions Explained With Real Production Examples</title>
      <dc:creator>Allen Jones</dc:creator>
      <pubDate>Thu, 10 Sep 2026 01:20:07 +0000</pubDate>
      <link>https://dev.to/allenarduino/javascript-and-typescript-interview-questions-explained-with-real-production-examples-176e</link>
      <guid>https://dev.to/allenarduino/javascript-and-typescript-interview-questions-explained-with-real-production-examples-176e</guid>
      <description>&lt;p&gt;Most explanations of these concepts stop at the definition. Scope gets a rule about hoisting. Closures get a counter function that increments a number nobody would ever ship. The event loop gets a diagram. &lt;code&gt;any&lt;/code&gt; versus &lt;code&gt;unknown&lt;/code&gt; gets a one-line rule. Generics get a toy example with numbers and strings that never shows up in a real codebase.&lt;/p&gt;

&lt;p&gt;That's enough to answer a question in an interview. It's not enough to actually recognize these patterns when they show up in your own code, which is the thing that matters once you're past the interview and actually shipping something.&lt;/p&gt;

&lt;p&gt;This is the first in a short series working through concepts that come up constantly in interviews, explained the way I'd actually explain them to someone on my team, with real examples from production code, not just the textbook version.&lt;/p&gt;

&lt;h2&gt;
  
  
  var, let, and const
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;var&lt;/code&gt;, &lt;code&gt;let&lt;/code&gt;, and &lt;code&gt;const&lt;/code&gt; are the three ways to declare a variable in JavaScript. &lt;code&gt;var&lt;/code&gt; is function-scoped and can be redeclared and reassigned freely. &lt;code&gt;let&lt;/code&gt; is block-scoped and can be reassigned but not redeclared in the same scope. &lt;code&gt;const&lt;/code&gt; is block-scoped and can't be reassigned after its initial value is set, though an object or array it points to can still be mutated internally.&lt;/p&gt;

&lt;p&gt;The rule everyone learns is "use &lt;code&gt;const&lt;/code&gt; by default, &lt;code&gt;let&lt;/code&gt; when a value changes, avoid &lt;code&gt;var&lt;/code&gt;." The part that's harder to see from that rule alone is what it looks like once real state is involved, not just a single reassignment.&lt;/p&gt;

&lt;p&gt;Take pagination on a leads or submissions list, the kind of thing &lt;a href="https://formgrid.dev" rel="noopener noreferrer"&gt;Formgrid.dev's&lt;/a&gt; dashboard actually does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&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;currentPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nextPage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;currentPage&lt;/span&gt;&lt;span class="o"&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;code&gt;pageSize&lt;/code&gt; never changes for the life of that list, so &lt;code&gt;const&lt;/code&gt; says so directly. &lt;code&gt;currentPage&lt;/code&gt; changes every time someone clicks through the pagination, so &lt;code&gt;let&lt;/code&gt; is the honest choice. The value of picking correctly here isn't stylistic; it's that anyone reading this code later can tell, without tracing every line, which values are safe to assume are fixed and which ones move. &lt;code&gt;var&lt;/code&gt; doesn't offer that signal at all, and it doesn't respect block scope the way &lt;code&gt;let&lt;/code&gt; and &lt;code&gt;const&lt;/code&gt; do, which is reason enough to leave it out of new code entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closures
&lt;/h2&gt;

&lt;p&gt;A closure is a function that keeps access to variables from the scope it was created in, even after that outer scope has already finished running.&lt;/p&gt;

&lt;p&gt;The clearest real-world use of a closure is a small API client factory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createApiClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&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="k"&gt;return&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="na"&gt;endpoint&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="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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;endpoint&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;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;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="nx"&gt;data&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="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createApiClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;api&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="s2"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;api&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="s2"&gt;/products&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;createApiClient()&lt;/code&gt; finishes running the moment it returns, but the object it hands back still remembers &lt;code&gt;baseUrl&lt;/code&gt;. That's the closure: &lt;code&gt;get&lt;/code&gt; and &lt;code&gt;post&lt;/code&gt; keep access to a variable from a scope that's technically already finished executing.&lt;/p&gt;

&lt;p&gt;The reason this matters beyond the definition is what it lets you do: create multiple independent clients from the same function, each remembering its own configuration.&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;productionApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createApiClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com&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;stagingApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createApiClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://staging.example.com&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;Each one is a separate closure over a different &lt;code&gt;baseUrl&lt;/code&gt;, with zero shared state between them. This is the same shape I'd reach for anywhere a function needs to remember configuration without a class: event handlers, callbacks, factories, or anything that needs private state without exposing it directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Promise.all, when requests don't depend on each other
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Promise.all()&lt;/code&gt; takes an array of promises and resolves once every one of them has resolved, returning their results in the same order. If any single promise in the array rejects, &lt;code&gt;Promise.all()&lt;/code&gt; rejects immediately, discarding the results of everything else.&lt;/p&gt;

&lt;p&gt;Formgrid's dashboard loads several independent things on page load: lead pipeline stats, recent submissions, integration status, and account usage. None of them depend on each other's results.&lt;/p&gt;

&lt;p&gt;The naive version waits for each one in sequence:&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;stats&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;fetchPipelineStats&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;submissions&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;fetchRecentSubmissions&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;integrations&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;fetchIntegrationStatus&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;usage&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;fetchAccountUsage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If each of those takes something like 500ms, 800ms, 600ms, and 300ms, waiting for them one at a time adds up to roughly 2.2 seconds before the dashboard can render anything.&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;submissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;usage&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;fetchPipelineStats&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;fetchRecentSubmissions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;fetchIntegrationStatus&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;fetchAccountUsage&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;Run concurrently instead; the total time is close to the slowest single request, around 800ms, not the sum of all four. &lt;code&gt;Promise.all&lt;/code&gt; is the right tool specifically when the requests are genuinely independent, and you need all of them to proceed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Promise.allSettled, when one failure shouldn't take down the rest
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Promise.allSettled()&lt;/code&gt; also takes an array of promises, but it waits for every one of them to finish regardless of outcome, and returns an array describing each result as either fulfilled, with its value, or rejected, with its reason. Nothing gets discarded because one promise failed.&lt;/p&gt;

&lt;p&gt;The catch with &lt;code&gt;Promise.all&lt;/code&gt; is that a single rejection fails the entire batch. That's fine when every result is required before anything can render. It's the wrong choice when one piece failing shouldn't block the others.&lt;/p&gt;

&lt;p&gt;Say the AI Smart Inbox category breakdown widget on that same dashboard has a bad moment and its request fails, while the lead stats, submissions, and integrations requests all succeed fine. With &lt;code&gt;Promise.all&lt;/code&gt;, that one failure would throw away three perfectly good results along with it.&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="nf"&gt;fetchPipelineStats&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;fetchRecentSubmissions&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;fetchIntegrationStatus&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;fetchAiInboxBreakdown&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;result&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fulfilled&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;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="s2"&gt;Loaded:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&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;Now the dashboard can render the three sections that succeeded and show a quiet error state only where the AI breakdown actually failed. &lt;code&gt;Promise.allSettled&lt;/code&gt; is the right call whenever a partial result is genuinely more useful than an all-or-nothing failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The event loop isn't just a diagram; it's why setTimeout is deliberately used in real apps
&lt;/h2&gt;

&lt;p&gt;The event loop is the mechanism that lets JavaScript, a language that runs on a single thread, handle asynchronous work without blocking. It continuously checks whether the call stack is empty, and once it is, pulls the next pending callback onto the stack to run, microtasks first, then the task queue where things like &lt;code&gt;setTimeout&lt;/code&gt; callbacks wait.&lt;/p&gt;

&lt;p&gt;Start with a concept that trips people up: fire and forget.&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;sendEmail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Done&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 &lt;code&gt;sendEmail()&lt;/code&gt; starts an asynchronous operation and you don't &lt;code&gt;await&lt;/code&gt; it, your code doesn't wait around for it to finish. It starts the operation, moves on immediately, and the email finishes sometime later, off to the side. Compare that to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Done&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;which waits for the email to fully send before moving on. The fire-and-forget version is genuinely useful in production; it's the same pattern behind background jobs that shouldn't block a response, but it comes with a real catch: if that unawaited promise rejects, you get an unhandled rejection nobody's watching for. Fire and forget on purpose, with a &lt;code&gt;.catch()&lt;/code&gt; attached, is a deliberate choice. Fire and forget by accident, because you forgot the &lt;code&gt;await&lt;/code&gt;, is a bug waiting to surface in production logs.&lt;/p&gt;

&lt;p&gt;That same asynchronous mental model is exactly what makes &lt;code&gt;setTimeout&lt;/code&gt; more than an interview trivia question. It's genuinely useful, constantly, in real applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Showing a temporary success message.&lt;/strong&gt; Say a form submission on Formgrid succeeds and you want to show "Form created successfully" for three seconds:&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;showSuccessMessage&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;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;#success&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;setTimeout&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;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&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="mi"&gt;3000&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;JavaScript doesn't freeze for three seconds waiting for this. It shows the message, keeps doing whatever else it needs to do, and the event loop picks the hide callback back up once the timer's done and the call stack is free.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debouncing a search box.&lt;/strong&gt; This is the one that shows up in real products constantly. If a user types "formgrid" one character at a time, you don't want to fire an API request on every keystroke:&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;let&lt;/span&gt; &lt;span class="nx"&gt;timeout&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;searchUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users?search=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;500&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;Every keystroke cancels the previous timer and starts a new one. Only once the user actually stops typing for 500 milliseconds does the request go out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrying a failed request after a delay.&lt;/strong&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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="s2"&gt;/api/data&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&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="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Request failed&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="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;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;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Retrying...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&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="nf"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;2000&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;code&gt;setTimeout&lt;/code&gt; here is scheduling future work deliberately, giving a flaky dependency a couple of seconds to recover before trying again, rather than hammering it immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaking up expensive work so the page doesn't freeze.&lt;/strong&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;function&lt;/span&gt; &lt;span class="nf"&gt;processChunk&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// process some work&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;moreWork&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processChunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;processChunk&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;0&lt;/code&gt; here doesn't mean "run immediately." It means "let the current work finish and give the event loop a chance to handle anything else that's queued before running this next chunk."&lt;/p&gt;

&lt;h3&gt;
  
  
  Microtasks jump the queue ahead of setTimeout, even at zero delay
&lt;/h3&gt;

&lt;p&gt;Here's the example that actually separates people who've memorized the event loop diagram from people who've internalized it. Imagine a user clicks "Generate Report":&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="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="s2"&gt;Starting report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;setTimeout&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;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="s2"&gt;Report notification&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Update UI&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;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="s2"&gt;Request submitted&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 output is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Starting report
Request submitted
Update UI
Report notification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not what most people guess on first read. The synchronous lines run first, in order: &lt;code&gt;Starting report&lt;/code&gt;, then &lt;code&gt;Request submitted&lt;/code&gt;. Then, before the &lt;code&gt;setTimeout&lt;/code&gt; callback gets anywhere near the call stack, the promise's &lt;code&gt;.then()&lt;/code&gt; callback runs, because promise callbacks go into the microtask queue, and the microtask queue is fully drained before the event loop even looks at the task queue where &lt;code&gt;setTimeout&lt;/code&gt; callbacks live. Only after that does &lt;code&gt;Report notification&lt;/code&gt; finally run.&lt;/p&gt;

&lt;p&gt;This is exactly why a &lt;code&gt;setTimeout(fn, 0)&lt;/code&gt; never means "run this immediately." It means "run this after the current synchronous code finishes, and after every pending microtask has already run." Debugging unexpected execution order in a React app, a Node service, or anything mixing promises with timers almost always comes back to this exact ordering.&lt;/p&gt;

&lt;p&gt;If you're asked about this in an interview, debouncing is the strongest real-world example for &lt;code&gt;setTimeout&lt;/code&gt; specifically, and this report generation example is the strongest one for explaining why microtasks and macrotasks aren't the same queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  any versus unknown in TypeScript
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;any&lt;/code&gt; and &lt;code&gt;unknown&lt;/code&gt; are both TypeScript types that can hold a value of any shape. The difference is what TypeScript does with that value afterward: &lt;code&gt;any&lt;/code&gt; turns off type checking for it entirely, while &lt;code&gt;unknown&lt;/code&gt; requires you to narrow it, prove what it actually is, before you're allowed to use it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;any&lt;/code&gt; tells TypeScript: don't check this value, I know what I'm doing.&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;let&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;foo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;baz&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TypeScript won't say a word about this. JavaScript will still crash at runtime because a string doesn't have &lt;code&gt;.foo.bar.baz()&lt;/code&gt;, TypeScript just isn't watching anymore once something is typed &lt;code&gt;any&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;unknown&lt;/code&gt; takes the opposite position: I don't know what this is yet, so prove it before you use it.&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;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="s2"&gt;/api/user&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;data&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;=&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;You don't actually know that the server returned what you expected, just because the request succeeded. Typing this &lt;code&gt;any&lt;/code&gt; tells TypeScript to trust it completely. Typing it &lt;code&gt;unknown&lt;/code&gt; forces you to check before you can touch it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;Once narrowed like this, TypeScript trusts you again, but only inside that branch, and only as far as what you actually checked for.&lt;/p&gt;

&lt;p&gt;This matters most at boundaries you don't control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your TypeScript application
          ↓
     External API
          ↓
        JSON
          ↓
       unknown
          ↓
   Validate or narrow
          ↓
     Trusted data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;API responses, user input, third-party libraries, anything read from a database or a file, none of it should be trusted at the type level just because it compiled. &lt;code&gt;unknown&lt;/code&gt; forces the check. &lt;code&gt;any&lt;/code&gt; lets you skip it and find out at runtime instead, usually in production, usually at a worse time than during development.&lt;/p&gt;

&lt;p&gt;The short version worth remembering: &lt;code&gt;any&lt;/code&gt; says trust me, TypeScript stops protecting you. &lt;code&gt;unknown&lt;/code&gt; says I don't know yet, TypeScript makes you prove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generics in TypeScript
&lt;/h2&gt;

&lt;p&gt;Generics let you write a function, class, or interface that works with more than one type while still keeping full type information, using a placeholder type, conventionally written &lt;code&gt;T&lt;/code&gt;, that gets filled in with a real type at the point where the code is actually used.&lt;/p&gt;

&lt;p&gt;The idea behind generics is simpler than the syntax makes it look: write code once, and let it preserve whatever specific type it's handed, instead of losing that information.&lt;/p&gt;

&lt;p&gt;Without generics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&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;result&lt;/code&gt; is typed &lt;code&gt;any&lt;/code&gt; here. The function works, but you've thrown away the fact that you started with an array of numbers.&lt;/p&gt;

&lt;p&gt;With a generic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;T&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;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// T = number&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Allen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;John&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// T = string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; is a placeholder: give me a type, and I'll use that exact type everywhere I've written &lt;code&gt;T&lt;/code&gt;. TypeScript figures out what &lt;code&gt;T&lt;/code&gt; is from what you actually pass in, and carries that information through to the return type.&lt;/p&gt;

&lt;p&gt;The most common real-world shape of this is an API response wrapper:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ApiResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;success&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="nl"&gt;message&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ApiResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;success&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;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Allen&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;p&gt;&lt;code&gt;ApiResponse&amp;lt;User&amp;gt;&lt;/code&gt;, &lt;code&gt;ApiResponse&amp;lt;Product&amp;gt;&lt;/code&gt;, and &lt;code&gt;ApiResponse&amp;lt;Order&amp;gt;&lt;/code&gt; all reuse the same interface, fully typed, instead of writing three nearly identical ones.&lt;/p&gt;

&lt;p&gt;The same idea shows up just as often on the frontend, in a reusable table or list component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DataTable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DataTable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same component, same rendering logic, but each usage stays fully typed to the data it's actually given. This is why generics show up everywhere in API clients, reusable components, and utility functions in general; they're the mechanism that keeps one piece of code reusable without becoming untyped in the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this connects
&lt;/h2&gt;

&lt;p&gt;None of these ideas are actually separate from each other. &lt;code&gt;const&lt;/code&gt; and &lt;code&gt;let&lt;/code&gt; are about being explicit regarding what's allowed to change. Closures are about a function remembering the scope it was created in, even after that scope has technically finished running. &lt;code&gt;Promise.all&lt;/code&gt; and &lt;code&gt;Promise.allSettled&lt;/code&gt; are both about running independent work concurrently; they just disagree on what to do when one piece fails. The event loop underneath all of it decides the actual order any of this code runs in: synchronous first, then every microtask, then the task queue. &lt;code&gt;unknown&lt;/code&gt; exists because you sometimes genuinely don't know a type yet and need to prove it before using it. Generics exist because you often do know the type at the call site and don't want to throw that information away by writing something that treats everything as &lt;code&gt;any&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the pattern worth carrying into the next post in this series: data structures, algorithms, and testing as they actually show up in production, not as isolated interview trivia.&lt;/p&gt;

&lt;p&gt;If there's a concept here you'd explain differently, or a production example you think makes any of this land better, I'd like to hear it. Reach me at &lt;a href="mailto:allen@formgrid.dev"&gt;allen@formgrid.dev&lt;/a&gt;.&lt;/p&gt;




</description>
      <category>javascript</category>
      <category>interview</category>
      <category>typescript</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>NestJS for Express Developers: A Practical Guide, With Prisma ORM</title>
      <dc:creator>Allen Jones</dc:creator>
      <pubDate>Sat, 05 Sep 2026 22:37:41 +0000</pubDate>
      <link>https://dev.to/allenarduino/nestjs-for-express-developers-a-practical-guide-with-prisma-orm-537k</link>
      <guid>https://dev.to/allenarduino/nestjs-for-express-developers-a-practical-guide-with-prisma-orm-537k</guid>
      <description>&lt;p&gt;I've spent years building backends in Express. NestJS looks different on the surface, but here's the thing nobody tells you clearly enough: if you already know Express, you don't need to learn backend development again. You need to learn how NestJS organizes the things you already know how to do&lt;/p&gt;

&lt;p&gt;At first, NestJS looks like a lot more ceremony for the same result. A simple route handler turns into a controller class, a service class, decorators everywhere, and a module wiring them together. But once you understand why each piece exists, none of it feels like ceremony anymore; it feels like structure you were probably improvising by hand in every Express project anyway.&lt;/p&gt;

&lt;p&gt;This post is the explanation I wish I'd had on day one, mapped directly against Express, using Prisma instead of TypeORM, since that's the ORM I already use in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-sentence version
&lt;/h2&gt;

&lt;p&gt;Think of NestJS as:&lt;/p&gt;

&lt;p&gt;Express, plus TypeScript, plus dependency injection, plus modules, plus decorators, plus enforced architecture.&lt;/p&gt;

&lt;p&gt;In Express, you might write:&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;userService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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;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;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same idea in NestJS:&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="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;users&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;UsersController&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;usersService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersService&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;Post&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;create&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateUserDto&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More characters, same idea. The rest of this post is about why that extra structure exists and when it earns its keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The project structure
&lt;/h2&gt;

&lt;p&gt;Scaffold a new project, and you'll get something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nest-api/
├── src/
│   ├── app.controller.ts
│   ├── app.controller.spec.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test/
├── package.json
├── tsconfig.json
└── nest-cli.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four files matter to start: &lt;code&gt;main.ts&lt;/code&gt;, &lt;code&gt;app.module.ts&lt;/code&gt;, &lt;code&gt;app.controller.ts&lt;/code&gt;, &lt;code&gt;app.service.ts&lt;/code&gt;. Everything else is scaffolding you'll grow into.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;main.ts&lt;/code&gt; is your entry point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NestFactory&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;AppModule&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;./app.module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bootstrap&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;app&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;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppModule&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;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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&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="nf"&gt;bootstrap&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're coming from Express, this is your &lt;code&gt;const app = express(); app.listen(3000);&lt;/code&gt;, just bootstrapped from a root module instead of a bare instance. &lt;code&gt;NestFactory.create(AppModule)&lt;/code&gt; is Nest reading your entire application's shape from one root module and building it from there.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app.module.ts&lt;/code&gt; is the first genuinely unfamiliar concept:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;AppService&lt;/span&gt;&lt;span class="p"&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;AppModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hold off on fully understanding modules yet; they'll make more sense once you've seen a controller and a service in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                HTTP Request
                     │
                     ▼
              ┌─────────────┐
              │   Module    │
              └──────┬──────┘
                     │
                     ▼
              ┌─────────────┐
              │ Controller  │
              └──────┬──────┘
                     │
                     ▼
              ┌─────────────┐
              │   Service   │
              └──────┬──────┘
                     │
                     ▼
              ┌─────────────┐
              │   Prisma    │
              └──────┬──────┘
                     │
                     ▼
              ┌─────────────┐
              │ PostgreSQL  │
              └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pieces worth knowing, roughly in the order you'll actually meet them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Module&lt;/strong&gt;: organizes a feature&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controller&lt;/strong&gt;: handles HTTP requests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service&lt;/strong&gt;: contains business logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DTO&lt;/strong&gt;: describes and validates incoming data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guard&lt;/strong&gt;: controls authentication and authorization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipe&lt;/strong&gt;: validates or transforms input&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interceptor&lt;/strong&gt;: wraps or transforms request and response behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Middleware&lt;/strong&gt;: the same concept as Express middleware&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency injection&lt;/strong&gt;: Nest supplies each class the dependencies it needs, instead of you constructing them by hand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't try to hold all of these at once. Controllers and services get you most of the way there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Express knowledge maps directly
&lt;/h2&gt;

&lt;p&gt;This is genuinely the fastest way to internalize it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Express&lt;/th&gt;
&lt;th&gt;NestJS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;app.get()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Get()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;app.post()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Post()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;req.params&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Param()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;req.body&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Body()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;req.query&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@Query()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;res.json()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;return&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Middleware&lt;/td&gt;
&lt;td&gt;Middleware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Router&lt;/td&gt;
&lt;td&gt;Controller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service modules&lt;/td&gt;
&lt;td&gt;Providers and services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual dependency passing&lt;/td&gt;
&lt;td&gt;Dependency injection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Validation middleware&lt;/td&gt;
&lt;td&gt;Pipes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth middleware&lt;/td&gt;
&lt;td&gt;Guards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error middleware&lt;/td&gt;
&lt;td&gt;Exception filters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Router organization&lt;/td&gt;
&lt;td&gt;Modules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Express:&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;router&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;/users/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&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;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NestJS:&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="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;users&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;UsersController&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;usersService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersService&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;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;:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;id&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="k"&gt;return&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;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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 NestJS version is saying, out loud, in the type system: this class handles &lt;code&gt;/users&lt;/code&gt;, and this method handles &lt;code&gt;GET /users/:id&lt;/code&gt;. That's the whole trade the extra syntax is making: implicit routing conventions become explicit, checkable structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Controllers
&lt;/h2&gt;

&lt;p&gt;This is where you'll feel most at home immediately.&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="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;users&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;UsersController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;findAll&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="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;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;:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;id&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&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;Post&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;create&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;body&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;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;id&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&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;That's the equivalent of &lt;code&gt;router.get('/users', ...)&lt;/code&gt;, &lt;code&gt;router.get('/users/:id', ...)&lt;/code&gt;, &lt;code&gt;router.post('/users', ...)&lt;/code&gt;, and &lt;code&gt;router.delete('/users/:id', ...)&lt;/code&gt;. Nothing conceptually new here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Services, where Nest starts diverging
&lt;/h2&gt;

&lt;p&gt;In Express you might write:&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;userService&lt;/span&gt; &lt;span class="o"&gt;=&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;findAll&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;();&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;findOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In NestJS:&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="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;UsersService&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;findAll&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="p"&gt;[];&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;findOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&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;code&gt;@Injectable()&lt;/code&gt; is the important part. It tells Nest that this class can be managed by Nest's dependency injection system. Your controller then does this:&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="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;users&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;UsersController&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;usersService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersService&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;Notice what's missing: &lt;code&gt;const usersService = new UsersService()&lt;/code&gt; never appears anywhere. Nest creates it and hands it to the controller. That's dependency injection, and it's worth its own section, because it's the concept that actually changes how you think about structuring an app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modules
&lt;/h2&gt;

&lt;p&gt;A module groups related functionality into a feature boundary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── users/
│   ├── users.controller.ts
│   ├── users.service.ts
│   ├── users.module.ts
│   └── dto/
├── auth/
│   ├── auth.controller.ts
│   ├── auth.service.ts
│   └── auth.module.ts
└── app.module.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;UsersController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;UsersService&lt;/span&gt;&lt;span class="p"&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;UsersModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;imports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;UsersModule&lt;/span&gt;&lt;span class="p"&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;AppModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thinking about this against &lt;a href="https://formgrid.dev" rel="noopener noreferrer"&gt;Formgrid,s&lt;/a&gt; actual feature set is a useful exercise, even without rebuilding it in Nest: auth, users, forms, leads, integrations, webhooks, billing, and notifications would each map cleanly to its own module. That boundary is exactly what makes Nest attractive once a SaaS codebase grows past the size where "everything imports everything" stops being manageable in a flat Express app.&lt;/p&gt;

&lt;h2&gt;
  
  
  DTOs and validation
&lt;/h2&gt;

&lt;p&gt;Instead of the shapeless &lt;code&gt;@Body() body: any&lt;/code&gt;, you define what's actually expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateUserDto&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;password&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combined with &lt;code&gt;class-validator&lt;/code&gt;, this becomes real, enforced validation instead of a type hint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IsEmail&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="nx"&gt;MinLength&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;CreateUserDto&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="nx"&gt;name&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;IsEmail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;email&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;MinLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;Nest validates incoming requests against this automatically, which replaces a fair amount of hand-rolled validation middleware you'd otherwise be writing and maintaining yourself in Express.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pipes
&lt;/h2&gt;

&lt;p&gt;Pipes transform and validate input before it reaches your handler.&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&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;:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ParseIntPipe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;id&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;span class="k"&gt;return&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;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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;A URL parameter arrives as the string &lt;code&gt;"123"&lt;/code&gt;. The pipe converts it to the number &lt;code&gt;123&lt;/code&gt; before your method ever sees it, and throws an appropriate exception automatically if the value isn't valid in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exceptions
&lt;/h2&gt;

&lt;p&gt;Express:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;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;404&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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User not found&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;NestJS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NotFoundException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User not found&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&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;user&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;NotFoundException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User not found&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;user&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;Nest turns that thrown exception into the correct HTTP response for you. You also get &lt;code&gt;BadRequestException&lt;/code&gt;, &lt;code&gt;UnauthorizedException&lt;/code&gt;, &lt;code&gt;ForbiddenException&lt;/code&gt;, &lt;code&gt;ConflictException&lt;/code&gt;, and &lt;code&gt;InternalServerErrorException&lt;/code&gt; out of the box, all consistent, all readable at the point they're thrown instead of buried in a response call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guards
&lt;/h2&gt;

&lt;p&gt;You'll meet these implementing authentication:&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="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;AuthGuard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&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;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;getProfile&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getProfile&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;A request either fails the guard and never reaches the controller, or passes it and proceeds normally. That's generally cleaner than scattering an authentication check inside every individual Express route handler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interceptors
&lt;/h2&gt;

&lt;p&gt;These can wait until you actually need them. They wrap around a controller's execution, useful for logging, response transformation, timing, or caching:&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="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;LoggingInterceptor&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;NestInterceptor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;intercept&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;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CallHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Request started&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;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&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;
  
  
  Middleware
&lt;/h2&gt;

&lt;p&gt;Good news here: you already know this one.&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="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;LoggerMiddleware&lt;/span&gt; &lt;span class="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;NestMiddleware&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;req&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;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="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextFunction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&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;url&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;p&gt;This transfers from Express almost without translation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring up Prisma instead of TypeORM
&lt;/h2&gt;

&lt;p&gt;Most NestJS material defaults to TypeORM, but if you're already running Prisma, there's no reason to switch ORMs just to learn a framework. A thin &lt;code&gt;PrismaService&lt;/code&gt; is all it takes to bring Prisma into Nest's dependency injection system:&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="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;PrismaService&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&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;onModuleInit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&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="nf"&gt;$connect&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;Then your service depends on it like anything else:&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="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;UsersService&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;prisma&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PrismaService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="k"&gt;return&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;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the module wires both together:&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;UsersController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;UsersService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PrismaService&lt;/span&gt;&lt;span class="p"&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;UsersModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nest handles the application's structure and dependency graph. Prisma handles the database layer. Neither one needs to know much about the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding dependency injection: why @Injectable() actually exists
&lt;/h2&gt;

&lt;p&gt;This is the concept that took me the longest to actually feel, rather than just memorize, so it's worth its own deep section.&lt;/p&gt;

&lt;p&gt;Here's the honest question: Express works completely fine without anything like &lt;code&gt;@Injectable()&lt;/code&gt;. So why does Nest need it?&lt;/p&gt;

&lt;h3&gt;
  
  
  Express: you are the dependency injection system
&lt;/h3&gt;

&lt;p&gt;Suppose you have this:&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;class&lt;/span&gt; &lt;span class="nc"&gt;EmailService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sending email...&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;class&lt;/span&gt; &lt;span class="nc"&gt;UsersService&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;emailService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EmailService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&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;emailService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendEmail&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;class&lt;/span&gt; &lt;span class="nc"&gt;UsersController&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;usersService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;createUser&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createUser&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;In Express, you wire this by hand:&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;emailService&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;EmailService&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;usersService&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;UsersService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailService&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;usersController&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;UsersController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;usersService&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;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/users&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="nx"&gt;usersController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createUser&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;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Created&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;p&gt;No &lt;code&gt;@Injectable()&lt;/code&gt; anywhere, because none is needed. You are the dependency injection system: you decide what gets created, in what order, and what gets passed into what. TypeScript's constructors already support this pattern natively; nothing about it is Nest-specific.&lt;/p&gt;

&lt;h3&gt;
  
  
  What NestJS actually adds
&lt;/h3&gt;

&lt;p&gt;NestJS's pitch is essentially: why keep wiring this by hand? So instead:&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="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;EmailService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sending email...&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="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;UsersService&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;emailService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EmailService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&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;emailService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendEmail&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="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;users&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;UsersController&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;usersService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersService&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;Post&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nf"&gt;createUser&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usersService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createUser&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;controllers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;UsersController&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;UsersService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EmailService&lt;/span&gt;&lt;span class="p"&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;UsersModule&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now nothing gets manually constructed. Nest resolves the graph itself: the controller needs &lt;code&gt;UsersService&lt;/code&gt;, so Nest finds or creates one; that service needs &lt;code&gt;EmailService&lt;/code&gt;, so Nest finds or creates that too. &lt;code&gt;@Injectable()&lt;/code&gt; is the marker that tells Nest's container "this class is allowed to participate in that resolution," nothing more mystical than that. It isn't a requirement of TypeScript constructors; it's metadata Nest specifically needs to do its job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this matters more as the app grows
&lt;/h3&gt;

&lt;p&gt;At three classes, manual wiring in Express is completely fine, arguably even clearer, since you can see the whole graph in one place. The value of letting Nest do it shows up once the graph gets genuinely large.&lt;/p&gt;

&lt;p&gt;Picture something the shape of Formgrid's dependency tree, hypothetically, if it were structured this way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UsersController
      │
      ├── UsersService
      │        │
      │        ├── UsersRepository → DatabaseService
      │        ├── EmailService → ResendService
      │        └── PaymentService → PaddleService
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wired by hand in Express, that's:&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;database&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;DatabaseService&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;repository&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;UsersRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;database&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;resend&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;ResendService&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;email&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;EmailService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resend&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;paddle&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;PaddleService&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;payment&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;PaymentService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paddle&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;users&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;UsersService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payment&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;controller&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;UsersController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's manageable once. It gets genuinely unpleasant at real scale, a hundred services deep, where any one of them needing an additional dependency means updating every place that constructs it, in the right order, every time.&lt;/p&gt;

&lt;p&gt;In Nest, you just declare what each class needs:&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="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;repository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsersRepository&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;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EmailService&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;payment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaymentService&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;And the container resolves and constructs the entire tree for you, in the correct order, every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual insight
&lt;/h3&gt;

&lt;p&gt;Constructors aren't the difference between Express and NestJS. Both let you declare &lt;code&gt;constructor(private emailService: EmailService)&lt;/code&gt; and both will happily accept it; that part is plain TypeScript, nothing framework-specific about it either way.&lt;/p&gt;

&lt;p&gt;The difference is who is responsible for calling those constructors and supplying what they need. In Express, that's you, by hand, every time. In NestJS, that's the container, once you've told it, via &lt;code&gt;@Injectable()&lt;/code&gt; and a module's &lt;code&gt;providers&lt;/code&gt; array, which classes it's allowed to manage.&lt;/p&gt;

&lt;p&gt;That's the whole idea &lt;code&gt;@Injectable()&lt;/code&gt; exists to express. Everything else in Nest, controllers, modules, guards, pipes, is really just structure built on top of that one core mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;If you already know Express well, none of this is new backend knowledge; it's a new way of organizing backend knowledge you already have. Controllers are routers with better names. Services are the business logic you were already separating, formalized. Modules are the feature folders you were probably already creating by convention, now enforced by the framework instead of by team discipline.&lt;/p&gt;

&lt;p&gt;The place NestJS earns its extra ceremony is scale and team size; a large, long-lived API with many contributors benefits from a framework insisting on structure. A small API, a quick internal tool, or a solo project where you already have the discipline to keep things organized may not need it at all. Express isn't wrong for those; it's just a different tradeoff.&lt;/p&gt;

&lt;p&gt;If you've made the same jump from Express to NestJS, or gone the other way, I'd like to hear what actually clicked for you. Reach me at &lt;a href="mailto:allen@formgrid.dev"&gt;allen@formgrid.dev&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Allen, a full-stack TypeScript engineer and the founder of &lt;a href="https://formgrid.dev" rel="noopener noreferrer"&gt;Formgrid&lt;/a&gt; and &lt;a href="https://sheetrocket.com" rel="noopener noreferrer"&gt;SheetRocket&lt;/a&gt;. I write about real production engineering from products that people actually pay for. More at &lt;a href="https://jonesstack.com" rel="noopener noreferrer"&gt;jonesstack.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>express</category>
      <category>node</category>
      <category>backend</category>
    </item>
    <item>
      <title>I Built a Next.js Auth Boilerplate That Saves Developers Two to Four Weeks of Setup Time</title>
      <dc:creator>Allen Jones</dc:creator>
      <pubDate>Tue, 01 Sep 2026 20:11:29 +0000</pubDate>
      <link>https://dev.to/allenarduino/i-built-a-nextjs-auth-boilerplate-that-saves-developers-two-to-four-weeks-of-setup-time-2m3j</link>
      <guid>https://dev.to/allenarduino/i-built-a-nextjs-auth-boilerplate-that-saves-developers-two-to-four-weeks-of-setup-time-2m3j</guid>
      <description>&lt;p&gt;Every time I started a new project, whether it was Formgrid, Sheetrocket, or a client build, the first week or two disappeared into authentication. Email verification tokens. Password reset flows. NextAuth configuration. A Prisma schema for users and sessions. Setting up the Google OAuth consent screen. Middleware for protected routes.&lt;/p&gt;

&lt;p&gt;None of that is intellectually interesting. All of it is necessary. And it has to be done correctly, because a mistake in auth isn't a bug; it's a security vulnerability with someone's real account attached to it.&lt;/p&gt;

&lt;p&gt;After doing this properly from scratch for the fifth time, I stopped. I built it once, the right way, open-sourced it, and made it the starting point for every project after this one.&lt;/p&gt;

&lt;p&gt;The result is a production-ready authentication starter kit that gets a new Next.js project from zero to working auth in minutes instead of weeks.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Email and password authentication with Zod validation.&lt;/strong&gt; Not just a signup form, proper server-side validation that catches weak passwords, duplicate emails, and malformed input before any of it reaches the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email verification.&lt;/strong&gt; New users can't access protected routes until they verify their email, and the verification token expires after 24 hours. Without this, any email address, including ones the signer doesn't actually own, can be used to create an account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Password reset with secure tokens.&lt;/strong&gt; The reset token expires after one hour. Without an expiry, a leaked reset link is a permanent vulnerability sitting in someone's inbox forever. The token itself is hashed before it's stored, so even a full database breach doesn't hand over usable reset tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google OAuth with NextAuth.js v5.&lt;/strong&gt; Handles the OAuth flow, stores the account link in the database, and merges correctly with an existing email account if the user originally signed up with a password. Getting that merge logic wrong is one of the most common auth bugs I've seen in real production apps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protected routes with Next.js middleware.&lt;/strong&gt; Everything under the dashboard directory is protected automatically. The middleware checks the session and redirects unauthenticated users to login before the page component ever renders; no manual auth check copied into every page.&lt;/p&gt;

&lt;p&gt;** Role-based access.** The User model has a role field, defaulting to USER, with support for ADMIN and any other role you want to add. Middleware can gate admin-only routes without touching every protected page individually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A responsive dashboard layout in Tailwind.&lt;/strong&gt; A clean starting point for the authenticated UI, so you can start building your actual feature immediately after cloning instead of also designing a dashboard shell first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack, and the honest tradeoffs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Next.js 15 with the App Router.&lt;/strong&gt; The current standard for React apps with server components and server actions. The App Router changes how auth is wired up compared to the Pages Router, and NextAuth v5 is built specifically for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prisma with MySQL.&lt;/strong&gt; Type safe queries with automatic migrations. MySQL for wide hosting compatibility, easily swapped for PostgreSQL by changing one line in the Prisma schema and the DATABASE_URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NextAuth.js v5.&lt;/strong&gt; The current major version, with breaking changes from v4. The configuration pattern changed significantly, and most tutorials still floating around online use the old v4 pattern. This boilerplate uses the correct v5 approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resend for transactional email.&lt;/strong&gt; The most developer-friendly email API I've used. It's what Formgrid runs in production for verification and notification emails. The templates in the boilerplate are plain text and easy to reskin with your own branding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tailwind CSS.&lt;/strong&gt; Utility-first styling that ships zero unused CSS to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three decisions worth explaining, because most tutorials get them wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JWT sessions over database sessions
&lt;/h3&gt;

&lt;p&gt;NextAuth supports two session strategies. Database sessions store a session record per login and validate it against the database on every request. JWT sessions encode the session data into a signed token and validate it cryptographically, no database query required.&lt;/p&gt;

&lt;p&gt;Database sessions are more revocable; you can kill a session instantly by deleting its record. JWT sessions are faster, since there's no query on every authenticated request, but you can't invalidate one without extra infrastructure sitting on top. The boilerplate uses JWT for simplicity and speed. If your product needs immediate session revocation, for example, kicking someone out the moment their subscription lapses, that's the tradeoff worth knowing before you commit to this pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hashing verification and reset tokens
&lt;/h3&gt;

&lt;p&gt;Verification tokens and password reset tokens are stored hashed in the database, never in plain text. That means even if someone reads the raw database, they can't use the stored tokens to verify accounts or reset passwords themselves. The plain text token only ever exists in the email link, and it's compared against the hash at submission time. Same principle as password hashing, applied to a temporary token instead of a permanent credential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auth checks belong in middleware, not in page components
&lt;/h3&gt;

&lt;p&gt;Next.js middleware runs on the Edge runtime before the page ever renders. Putting the auth check there means a protected page is never rendered for an unauthenticated visitor, not even partially. Putting the check inside the page component instead means the page starts rendering and then redirects, which produces a flash of content the user was never supposed to see. Middleware is the correct place for the gate, not a convenience, but a correctness requirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone git@github.com:allenarduino/nextjs-prisma-auth-boilerplate.git

&lt;span class="nb"&gt;cd &lt;/span&gt;nextjs-prisma-auth-boilerplate

npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy &lt;code&gt;.env.example&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt; and fill in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DATABASE_URL&lt;/code&gt;, your MySQL connection string&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NEXTAUTH_SECRET&lt;/code&gt;, a random 32-character string; generate one with &lt;code&gt;openssl rand -base64 32&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NEXTAUTH_URL&lt;/code&gt;, your app's base URL&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GOOGLE_CLIENT_ID&lt;/code&gt; and &lt;code&gt;GOOGLE_CLIENT_SECRET&lt;/code&gt;, from the Google Cloud Console after creating an OAuth 2.0 credential&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RESEND_API_KEY&lt;/code&gt;, from resend.com after creating an account and verifying a sending domain&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EMAIL_FROM&lt;/code&gt;, the sender address your verification and reset emails go out from, for example &lt;code&gt;Formgrid &amp;lt;no-reply@yourdomain.com&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx prisma generate

npx prisma db push

npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole setup. From clone to a working signup, verification, login, and password reset flow, in minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to build on top of it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A SaaS product.&lt;/strong&gt; The auth layer is done. Add your subscription logic, your core features, and your billing integration. The User model's role field is already there to extend into a subscription tier if you need one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A client project.&lt;/strong&gt; Clone it, configure the environment variables for the client's domain and database, restyle the Tailwind to match their brand, and deploy. The auth is production-ready from day one, not a prototype you'll need to harden later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A side project.&lt;/strong&gt; Stop spending the first two weeks on auth and start building the part that's actually yours. The boilerplate handles the foundation so you can get to the feature that makes your project worth building in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  How this connects to Formgrid
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://formgrid.dev" rel="noopener noreferrer"&gt;Formgrid&lt;/a&gt; runs a similar auth pattern, built in Express and Prisma instead of NextAuth, but the same principles apply underneath: email verification, hashed tokens, JWT sessions, Google OAuth, protected routes. Building this boilerplate was really just distilling those same patterns, ones already proven in a product real customers pay for, into a shareable starting point for the Next.js ecosystem specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands right now
&lt;/h2&gt;

&lt;p&gt;The repo has picked up 17 GitHub stars and 9 forks in a short amount of time, which is a decent early signal that the pain point is real and not just mine. You can browse the code at &lt;a href="https://github.com/allenarduino/nextjs-prisma-auth-boilerplate" rel="noopener noreferrer"&gt;github.com/allenarduino/nextjs-prisma-auth-boilerplate&lt;/a&gt; or try the live demo at &lt;a href="https://nextjs-prisma-auth-boilerplate.vercel.app" rel="noopener noreferrer"&gt;nextjs-prisma-auth-boilerplate.vercel.app&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Authentication is never the interesting part of a product. It's the necessary foundation everything else gets built on. Getting it right matters because getting it wrong means real security vulnerabilities attached to real users. Getting it done quickly matters because every week spent on auth is a week not spent on the feature that actually makes your product worth using.&lt;/p&gt;

&lt;p&gt;Clone the repo, configure the environment, and go build the interesting part.&lt;/p&gt;

&lt;p&gt;If you use it on a project, or find a way to improve it, I'd genuinely like to hear about it. The repo is open for contributions and issues at &lt;a href="https://github.com/allenarduino/nextjs-prisma-auth-boilerplate" rel="noopener noreferrer"&gt;github.com/allenarduino/nextjs-prisma-auth-boilerplate&lt;/a&gt;. Reach me at &lt;a href="mailto:allen@formgrid.dev"&gt;allen@formgrid.dev&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Allen, a full-stack TypeScript engineer and the founder of &lt;a href="https://formgrid.dev" rel="noopener noreferrer"&gt;Formgrid&lt;/a&gt; and &lt;a href="https://sheetrocket.com" rel="noopener noreferrer"&gt;Sheetrocket&lt;/a&gt;. I write about real production engineering from products that people actually pay for. More at &lt;a href="https://jonesstack.com" rel="noopener noreferrer"&gt;jonesstack.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>nextjs</category>
      <category>nextauth</category>
    </item>
    <item>
      <title>I Deployed a Backend API on Vercel and Here Is What Broke</title>
      <dc:creator>Allen Jones</dc:creator>
      <pubDate>Mon, 31 Aug 2026 10:19:12 +0000</pubDate>
      <link>https://dev.to/allenarduino/i-deployed-a-backend-api-on-vercel-and-here-is-what-broke-29e5</link>
      <guid>https://dev.to/allenarduino/i-deployed-a-backend-api-on-vercel-and-here-is-what-broke-29e5</guid>
      <description>&lt;p&gt;The backend was for one of my&lt;br&gt;
projects, &lt;a href="https://sheetrocket.com" rel="noopener noreferrer"&gt;Sheetrocket&lt;/a&gt;, a tool that turns any Google Sheet into a REST API or a set of no-code embeddable widgets. &lt;/p&gt;

&lt;p&gt;The idea is simple: developers connect&lt;br&gt;
a Google Sheet and get a REST endpoint that returns their&lt;br&gt;
data as JSON, while non-technical users display that same data&lt;br&gt;
on any website as cards, catalogues, or tables without writing a single line of code. Update the sheet, and everything stays in sync automatically.&lt;/p&gt;

&lt;p&gt;The problem is that Google Sheets enforces rate limits on API requests. If every request to a Sheetrocket endpoint triggered a fresh call to the Google Sheets API, I would hit those limits fast, especially under any real traffic. The obvious solution was in-memory caching: fetch the sheet data once, store it in a &lt;code&gt;Map&lt;/code&gt;, return the cached version on subsequent requests, and only refresh from Google when the cache expired.&lt;/p&gt;

&lt;p&gt;I had built exactly this pattern before, on long-running servers, without any issues. The cache sat at the module level, warmed up on the first request, and served every request after that from memory. Fast, simple, and it kept Google Sheets API calls to a minimum.&lt;/p&gt;

&lt;p&gt;Vercel felt like the obvious deployment choice. One-click deploy from GitHub, a generous free tier, and I was already using Next.js for other work. It barely felt like a decision.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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;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="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;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="nf"&gt;fetchFromDatabase&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;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in production, the cache never worked. Every request was hitting the Google Sheets API directly, consuming rate limit quota on every single call. The cache that worked perfectly in local development was silently empty in production every time.&lt;/p&gt;

&lt;p&gt;I spent time assuming it was a bug in my caching logic before I realized the problem had nothing to do with my code at all.&lt;/p&gt;

&lt;p&gt;The problem was not the code. It was where the code was running.&lt;/p&gt;

&lt;p&gt;And the cost of that was not just slower responses; it was correctness. A cache failure here meant real Google Sheets API quota burning on every request instead of most requests getting served from memory. Under any real traffic, that means hitting Google's rate limit, getting throttled, and returning errors instead of sheet data. Visitors don't see a slow widget at that point; they see a broken one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;Vercel runs Next.js API routes as serverless functions, not as a traditional server that starts once and stays alive. A serverless function spins up when a request arrives and shuts down when it goes idle.&lt;/p&gt;

&lt;p&gt;My in-memory cache lived inside that function instance. When the instance shut down, the cache went with it. When a new instance started, the cache was empty again. The code correctly populated the cache on the first request, but by the time the next request came in, Vercel may well have spun up a fresh instance with no memory of it at all.&lt;/p&gt;

&lt;p&gt;This is why the same code behaves differently locally versus on Vercel. Locally, the Next.js development server runs as a long-running process. It starts once and stays alive, so the cache persists across requests because the same process handles them all. Everything works perfectly in development. On Vercel, the same code runs as serverless functions. The cache works sometimes, when the same warm instance happens to handle consecutive requests. It fails other times, when a cold instance with an empty cache picks up the request instead. The behavior is inconsistent, and it depends entirely on Vercel's internal instance management, which the developer has no visibility into or control over.&lt;/p&gt;

&lt;p&gt;One sentence covers all of it: Vercel runs API routes as serverless functions, and serverless functions have no guaranteed persistent memory between requests. An in-memory cache assumes the same process handles every request. On Vercel, that assumption is simply wrong.&lt;/p&gt;

&lt;p&gt;Before I go further: Vercel has genuinely improved here. Fluid compute now keeps functions warm between requests for longer and reduces cold starts meaningfully compared to a couple of years ago. This isn't a case against Vercel as a platform. It's a case about matching an execution model to a problem, and a pure JSON API with in-memory state and no rendering is not the problem serverless was built to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything else that breaks, for the same reason
&lt;/h2&gt;

&lt;p&gt;Every problem in this post traces back to one root cause: serverless functions are stateless, and in-memory state requires a stateful process to hold it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate limiting
&lt;/h3&gt;

&lt;p&gt;A common pattern tracks how many requests an IP address has made in a rolling window, using an in-memory counter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestCounts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isRateLimited&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&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;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestCounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&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="nx"&gt;requestCounts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a long-running server, this works exactly as written. On serverless, each function instance keeps its own counter. Ten concurrent requests might land on ten different instances, each with a counter showing one request. A limit meant to be ten requests per minute is effectively multiplied by however many instances Vercel has spun up. The rate limiter isn't broken loudly; it's broken silently, which is worse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cold start latency
&lt;/h3&gt;

&lt;p&gt;When a function has been idle, Vercel has to spin up a new instance: load the Node.js runtime, import every module, establish any connections, and only then handle the request. For a simple always-on API expected to respond in milliseconds, that adds latency that is not just slower; it's unpredictable, shifting based on traffic patterns rather than staying consistent.&lt;/p&gt;

&lt;p&gt;For an internal tool used sporadically through the day, this means the first person to use it after a quiet stretch always waits longer than everyone after them. For a customer-facing API, it's the opposite of what you'd want: the slowest responses land on whoever arrives after the longest gap, often the exact users you'd least want to leave with a bad first impression.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database connection overhead
&lt;/h3&gt;

&lt;p&gt;A long-running server maintains a connection pool to the database once, at startup. A pool of ten connections can handle thousands of requests a second by reusing those same ten connections over and over.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&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;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="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;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;/users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM users&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;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&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;A serverless function doesn't get that luxury. Each cold start either opens a fresh connection or competes with other invocations over a shared pool. If a traffic spike causes Vercel to spin up 50 instances and each one opens its own connection, you've consumed 50 of your available database connections almost instantly. PostgreSQL's free and small paid tiers often cap connections somewhere between 25 and 100. The 51st instance simply can't connect, and that request fails, not because of load on the database itself, but because of how many separate processes are all trying to reach it at once.&lt;/p&gt;

&lt;h3&gt;
  
  
  Background jobs
&lt;/h3&gt;

&lt;p&gt;If you want to kick off a background task when a request arrives and let it keep running after the response is sent, Vercel will kill that task the moment the function shuts down. Fire-and-forget patterns that work cleanly on a long-running server don't work on serverless without extra infrastructure to keep the job alive independently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://formgrid.dev" rel="noopener noreferrer"&gt;Formgrid&lt;/a&gt; relies on exactly this pattern for four background operations after every form submission: Google Sheets sync, Notion sync, AI analysis, and the email notification itself. None of them block the HTTP response the visitor sees, and all of them keep running after that response is sent. That only works because Formgrid runs on a long-running Express server on Render, where the process stays alive long enough to actually finish the background work instead of being torn down mid-task.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Vercel and serverless are genuinely the right call
&lt;/h2&gt;

&lt;p&gt;This isn't an argument that serverless is bad; it's specific to what it's optimized for. Serverless is a strong choice for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static sites and marketing pages, where there's no state to maintain&lt;/li&gt;
&lt;li&gt;Server-side rendered pages with Next.js, where rendering happens fresh per request anyway&lt;/li&gt;
&lt;li&gt;Edge functions serving geographically distributed, low-latency responses&lt;/li&gt;
&lt;li&gt;Webhooks and event-driven functions that run occasionally, not continuously, with no state requirements&lt;/li&gt;
&lt;li&gt;Bursty, unpredictable workloads where paying per request beats paying for idle time&lt;/li&gt;
&lt;li&gt;Rapid prototyping, where deployment simplicity matters more than performance tuning&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A decision framework
&lt;/h2&gt;

&lt;p&gt;A short set of questions, each pointing toward an answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does your API maintain state between requests, caches, counters, session data?&lt;/strong&gt; Yes: long-running server. No: either works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it hold open connections to a database or external service that are expensive to establish?&lt;/strong&gt; Yes: long-running server with connection pooling. No: either works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you need consistent sub-100ms response times regardless of traffic patterns?&lt;/strong&gt; Yes: long-running server. No: serverless is acceptable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do you need background processing that continues after the response is sent?&lt;/strong&gt; Yes: long-running server. No: either works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is your traffic bursty and unpredictable, and is paying per request more important than consistent latency?&lt;/strong&gt; Yes: serverless is the better economic choice. No: a long-running server is usually more cost-effective and predictable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is this a Next.js app with both a frontend and API routes, where the frontend genuinely benefits from Vercel's CDN and deploy experience?&lt;/strong&gt; Yes: Vercel makes sense; the frontend benefits usually outweigh the API route limitations for most apps. No, pure API only: a long-running server on Render or Railway is almost always the better choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform comparison, with real costs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Render&lt;/strong&gt;: the free tier spins down after inactivity, which reintroduces the same cold start problem you're trying to escape. The paid tier, $7 a month, keeps the server always on with consistent performance. Formgrid runs on a $7 a month Render instance serving around 1,000 daily visitors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Railway&lt;/strong&gt;: a free tier with a $5 monthly credit, then usage-based, roughly $5 to $10 a month for a small always-on API. Good developer experience, flexible for more complex setups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fly.io&lt;/strong&gt;: a free tier is available, runs containers globally, a solid option for geographic distribution without full serverless complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hetzner&lt;/strong&gt;: around 4 euros a month for a small VPS with 2GB of RAM. The cheapest option if you're comfortable managing your own server, full control, no abstraction layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DigitalOcean App Platform&lt;/strong&gt;: a free tier available, managed containers, an experience fairly similar to Render.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hybrid approach
&lt;/h2&gt;

&lt;p&gt;Plenty of real applications use both, and it's worth naming because it's the actual nuance most take on this miss. A Next.js frontend on Vercel handles the marketing site, while a separate, long-running Express API on Render handles the backend that needs connection pooling and background processing. Vercel does what it's built for. Render does what it's built for.&lt;/p&gt;

&lt;p&gt;Formgrid is built this way. The API is a long-running Express server on Render. The dashboard is a React SPA. They're separate services, deployed separately, because each one has genuinely different infrastructure requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The infrastructure decision you make at the start of a project is one of the hardest to undo later. Not because the migration itself is usually technically complex; it typically isn't. But because you have to do it without anyone noticing.&lt;/p&gt;

&lt;p&gt;Understanding what serverless is actually optimized for, before reaching for it because it feels familiar, is one of the most valuable habits you can build as an engineer. Not because serverless is wrong. Because the right tool for the right job beats the familiar tool for every job, every time.&lt;/p&gt;

&lt;p&gt;If you've hit this same wall, or made the same call and lived to tell about it, I'd genuinely like to hear what you learned. Reach me at &lt;a href="mailto:allen@formgrid.dev"&gt;allen@formgrid.dev&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Allen, a full-stack TypeScript engineer and the founder of &lt;a href="https://sheetrocket.com" rel="noopener noreferrer"&gt;SheetRocket&lt;/a&gt;, a tool that turns Google Sheets into REST APIs and embeddable widgets, and &lt;a href="https://formgrid.dev" rel="noopener noreferrer"&gt;Formgrid&lt;/a&gt;, an open-source form backend and lead pipeline. Both run on long-running Express servers on Render. I write from what actually happens in production, not from theory. More at &lt;a href="https://jonesstack.com" rel="noopener noreferrer"&gt;jonesstack.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>backend</category>
      <category>infrastructure</category>
      <category>node</category>
    </item>
    <item>
      <title>How to Send Form Submissions to Notion Automatically Without Zapier</title>
      <dc:creator>Allen Jones</dc:creator>
      <pubDate>Sat, 29 Aug 2026 22:59:12 +0000</pubDate>
      <link>https://dev.to/allenarduino/how-to-send-form-submissionsto-notion-automatically-withoutzapier-56co</link>
      <guid>https://dev.to/allenarduino/how-to-send-form-submissionsto-notion-automatically-withoutzapier-56co</guid>
      <description>&lt;p&gt;If your team lives in Notion, your website contact form probably does not fit into that workflow.&lt;/p&gt;

&lt;p&gt;A visitor fills out your form. The submission lands in an email inbox somewhere, separate from the Notion workspace where your team actually manages clients, projects, and leads. Someone has to notice the email, copy the details, and paste them into Notion by hand. On a busy week, that step gets skipped. The enquiry sits in an inbox nobody checks until it is too late to reply.&lt;/p&gt;

&lt;p&gt;Moving data from email into Notion manually does not scale, and it is exactly the kind of small, repeated task that quietly costs you leads.&lt;/p&gt;

&lt;p&gt;This post walks through how to send form submissions straight into a Notion database automatically, with no Zapier account, no custom code, and no manual copying, using Formgrid's native Notion integration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Short Version
&lt;/h2&gt;

&lt;p&gt;Skip Zapier and custom webhook code entirely. Create your form, connect your Notion workspace once through Notion's own authorization screen, choose which database submissions should go into, and every new entry appears as a page automatically. Properties are created for you from your form fields on the first submission.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://formgrid.dev/app/signup" rel="noopener noreferrer"&gt;Try it free at formgrid.dev&lt;/a&gt;, or keep reading for the full walkthrough with screenshots.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Is Harder Than It Should Be
&lt;/h2&gt;

&lt;p&gt;If you have looked into connecting a form to Notion before, you have probably run into one of these:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zapier.&lt;/strong&gt; It works, but it costs $20 or more per month on top of whatever you already pay for your form tool. You also have to build the Zap yourself, and Zaps break quietly when either tool changes something on their end. There is also a delay between the form submission and the Notion entry showing up, since Zapier checks for new data on a schedule rather than instantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manual export.&lt;/strong&gt; Download a CSV of your submissions and import it into Notion. This works exactly once, then becomes a task you have to remember to repeat. It is never real-time, and it is the first thing to get skipped when you are busy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notion integrations built into some form tools.&lt;/strong&gt; A few form builders offer a native Notion connection, but only for forms built inside that tool. If you already have an HTML form on your website, or a form built somewhere else, those integrations are not an option for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building it yourself.&lt;/strong&gt; Writing a webhook handler that receives your form submission and inserts it into Notion through their API is a real option if you can code, but it means hosting backend code, handling authentication with Notion, and maintaining it going forward. That is a lot of ongoing work for something that should be a checkbox.&lt;/p&gt;

&lt;p&gt;None of these are good defaults for a team that just wants their form submissions to show up in Notion the moment someone submits.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who This Is For
&lt;/h2&gt;

&lt;p&gt;This guide is for anyone who manages their work in Notion and wants form submissions to land there automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agencies and freelancers&lt;/strong&gt; who track clients and projects in Notion and want new enquiries to appear as a page they can move into their existing workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small teams&lt;/strong&gt; who use a Notion database as their shared source of truth and do not want leads sitting in an inbox separate from everything else they manage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Course creators and community builders&lt;/strong&gt; who run their whole operation out of Notion and want enrollment or membership enquiries to show up right alongside the rest of their workspace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Anyone replacing a Zapier setup&lt;/strong&gt; who wants the same result without the extra monthly cost or the Zap that needs maintaining.&lt;/p&gt;

&lt;p&gt;If your team already lives in Notion, this integration was built for that exact workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Simple Alternative
&lt;/h2&gt;

&lt;p&gt;Formgrid, an open-source form backend and no-code form builder, now has a native Notion integration built directly into the platform.&lt;/p&gt;

&lt;p&gt;You connect your Notion workspace once, using Notion's own authorization screen, the same one you have seen when connecting other tools to Notion. From that point on, every form submission automatically creates a new page in the Notion database you choose. No Zapier account, no webhook to write, no manual export.&lt;/p&gt;

&lt;p&gt;It works whether you build your form using Formgrid's no-code form builder or you already have an existing HTML form on your website pointed at a Formgrid endpoint. Either way, the moment someone submits, the same submission that triggers your email notification also creates a page in Notion, in real time.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Notion Databases and Pages Work, in Plain Terms
&lt;/h2&gt;

&lt;p&gt;Before the walkthrough, here is the short version of what you are actually setting up.&lt;/p&gt;

&lt;p&gt;In Notion, a database is like a spreadsheet. Each row in that spreadsheet is called a page, and you can open any page to read the full details behind it. Each column in the spreadsheet is called a property, things like Name, Email, or Message.&lt;/p&gt;

&lt;p&gt;When a form is submitted, Formgrid creates one new page in your chosen database, essentially adding one new row. Each field from your form becomes a property on that page: your Name field becomes a Name property, your Email field becomes an Email property, and so on. The submission time is added automatically as a property called Submitted At.&lt;/p&gt;

&lt;p&gt;You do not need to set any of this up manually. The properties are created automatically the first time a submission comes in, matched to your form's fields.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step by Step: Connecting Formgrid to Notion
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Sign Up at Formgrid
&lt;/h3&gt;

&lt;p&gt;Head to &lt;a href="https://formgrid.dev" rel="noopener noreferrer"&gt;formgrid.dev&lt;/a&gt; and sign up using Google or email. No credit card required.&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%2F2i7kk8wu52ink40h5xed.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%2F2i7kk8wu52ink40h5xed.png" alt="Screenshot: Formgrid landing page" width="800" height="443"&gt;&lt;/a&gt;&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%2Fiv8p690iozkevgnxgcm2.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%2Fiv8p690iozkevgnxgcm2.png" alt="Screenshot: Formgrid signup page" width="800" height="667"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create Your Form
&lt;/h3&gt;

&lt;p&gt;Once you are logged in, you will land on your dashboard. Click New Form to get started.&lt;/p&gt;

&lt;p&gt;You can either build a form from scratch using Formgrid's drag-and-drop form builder, or, if you already have an existing HTML form on your website, create a form and use the endpoint URL it gives you instead. Both work the same way with the Notion integration.&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%2F6gtay1d12ykduo2ssx55.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%2F6gtay1d12ykduo2ssx55.png" alt="Screenshot: Formgrid dashboard showing the New Form button and a created form" width="800" height="584"&gt;&lt;/a&gt;&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%2F8t5250nh8gdoml4pjsyn.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%2F8t5250nh8gdoml4pjsyn.png" alt="Screenshot: Formgrid form details page showing the overview tab" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Open the Integrations Tab
&lt;/h3&gt;

&lt;p&gt;Click into your form to open its details page, then click the Integrations tab.&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%2Ftedbiwapi19wxn1o3y0q.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%2Ftedbiwapi19wxn1o3y0q.png" alt="Screenshot: Formgrid form details page with the Integrations tab highlighted" width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Find the Notion Section
&lt;/h3&gt;

&lt;p&gt;Scroll down to the Notion integration section and click Connect with Notion.&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%2Fdtbm80sfe52866cndw9a.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%2Fdtbm80sfe52866cndw9a.png" alt="Screenshot: Notion integration section in the Integrations tab, showing the Connect with Notion button" width="798" height="234"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Authorize Formgrid in Notion
&lt;/h3&gt;

&lt;p&gt;You are redirected to Notion's own authorization screen. This is Notion's official page, not a Formgrid page, so it looks exactly like the consent screen you have seen when connecting any other tool to Notion. Choose the workspace and the pages or databases you want to share with Formgrid, then confirm.&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%2Fdtmtnry3gkz0x7vxwr8w.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%2Fdtmtnry3gkz0x7vxwr8w.png" alt="Screenshot: Notion's official OAuth consent screen" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Choose Your Database
&lt;/h3&gt;

&lt;p&gt;Back in Formgrid, you will see a dropdown listing every Notion database you shared with the integration in the previous step. Select the one where you want form submissions to appear.&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%2Foxeo6e1hc3o9p66crkh8.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%2Foxeo6e1hc3o9p66crkh8.png" alt="Screenshot: Formgrid database picker dropdown showing available Notion databases" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Confirm the Connection
&lt;/h3&gt;

&lt;p&gt;Click Connect. The Integrations tab now shows a connected state, including the name of the Notion database you selected, along with the option to disconnect at any time.&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%2Fexu35q3n8ylrrjjgyu3w.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%2Fexu35q3n8ylrrjjgyu3w.png" alt="First Screenshot: Formgrid Integrations tab showing a connected Notion database with the disconnect option" width="800" height="323"&gt;&lt;/a&gt;&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%2Fbs60uccavo16ij83ixbi.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%2Fbs60uccavo16ij83ixbi.png" alt="Second Screenshot: Formgrid Integrations tab showing a connected Notion database with the disconnect option" width="799" height="223"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 8: Test It
&lt;/h3&gt;

&lt;p&gt;Fill out your form yourself, either on your live website or through the Formgrid shareable link. Within moments, three things happen at once, automatically, with no extra setup on your part.&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%2F9hlhg0s3jdv3abfs9ew5.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%2F9hlhg0s3jdv3abfs9ew5.png" alt="Screenshot showing form with inputs filled" width="800" height="608"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You get an email notification instantly, and it is not a generic "new submission" email. &lt;/p&gt;

&lt;p&gt;Formgrid analyzes every submission with AI before the email arrives, so the subject line already tells you what kind of submission it is before you open it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Hot Lead] Contact Form
[Likely Spam] Contact Form
[Sales Pitch] Contact Form
[Support Request] Contact Form
[General Enquiry] Contact Form
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F886bw2axmyd9f1m92rdy.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%2F886bw2axmyd9f1m92rdy.png" alt="Screenshot showing email with general enquiry" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You know what arrived without opening the email. Spam gets deleted in one tap. A hot lead gets opened and answered immediately. The AI also writes a summary inside the email body, so you understand what the person needs without reading through every field yourself.&lt;/p&gt;

&lt;p&gt;At the same time, a new page appears in your connected Notion database with every field from your submission filled in as a property.&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%2F7qaqagb6wx2mo4ql5xhe.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%2F7qaqagb6wx2mo4ql5xhe.png" alt="First Screenshot: a Notion database showing a new page created from a form submission, with Name, Email, Message, and Submitted At properties populated" width="800" height="461"&gt;&lt;/a&gt;&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%2Ft11801o96akou123qhkd.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%2Ft11801o96akou123qhkd.png" alt="Second Screenshot: a Notion database showing a new page created from a form submission, with Name, Email, Message, and Submitted At properties populated" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The same submission also becomes a tracked lead in your Formgrid dashboard, starting at New, which you will see in the next section.&lt;/p&gt;

&lt;p&gt;That is the entire setup. From here, every future submission to this form lands in Notion automatically, with no further steps.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Picture: What Happens to Every Submission
&lt;/h2&gt;

&lt;p&gt;It is worth being explicit about all three things that happen simultaneously, since most people searching for a Notion integration are only thinking about the first one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instant email notification with AI analysis.&lt;/strong&gt; Every submission is analyzed before the email reaches your inbox, so the subject line tells you what it is: a hot lead, likely spam, a sales pitch, or a support request, before you open it. A short AI summary inside the email tells you what the person actually needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatic Notion database entry.&lt;/strong&gt; At the same moment, a new page is created in your connected database with the full submission as properties, organized in the workspace your team already uses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lead pipeline tracking.&lt;/strong&gt; The same submission also becomes a tracked lead inside Formgrid, moving through New, Contacted, and Converted as you work it. You can add a private note, set a follow-up reminder that emails you on the day you need to act, and see your conversion rate over time. If you reply to a lead directly from the Formgrid dashboard using a connected Gmail account, the status updates to Contacted automatically and the reply is logged on that lead's timeline.&lt;/p&gt;

&lt;p&gt;You get the speed of an email, the organization of Notion, and a managed follow-up workflow, all from one submission, with no manual step connecting any of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Notion and Formgrid Work Together
&lt;/h2&gt;

&lt;p&gt;It helps to think of these as two different jobs rather than two tools doing the same thing.&lt;/p&gt;

&lt;p&gt;Notion is where you organize and search your submission data. Filter by date, search by email, open a page to read the full message, or build a Notion view on top of it, exactly the way your team already works with everything else in your workspace.&lt;/p&gt;

&lt;p&gt;Formgrid is where you manage the human side of each enquiry. Pipeline stages, follow-up reminders, conversion tracking, AI categorization, and replying from your own Gmail address without leaving the dashboard. None of that exists natively inside Notion.&lt;/p&gt;

&lt;p&gt;Connected, your data lives in Notion, and your workflow runs in Formgrid. Most people come looking for a way to get form data into Notion and only discover the follow-up side once they see it working, which tends to be the part that actually justifies paying for it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Managing Your Connection
&lt;/h2&gt;

&lt;p&gt;Once connected, the Integrations tab shows your connection status.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pause the integration&lt;/strong&gt; using the Active toggle. When paused, new submissions are still saved to Formgrid but not sent to Notion. Toggle it back on to resume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disconnect&lt;/strong&gt; removes the connection entirely. Your existing Notion pages stay exactly as they are, but new submissions no longer sync.&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Could not access this database"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The database was not shared with the Formgrid integration during the Notion authorization step. Reconnect and make sure to select the database you want to use when Notion asks which pages and databases to share.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Properties look wrong or missing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Properties are created automatically from your form's field names on the first submission. If you renamed a field after connecting, new properties are created rather than the old one being renamed, so it is worth connecting the integration after your form fields are finalized.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Submissions not appearing in Notion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check that the integration is Active in your Integrations tab. If it shows Paused, click the toggle to resume.&lt;/p&gt;




&lt;h2&gt;
  
  
  How This Compares to Zapier
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Zapier Route&lt;/th&gt;
&lt;th&gt;Formgrid Native&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Setup time&lt;/td&gt;
&lt;td&gt;20 to 40 minutes&lt;/td&gt;
&lt;td&gt;Under 5 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra cost&lt;/td&gt;
&lt;td&gt;$20 or more per month for Zapier&lt;/td&gt;
&lt;td&gt;Included with Formgrid Premium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works with existing HTML forms&lt;/td&gt;
&lt;td&gt;Only with extra configuration&lt;/td&gt;
&lt;td&gt;Yes, by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real time&lt;/td&gt;
&lt;td&gt;Depends on Zap frequency&lt;/td&gt;
&lt;td&gt;Instant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technical knowledge&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What You Can Do With the Data Once It Is in Notion
&lt;/h2&gt;

&lt;p&gt;Once submissions are landing as pages in your database, you get everything Notion already gives you for free: view all submissions as a table, filter by date or by any property, search by email, sort by most recent, or open an individual submission to read the full message. If your team already manages projects or clients in Notion, new leads now show up right alongside that work instead of living in a separate inbox.&lt;/p&gt;




&lt;h2&gt;
  
  
  Plan Limits
&lt;/h2&gt;

&lt;p&gt;Formgrid's free plan includes 10 Notion pages, one-time, not a monthly reset, along with 10 AI Smart Inbox analyses, also one-time. That is enough to fully test both the Notion integration and the AI categorization, confirm they work with your form, and see exactly what a submission looks like before deciding to upgrade.&lt;/p&gt;

&lt;p&gt;The Premium plan, at $12 per month, raises those limits considerably and adds everything else covered in this post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;500 Notion pages per month
1,000 AI Smart Inbox analyses per month
Google Sheets sync, 500 rows per month
Email notifications to up to 3 recipients
Full lead pipeline with custom stages,
  notes, follow-up reminders, and
  conversion rate tracking
Reply to Lead: reply from your own Gmail
  address directly from the dashboard
CSV export and bulk operations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Business plan, at $29 per month, includes unlimited Notion pages and unlimited Google Sheets sync, alongside everything in Premium.&lt;/p&gt;

&lt;p&gt;None of the above requires Zapier, at any plan.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;If your team runs on Notion, there is no good reason for your form submissions to live somewhere else. Zapier adds a monthly cost and a Zap you have to maintain. Manual export is real work you will eventually forget to do. Writing your own integration means hosting and maintaining backend code for something that should not need it.&lt;/p&gt;

&lt;p&gt;Formgrid's Notion integration replaces all of that with a one-time connection: authorize once, pick your database, and every submission from that point forward becomes a page automatically, in real time, with no further setup.&lt;/p&gt;

&lt;p&gt;Most people set this up because they want their data in Notion. What tends to keep them on the Premium plan is everything running alongside it: the AI-categorized subject lines, the lead pipeline, and replying to a lead without leaving the dashboard. That is the same pattern behind Formgrid's Google Sheets integration: people come for the spreadsheet sync and stay once they see the rest of the workflow working.&lt;/p&gt;

&lt;p&gt;Try it on the free plan first. Ten submissions are enough to see it working end-to-end before you decide whether to upgrade.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://formgrid.dev/app/signup" rel="noopener noreferrer"&gt;Connect your form to Notion at formgrid.dev&lt;/a&gt;. No credit card required to start.&lt;/p&gt;

</description>
      <category>notion</category>
      <category>form</category>
      <category>integration</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Added AI Lead Classification to My Form Tool Without Slowing Down Submissions</title>
      <dc:creator>Allen Jones</dc:creator>
      <pubDate>Fri, 28 Aug 2026 14:42:19 +0000</pubDate>
      <link>https://dev.to/allenarduino/i-added-ai-lead-classification-to-my-form-tool-without-slowing-down-submissions-3fo7</link>
      <guid>https://dev.to/allenarduino/i-added-ai-lead-classification-to-my-form-tool-without-slowing-down-submissions-3fo7</guid>
      <description>&lt;p&gt;One of the things I've learned from running a SaaS is that a feature can be technically easy and still be a difficult production problem.&lt;/p&gt;

&lt;p&gt;Form submissions are a good example. My product already knew the moment a submission arrived. The harder problem was deciding what deserved a business owner's attention. A genuine customer asking for a quote, a spam bot, an SEO sales pitch, and a support request could all arrive in the inbox looking identical, same subject line, same formatting, with no way to tell which one actually mattered.&lt;/p&gt;

&lt;p&gt;I wanted the system to figure out what each submission actually was before the business owner opened the email.&lt;/p&gt;

&lt;p&gt;So I built an AI layer that classifies every submission, assigns a priority, generates a summary, and puts that information directly into the notification email and the dashboard.&lt;/p&gt;

&lt;p&gt;The interesting part wasn't calling an LLM. It was making the feature cheap enough to run on every single submission, fast enough that it couldn't affect form delivery, and resilient enough that an AI failure could never cause a lost lead.&lt;/p&gt;

&lt;p&gt;Here's how I built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a simple LLM call wasn't enough
&lt;/h2&gt;

&lt;p&gt;The naive version of this feature is easy to picture: submission comes in, call an LLM, wait for the response, attach the result to the notification. That works fine in a demo. It falls apart under a few real constraints:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; This runs on every submission, on every form, for every account on the platform. If the LLM call sits in the request path, a slow model response means a slow form submission, and a visitor filling out a contact form has no idea or reason to care that AI is running behind the scenes. Their form just needs to submit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reliability.&lt;/strong&gt; LLM APIs fail, time out, or rate-limit. If classification is a required step before a submission is accepted, then an AI outage becomes a lost lead, which is the one outcome this feature was never allowed to cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost at scale.&lt;/strong&gt; This isn't a one-off classification task; it's every submission, indefinitely. A model chosen because it produced impressive answers in testing can turn into a real cost problem once it's running continuously in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured, consistent output.&lt;/strong&gt; I needed a category from a fixed list, a priority level, and a summary, every time, in a format I could parse reliably. Not a flexible chat response I'd have to coerce into structure after the fact.&lt;/p&gt;

&lt;p&gt;Each of those ruled out the "just call an LLM inline" version of the feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The fix was to take the AI entirely off the critical path. The visitor's submission gets accepted and confirmed immediately. The classification happens afterward, as a background step that the request itself doesn't wait on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                    ┌───────────────┐
                    │  Form visitor │
                    └───────┬───────┘
                            │
                            ▼
                    ┌───────────────┐
                    │ Formgrid API  │
                    └───────┬───────┘
                            │
                   persist submission
                            │
                            ▼
                    ┌───────────────┐
                    │ HTTP response │  ← visitor sees success instantly
                    └───────────────┘

                            │
                     background job
                            ▼
                    ┌───────────────┐
                    │ Gemini Flash  │
                    └───────┬───────┘
                            │
                  classification + priority
                        + summary
                            │
                            ▼
                    ┌───────────────┐
                    │  Update lead  │
                    └───────┬───────┘
                            │
                            ▼
                    ┌───────────────┐
                    │ Notification  │
                    │     email     │
                    └───────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The visitor never waits on the AI step; they never even know it exists. If the classification job fails or times out, the submission has already been saved, and the business owner still gets notified, just without a category attached. The AI layer can degrade without ever taking the core product down with it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The lead detail view for the first example below, with category, priority, and summary generated automatically.&lt;/em&gt;&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%2Fww6wl671et8dojgryfyb.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%2Fww6wl671et8dojgryfyb.png" alt="Screenshot: the Formgrid dashboard lead detail view for the agency submission from the table above, showing the " width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the model
&lt;/h2&gt;

&lt;p&gt;I didn't benchmark models based on which one produced the most impressive-sounding answer. I benchmarked against the actual task: a six-way classification, a priority level, and a two-to-three sentence summary, run on every submission across every account on the platform.&lt;/p&gt;

&lt;p&gt;That reframing changed the decision. A larger, more capable model would have added cost and latency without adding accuracy that mattered for a fairly constrained classification problem. I went with Google Gemini Flash; it gave me the speed and the economics the task actually needed, while being more than capable of handling six categories and a summary consistently.&lt;/p&gt;

&lt;p&gt;The six categories I landed on, after looking at what actually shows up in a real form inbox:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Potential Customer:&lt;/strong&gt; Genuine buying or engagement intent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Likely Spam:&lt;/strong&gt; Automated or irrelevant submissions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sales Pitch:&lt;/strong&gt; Someone trying to sell to the business, not buy from it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support Request:&lt;/strong&gt; An existing customer with a question or issue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job Application:&lt;/strong&gt; Someone applying for a role&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;General Enquiry:&lt;/strong&gt; A real message that doesn't fit anywhere else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each submission also gets a priority, High, Medium, or Low, based on urgency and specificity rather than category alone. A potential customer asking a vague question is Medium. A potential customer asking about pricing for 50 seats by Friday is High.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling failure without losing a lead
&lt;/h2&gt;

&lt;p&gt;The rule I held myself to: an AI failure can never cause a lost lead. If Gemini times out, errors, or the account hits its analysis limit for the billing period, the submission still saves, the business owner still gets notified, and it arrives without a category badge instead of breaking anything downstream. The AI layer only ever adds information. It's never allowed to be a dependency the core product relies on to function.&lt;/p&gt;

&lt;p&gt;That constraint is also why this couldn't be an inline, request-blocking call in the first place. Once the submission is treated as complete the moment it's persisted, an AI failure downstream literally cannot touch it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the classification actually looks like
&lt;/h2&gt;

&lt;p&gt;Abstract categories are easier to trust with real examples. Here's what four different submissions to the same contact form produce:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Submission&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;th&gt;AI summary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Hi, we're a 40-person agency looking for a form tool with an API. Can you send pricing for the Business plan? We'd want to migrate by the end of the month."&lt;/td&gt;
&lt;td&gt;Potential Customer&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Agency evaluating Business plan for a 40-person team, wants to migrate by end of month, time-sensitive purchase intent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"I'm an existing customer, my Google Sheets sync stopped working yesterday, can someone help?"&lt;/td&gt;
&lt;td&gt;Support Request&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Existing customer reporting broken Google Sheets sync since yesterday, needs troubleshooting.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Do you offer a student discount?"&lt;/td&gt;
&lt;td&gt;General Enquiry&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Prospective user asking about pricing discounts, no urgency indicated.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"hey i can get you to page 1 of google in 30 days guaranteed, reply for more info"&lt;/td&gt;
&lt;td&gt;Sales Pitch&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Unsolicited SEO service pitch, not a product enquiry.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"asdkjf83 !!! CLICK HERE now4free-followers.xyz buy cheap instagram followers 100% guaranteed !!!"&lt;/td&gt;
&lt;td&gt;Likely Spam&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Automated or bot-generated submission with no coherent message, promotional link pattern typical of spam.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same form, same inbox, five completely different submissions, and now five completely different subject lines instead of one identical "New submission" notification for all of them. That's the whole point of the feature: the business owner never has to open the spam or the SEO pitch to know they don't matter, and never risks missing the agency lead because it's buried between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I haven't measured yet
&lt;/h2&gt;

&lt;p&gt;I'd like to be honest about where the data stands rather than make this sound more finished than it is: I don't yet have hard numbers on what percentage of submissions get classified as spam versus genuine leads across accounts, or the exact inference cost per 1,000 submissions at current volume. I'm instrumenting both now, cost per classification and category distribution across real accounts, and I plan to publish those numbers once I have a few weeks of real data behind them rather than guess at them here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it shows up
&lt;/h2&gt;

&lt;p&gt;Once a submission is classified, the result appears in three places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The email subject line&lt;/strong&gt;, as a prefix before the form name, &lt;code&gt;[Hot Lead]&lt;/code&gt;, &lt;code&gt;[Likely Spam]&lt;/code&gt;, &lt;code&gt;[Sales Pitch]&lt;/code&gt;, &lt;code&gt;[Support Request]&lt;/code&gt;, so it's readable without opening the email at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The email body&lt;/strong&gt;, with the priority, category, and full summary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dashboard&lt;/strong&gt;, as a colored badge on every lead row, with a filter to pull up just the high-priority leads or exclude spam entirely.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The clearest way to see it in one place is the leads list itself, all five example submissions from the table above, each carrying its own category badge:&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%2Fw4iugmnip66t2era2hf6.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%2Fw4iugmnip66t2era2hf6.png" alt="Screenshot: a leads list view with rows corresponding to the example submissions above, each with its colored AI category badge visible (" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Caption: All example submissions in the leads list, filterable by category.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The form tool behind this
&lt;/h2&gt;

&lt;p&gt;The product this shipped on is &lt;a href="https://formgrid.dev" rel="noopener noreferrer"&gt;Formgrid&lt;/a&gt;, an open-source form backend and lead management platform I've been building and running on my own since September 2025, alongside a full-time contract role. Every form submission on Formgrid becomes a tracked lead automatically, and this AI layer, which I've been calling AI Smart Inbox, is the newest piece of that pipeline.&lt;/p&gt;

&lt;p&gt;If you want to see it running on a real submission, every account gets 10 free AI analyses to try it with no setup required.&lt;/p&gt;

&lt;p&gt;The broader lesson, for me at least: the hard part of shipping AI in a real product usually isn't the model call. It's everywhere around it, keeping it off the critical path, deciding what happens when it fails, and picking a model sized to the actual task instead of the most impressive one available.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>leadcapture</category>
      <category>saas</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Don't Build Your Own Form Backend. Here's What It Actually Costs You.</title>
      <dc:creator>Allen Jones</dc:creator>
      <pubDate>Thu, 27 Aug 2026 11:00:51 +0000</pubDate>
      <link>https://dev.to/allenarduino/dont-build-your-own-form-backend-heres-what-it-actually-costs-you-1kc1</link>
      <guid>https://dev.to/allenarduino/dont-build-your-own-form-backend-heres-what-it-actually-costs-you-1kc1</guid>
      <description>&lt;p&gt;Every developer has this thought at some point. "It's just a form. I'll write a quick endpoint, send an email with Resend or SendGrid, and be done in an afternoon."&lt;/p&gt;

&lt;p&gt;Then you actually build it.&lt;/p&gt;

&lt;p&gt;Two hours in, you have a working POST endpoint. Three days later, you are writing spam filtering rules by hand. Two weeks in, a client asks, "Can you show me all the submissions from last month?" and you realize you never built a dashboard, so you are querying your production database directly to answer a simple question. A month in, you are debugging why half your notification emails are landing in spam because you never set up SPF and DKIM records properly.&lt;/p&gt;

&lt;p&gt;None of that was in the "quick afternoon" estimate.&lt;/p&gt;

&lt;p&gt;This post explains why building your own form backend is almost always the wrong call, how much unexpected time it can cost you, and what you should use instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Feels Like It Should Be Easy
&lt;/h2&gt;

&lt;p&gt;On the surface, a form backend looks trivial. Receive a POST request, read the fields, send an email. That part genuinely does take an afternoon.&lt;/p&gt;

&lt;p&gt;The problem is that a form backend that is actually usable in production is not that. It is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. An endpoint that accepts submissions
2. Spam and bot filtering
3. Email delivery that does not land in spam
4. A dashboard to view past submissions
5. Storage that does not disappear
6. Rate limiting so one bad actor can't flood you
7. Input validation and sanitization
8. File upload handling, if you need it
9. A way to track what happens after a submission arrives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You do not see most of this list on day one. You see it the first time a bot floods your endpoint, the first time a client asks to see old submissions, or the first time your notification emails start landing in spam because you are sending transactional email from a domain with no reputation.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hidden Costs Nobody Budgets For
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Spam filtering.&lt;/strong&gt; The moment your form is public, bots find it. Not eventually, immediately. You will need a honeypot field at minimum, and realistically CAPTCHA or a service like hCaptcha or Cloudflare Turnstile on top of that. Writing spam heuristics that catch crypto spam, disposable emails, and HTML injection without also blocking real users takes real iteration, not a weekend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email deliverability.&lt;/strong&gt; Sending email reliably is its own discipline. SPF, DKIM, DMARC, a sending domain with reputation, retry logic for failed sends, and a fallback for when your email provider has an outage. Get any of this wrong, and your "notification" system silently stops notifying you, which is the worst possible failure mode for a form backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A dashboard.&lt;/strong&gt; The first time someone asks "can I see submissions from before I set up email forwarding" or "can you export the last 200 leads to CSV," you realize you built an endpoint, not a system. Now you are building an admin panel: auth, pagination, search, filtering, export. That is not a form backend feature anymore; that is a small internal product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Storage and retention.&lt;/strong&gt; Where do submissions live? For how long? What happens when the database grows? Do you need backups? None of this is form-backend-specific; it is generic infrastructure you now own because you decided to roll your own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting and abuse prevention.&lt;/strong&gt; Without it, one bad actor can submit your form thousands of times a minute, which either racks up your email sending bill or takes down your database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GDPR and data handling.&lt;/strong&gt; If you collect any personal data from a form, and you almost always do, you now have a small compliance surface: right to deletion, data retention policy, and knowing exactly where the data lives. This is easy to ignore until someone asks you to delete their data and you have no clean way to find it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ongoing maintenance.&lt;/strong&gt; Every one of the systems above needs to keep working as your dependencies update, as spam patterns evolve, and as your email provider changes their API. A form backend is not something you build once. It is something you maintain forever, or it slowly rots.&lt;/p&gt;

&lt;p&gt;None of this is a knock on your ability to build it. All of it is buildable. The question is whether it is worth building, given that it is not the product you are actually trying to ship.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Math
&lt;/h2&gt;

&lt;p&gt;Say you are honest about the real scope: endpoint, spam filtering, email delivery, a basic dashboard, and rate limiting. That is not an afternoon. That is realistically two to four days of focused work if you already know what you are doing, longer if you do not.&lt;/p&gt;

&lt;p&gt;At any reasonable hourly rate, two to four days of engineering time costs more than a year of a managed form backend, and that is before counting the maintenance time you will keep spending every time something breaks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DIY form backend:
  2-4 days initial build
  + ongoing maintenance
  + your time debugging deliverability issues
  + your time when a client asks for a feature
    the endpoint does not have

Managed form backend:
  10 minutes to set up
  $0-12/month
  Someone else's job to keep spam filtering,
  deliverability, and uptime working
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same tradeoff as writing your own authentication system instead of using an auth provider, or rolling your own payment processing instead of using Stripe. It is not that you cannot build it. It is that building it is rarely the highest-value use of your time, because it is infrastructure, not your product.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Building Your Own Actually Makes Sense
&lt;/h2&gt;

&lt;p&gt;To be fair, there are real cases where rolling your own is the right call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You have genuinely unusual requirements
no service can meet

You are building this as the product itself,
not as infrastructure for something else

You have strict data residency or compliance
requirements that require full control

You already have this infrastructure built
for other reasons and reusing it is nearly free

You are doing it deliberately as a learning
exercise, not for a production client site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If none of those apply to you, and for most people building a contact form, a quote request form, or a lead capture form on a client site or their own product, none of them do, a managed service is the better trade every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "Just Use a Service" Actually Looks Like
&lt;/h2&gt;

&lt;p&gt;Here is the entire difference between the DIY approach and using a form backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DIY, roughly:&lt;/strong&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;// You write and maintain all of this yourself&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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;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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// validate input&lt;/span&gt;
  &lt;span class="c1"&gt;// check honeypot / run captcha verification&lt;/span&gt;
  &lt;span class="c1"&gt;// rate limit by IP&lt;/span&gt;
  &lt;span class="c1"&gt;// sanitize fields&lt;/span&gt;
  &lt;span class="c1"&gt;// save to a database you provisioned&lt;/span&gt;
  &lt;span class="c1"&gt;// send email via a provider you configured&lt;/span&gt;
  &lt;span class="c1"&gt;// handle delivery failures&lt;/span&gt;
  &lt;span class="c1"&gt;// return a response&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;success&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Using a form backend:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"https://formgrid.dev/api/f/your-form-id"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Your Name"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Your Email"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Your Message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send Message&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire integration. No server code. No database to provision. No email service to configure. Spam filtering, delivery, storage, and a dashboard all come with the endpoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting This Up Takes About Ten Minutes
&lt;/h2&gt;

&lt;p&gt;If you want to see exactly how little is involved, here is the full setup using Formgrid, an open-source form backend, as the example.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create an Account
&lt;/h3&gt;

&lt;p&gt;Head to &lt;a href="https://formgrid.dev" rel="noopener noreferrer"&gt;formgrid.dev&lt;/a&gt; and sign up with Google or email. No credit card required.&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%2Fx2pat1cb833frkflg7bt.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%2Fx2pat1cb833frkflg7bt.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create a Form and Copy the Endpoint
&lt;/h3&gt;

&lt;p&gt;Click New Form, give it a name, and click Create Form.&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.amazonaws.com%2Fuploads%2Farticles%2Fozar28d2u9v4whi05ogg.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.amazonaws.com%2Fuploads%2Farticles%2Fozar28d2u9v4whi05ogg.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the Overview tab, copy your endpoint URL.&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%2Fxcbr1ogkbp6odvk7v2zm.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%2Fxcbr1ogkbp6odvk7v2zm.png" alt=" " width="799" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Point Your Form at It
&lt;/h3&gt;

&lt;p&gt;Drop the endpoint into your form's &lt;code&gt;action&lt;/code&gt; attribute. If you already have a form on your site, this is the only line that changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"https://formgrid.dev/api/f/your-form-id"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- honeypot, invisible to real users --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"_honey"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:none"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spam filtering, rate limiting, and bot protection are already active. You did not configure any of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Notifications Are Already On
&lt;/h3&gt;

&lt;p&gt;Nothing to set up here. The email address you signed up with is the default notification address the moment your form goes live. If you want submissions sent somewhere else, like a client's inbox or a shared team address, that is a single field in Settings.&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.amazonaws.com%2Fuploads%2Farticles%2F8kp2sswbjh2dvwvi41ed.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.amazonaws.com%2Fuploads%2Farticles%2F8kp2sswbjh2dvwvi41ed.png" alt=" " width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Every Submission Is Already a Tracked Lead
&lt;/h3&gt;

&lt;p&gt;This is the part a DIY endpoint never gets to, because building it is a whole separate project. Every submission lands in a dashboard automatically, with a status you can move through New, Contacted, and Converted, not just a one-off email you have to remember to act on.&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%2Fcy6o7wogoc68ewiqozt7.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%2Fcy6o7wogoc68ewiqozt7.png" alt=" " width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can see the submitter's details, add a private note, and set a follow-up reminder, all without writing a line of backend code.&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%2Fnjta68h51law54ktrr5w.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%2Fnjta68h51law54ktrr5w.png" alt=" " width="800" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the entire setup. No server to maintain, no email provider to configure, no dashboard to build, and no spam filtering logic to write and keep tuning.&lt;/p&gt;




&lt;h2&gt;
  
  
  What You Get That You Would Not Have Built Yourself
&lt;/h2&gt;

&lt;p&gt;This is really the heart of the argument. It is not just that a managed form backend saves you the two to four days of initial build time. It is that most people never get around to building the parts that make a form backend actually useful, because those parts are not urgent until they suddenly are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A lead pipeline.&lt;/strong&gt; Not just an inbox. Every submission gets a status, New, Contacted, Converted, so you know exactly what happened after someone filled out your form. This is the single most common gap in a DIY form backend, because building it means building a small CRM, not a form handler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatic submission triage.&lt;/strong&gt; Modern form backends can categorize what came in for you, flagging a genuine enquiry versus a sales pitch versus spam, and surfacing the important ones first. Building this yourself means training or wiring up a classification model on top of everything else on this list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permanent, searchable storage.&lt;/strong&gt; Not a 30-day window, not a database table you have to remember to back up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Integrations you did not have to build.&lt;/strong&gt; Google Sheets sync, Notion sync, webhooks, CSV export. Each of these is a real integration with its own API quirks and edge cases if you build it yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spam filtering that improves without your involvement.&lt;/strong&gt; You are not the one updating the spam patterns when a new kind of crypto spam campaign shows up.&lt;/p&gt;

&lt;p&gt;None of this is exotic. It is just the difference between a weekend project and a product that someone maintains full-time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building your own form backend is a genuinely fun weekend project, and if that is what it is for you, build it. There is real value in understanding how the pieces fit together.&lt;/p&gt;

&lt;p&gt;But if the form is infrastructure for something else, a client site, your own product's contact page, a lead capture flow for a business, it is almost never worth the time. The part that looks like the whole job, receive a POST request and send an email, is maybe 10% of what a form backend actually needs to do well. The other 90% is spam filtering, deliverability, a dashboard, storage, and a way to track what happens after someone submits, and that 90% is exactly the part that never makes it into the "quick afternoon" estimate.&lt;/p&gt;

&lt;p&gt;Stop building your own form backend. Point your form at one instead, and spend the time you saved on the thing you were actually trying to build.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://formgrid.dev/app/signup" rel="noopener noreferrer"&gt;Try Formgrid free at formgrid.dev&lt;/a&gt;. No credit card required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full disclosure: I built Formgrid. I wrote this as honestly as I could. If anything looks inaccurate, let me know in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>formbackend</category>
      <category>formgrid</category>
      <category>spamprotection</category>
    </item>
    <item>
      <title>Build a Restaurant Website in HTML with a Menu That Updates from Google Sheets</title>
      <dc:creator>Allen Jones</dc:creator>
      <pubDate>Wed, 26 Aug 2026 16:36:00 +0000</pubDate>
      <link>https://dev.to/allenarduino/build-a-restaurant-website-in-html-with-a-menu-that-updates-from-google-sheets-5hic</link>
      <guid>https://dev.to/allenarduino/build-a-restaurant-website-in-html-with-a-menu-that-updates-from-google-sheets-5hic</guid>
      <description>&lt;p&gt;Most restaurant websites have the same problem.&lt;/p&gt;

&lt;p&gt;The menu is hardcoded in HTML. Every time a price changes or a dish gets added, someone has to edit the website. If the owner does not know how to code,  they call their developer. The developer charges for the update. The menu is always slightly out of date.&lt;/p&gt;

&lt;p&gt;This tutorial shows you a different approach. You &lt;br&gt;
build a complete restaurant website with plain HTML. &lt;br&gt;
The menu section pulls live data from a Google Sheet. &lt;br&gt;
When the owner changes a price or adds a new dish, the &lt;br&gt;
website updates automatically. No developer needed. &lt;br&gt;
No rebuild. No redeploy.&lt;/p&gt;

&lt;p&gt;Here is what you will build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A complete restaurant website in plain HTML and CSS&lt;/li&gt;
&lt;li&gt;A live menu section powered by a Google Sheet&lt;/li&gt;
&lt;li&gt;Search and filter by category&lt;/li&gt;
&lt;li&gt;Cart and online ordering built in&lt;/li&gt;
&lt;li&gt;Automatic order email to the restaurant owner&lt;/li&gt;
&lt;li&gt;Hosted on any static platform for free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The menu at the bottom of this page is a live demo. &lt;br&gt;
It is pulling real data from a Google Sheet right now.&lt;/p&gt;


&lt;h2&gt;
  
  
  What You Will Need
&lt;/h2&gt;

&lt;p&gt;Before starting, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A text editor (VS Code or any editor)&lt;/li&gt;
&lt;li&gt;A Google account&lt;/li&gt;
&lt;li&gt;A free Sheetrocket account&lt;/li&gt;
&lt;li&gt;Basic HTML knowledge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No server. No backend. No database.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Here is how the whole thing works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Restaurant owner edits Google Sheet
          |
          v
Sheetrocket reads the sheet via API
          |
          v
Widget script fetches the latest data
          |
          v
Menu renders on the HTML page automatically
          |
          v
Customer adds to cart and places order
          |
          v
Restaurant owner receives order email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The restaurant owner never touches the website. &lt;br&gt;
They manage everything in a spreadsheet they already know how to use.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 1: Set Up Your Google Sheet
&lt;/h2&gt;

&lt;p&gt;Start with your menu data. Create a new Google &lt;br&gt;
Sheet with the following columns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name | Price | Category | Description | Image URL | Available
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a sample menu to get you started:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name              | Price  | Category | Description                    | Image URL | Available
Margherita Pizza  | $12.99 | Pizza    | Classic tomato and mozzarella  | https://... | Yes
Pepperoni Pizza   | $14.99 | Pizza    | Spicy pepperoni and cheese     | https://... | Yes
Caesar Salad      | $8.99  | Salads   | Romaine, croutons, parmesan    | https://... | Yes
Grilled Chicken   | $16.99 | Mains    | Herb marinated grilled chicken | https://... | Yes
Chocolate Cake    | $6.99  | Desserts | Rich dark chocolate layers     | https://... | Yes
Lemonade          | $3.99  | Drinks   | Fresh squeezed with mint       | https://... | Yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use any image URLs from Unsplash, your own &lt;br&gt;
Google Drive, or any image hosting service. For &lt;br&gt;
Unsplash images, add &lt;code&gt;?w=400&amp;amp;h=300&amp;amp;fit=crop&lt;/code&gt; to &lt;br&gt;
the end of the URL to get a consistent size.&lt;/p&gt;

&lt;p&gt;Keep the column names exactly as shown. Sheetrocket &lt;br&gt;
will use them for automatic mapping.&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%2F37mkciykl4nm6hb61g3q.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%2F37mkciykl4nm6hb61g3q.png" alt="Screenshot of the Google Sheet with menu data filled in across all columns" width="799" height="349"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 2: Create Your SheetRocket Account
&lt;/h2&gt;

&lt;p&gt;Go to &lt;a href="https://sheetrocket.com" rel="noopener noreferrer"&gt;sheetrocket.com&lt;/a&gt; and &lt;br&gt;
sign up for free. No credit card required.&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%2Fbdjlx4qlpf3ww9w1r6m5.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%2Fbdjlx4qlpf3ww9w1r6m5.png" alt="Sheetrocket Landing Page Screenshot" width="800" height="446"&gt;&lt;/a&gt;&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%2Favq8pgq3k14vm63hbk1a.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%2Favq8pgq3k14vm63hbk1a.png" alt="Screenshot of the Sheetrocket signup page" width="799" height="613"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you are logged in, you will see your dashboard. &lt;br&gt;
This is where you create and manage your widgets, &lt;br&gt;
sites, and APIs.&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%2Fbo68cvew2eju60eslvf4.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%2Fbo68cvew2eju60eslvf4.png" alt="Screenshot of the Sheetrocket dashboard with the sidebar showing Widgets, Sites, and APIs" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3: Create a Menu Widget
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;Widgets&lt;/strong&gt; in the left sidebar. Then click &lt;br&gt;
&lt;strong&gt;Create widget&lt;/strong&gt;.&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%2Fj5uv3r6k9x5hnbymalxz.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%2Fj5uv3r6k9x5hnbymalxz.png" alt="Screenshot of the Widgets page with the Create widget button highlighted" width="799" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will see a gallery of widget templates. Click &lt;br&gt;
on &lt;strong&gt;Menu Widget&lt;/strong&gt;.&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%2Fm87cekqikuzf09b2adtf.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%2Fm87cekqikuzf09b2adtf.png" alt="Screenshot of the widget template gallery with Menu Widget highlighted" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Give your widget a name. Something like &lt;br&gt;
"Allen's Restaurant Menu" works. Click &lt;strong&gt;Create&lt;/strong&gt;.&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%2Fr09gjwhj7tfmjdrg6y8r.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%2Fr09gjwhj7tfmjdrg6y8r.png" alt="Screenshot of the widget naming step" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will be taken to the widget builder. On the right side, you will see a live preview of your widget with sample data. On the left side, you have three tabs: Data, Design, and Filters.&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%2F17nf8kil5ohtmug1rhxl.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%2F17nf8kil5ohtmug1rhxl.png" alt="Screenshot of the widget builder with sample data showing in the preview panel" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 4: Connect Your Google Sheet
&lt;/h2&gt;

&lt;p&gt;Click the &lt;strong&gt;Data&lt;/strong&gt; tab in the left panel.&lt;/p&gt;

&lt;p&gt;You will see two things: a data source section &lt;br&gt;
and the column mapping fields below it. The column mapping fields are always visible so you can see exactly what columns your sheet needs before connecting.&lt;/p&gt;

&lt;p&gt;Sheetrocket uses a service account to read your Google Sheet securely. You need to share your sheet with the SheetRocket service account email.&lt;/p&gt;

&lt;p&gt;Find the service account email in the Data tab. &lt;br&gt;
It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sheetrocket@sheetrocket-app.iam.gserviceaccount.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fdd8en6jpgskr0dsoa2hg.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%2Fdd8en6jpgskr0dsoa2hg.png" alt="Screenshot of the Data tab showing the service account email with a copy button" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Copy that email address. Now go back to your &lt;br&gt;
Google Sheet.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Share&lt;/strong&gt; in the top right corner of your &lt;br&gt;
Google Sheet. Paste the service account email &lt;br&gt;
into the share field. Set the permission to &lt;br&gt;
&lt;strong&gt;Viewer&lt;/strong&gt;. Click &lt;strong&gt;Share&lt;/strong&gt;.&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%2Fyn4bnkyfrm99jbkoxrij.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%2Fyn4bnkyfrm99jbkoxrij.png" alt="Screenshot of the Google Sheets share dialog" width="800" height="446"&gt;&lt;/a&gt;&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%2Fwhae2yi4g585shrqbhr5.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%2Fwhae2yi4g585shrqbhr5.png" alt="Screenshot of the Google Sheets share dialog with the service account email pasted in" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now copy the URL of your Google Sheet from your browser's address bar. It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://docs.google.com/spreadsheets/d/
1abc123def456/edit#gid=0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go back to SheetRocket and paste this URL into &lt;br&gt;
the &lt;strong&gt;Google Sheet URL&lt;/strong&gt; field in the Data tab. &lt;br&gt;
Click &lt;strong&gt;Connect&lt;/strong&gt;.&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%2Ffqna5caikp5lwd4nu5os.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%2Ffqna5caikp5lwd4nu5os.png" alt="Screenshot of the Data tab with the Google Sheet URL pasted in and the Connect button" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sheetrocket will read your sheet and automatically &lt;br&gt;
detect your columns. You will see a success message, and the auto-mapping will pre-fill the column &lt;br&gt;
mapping fields.&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%2Fvkgv3fkrl6a25fsmvkje.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%2Fvkgv3fkrl6a25fsmvkje.png" alt="Screenshot showing successful sheet connection with columns auto-mapped and a green success indicator" width="800" height="1119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Review the column mapping. SheetRocket should &lt;br&gt;
have matched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Item name    →   Name
Price        →   Price
Image        →   Image URL
Category     →   Category
Description  →   Description
Available    →   Available
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any mapping is off, use the dropdown to &lt;br&gt;
correct it. Click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Your preview on the right side of the builder &lt;br&gt;
will now show your real menu items.&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%2Fptab8wzfh2lvm0c2wx16.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%2Fptab8wzfh2lvm0c2wx16.png" alt="Screenshot of the widget builder with real menu items showing in the preview, including food photos" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 5: Customize the Design
&lt;/h2&gt;

&lt;p&gt;Click the &lt;strong&gt;Design&lt;/strong&gt; tab.&lt;/p&gt;

&lt;p&gt;Here you can match the widget to your restaurant &lt;br&gt;
branding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Theme:&lt;/strong&gt; Choose Light or Dark. Dark works well &lt;br&gt;
for upscale restaurants. Light works for casual &lt;br&gt;
cafes and family restaurants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accent color:&lt;/strong&gt; Set this to your restaurant's &lt;br&gt;
primary brand color. This affects badge colors &lt;br&gt;
and the cart button.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Card columns:&lt;/strong&gt; Choose 2, 3, or 4. Three columns work well for most menu layouts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image height:&lt;/strong&gt; 200px to 240px works for most &lt;br&gt;
food photography.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Card radius:&lt;/strong&gt; 12px gives a modern rounded look. &lt;br&gt;
0px gives a sharper, more minimal look.&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%2Fvrnzb55jvfuvj4j4lc1j.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%2Fvrnzb55jvfuvj4j4lc1j.png" alt="First Screenshot of the Design tab with the color picker and column options visible" width="800" height="447"&gt;&lt;/a&gt;&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%2Fc4cb1imn0f6rnn6mcraa.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%2Fc4cb1imn0f6rnn6mcraa.png" alt="Second Screenshot of the Design tab with the color picker and column options visible" width="800" height="447"&gt;&lt;/a&gt;&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%2Fmykkhqu3fa6ya20eyasq.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%2Fmykkhqu3fa6ya20eyasq.png" alt="Third Screenshot of the Design tab with the color picker and design options visible" width="800" height="447"&gt;&lt;/a&gt;&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%2Fibjp27m5hjdjfwo1mb4h.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%2Fibjp27m5hjdjfwo1mb4h.png" alt="Fourth Screenshot of the Design tab with the color picker and design options visible" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The preview updates in real time as you make &lt;br&gt;
changes. Take your time here to get it right before moving on.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 6: Set Up Category Filtering
&lt;/h2&gt;

&lt;p&gt;Click the &lt;strong&gt;Filters&lt;/strong&gt; tab.&lt;/p&gt;

&lt;p&gt;Enable &lt;strong&gt;Show search bar&lt;/strong&gt;. This lets customers search for specific dishes by name.&lt;/p&gt;

&lt;p&gt;Enable &lt;strong&gt;Show filter dropdown&lt;/strong&gt;. Set the filter &lt;br&gt;
column to &lt;strong&gt;Category&lt;/strong&gt;. This adds a dropdown &lt;br&gt;
that lets customers filter by Pizza, Salads, &lt;br&gt;
Mains, Desserts, and Drinks.&lt;/p&gt;

&lt;p&gt;Enable &lt;strong&gt;Pagination&lt;/strong&gt; if you have more than 12 &lt;br&gt;
menu items. Set items per page to 12.&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%2Fezaj9x4cxvdc6zq37nec.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%2Fezaj9x4cxvdc6zq37nec.png" alt="Screenshot of the Filters tab with search enabled, category filter enabled, and pagination on" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your preview will now show the search bar and &lt;br&gt;
filter dropdown at the top of the menu.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 7: Enable Cart and Ordering
&lt;/h2&gt;

&lt;p&gt;This is the part that turns a menu display into an actual ordering system.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;Design&lt;/strong&gt; tab, scroll down to the &lt;br&gt;
&lt;strong&gt;Button&lt;/strong&gt; section.&lt;/p&gt;

&lt;p&gt;Toggle &lt;strong&gt;Add to cart mode&lt;/strong&gt; on. This changes &lt;br&gt;
the button on each menu card from a link to &lt;br&gt;
a circular plus button. Customers tap the &lt;br&gt;
plus to add items to their cart.&lt;/p&gt;

&lt;p&gt;Set the &lt;strong&gt;Button color&lt;/strong&gt; to match your &lt;br&gt;
accent color.&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%2Fngtoko5hevl6o47yyzfv.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%2Fngtoko5hevl6o47yyzfv.png" alt="First Screenshot of the button settings with Add to cart mode toggled on" width="800" height="439"&gt;&lt;/a&gt;&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%2Fhxqkipklsszfkmild51d.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%2Fhxqkipklsszfkmild51d.png" alt="Second Screenshot of the button settings with Add to cart mode toggled on" width="800" height="447"&gt;&lt;/a&gt;&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%2Fvsm34f3zuhnyvp33ims6.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%2Fvsm34f3zuhnyvp33ims6.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now scroll down further to the &lt;strong&gt;Cart settings&lt;/strong&gt; &lt;br&gt;
section.&lt;/p&gt;

&lt;p&gt;Set your &lt;strong&gt;Order notification email&lt;/strong&gt;. This is where SheetRocket will send an email every time a customer places an order. Use the &lt;br&gt;
restaurant owner's email address.&lt;/p&gt;

&lt;p&gt;You can also configure what fields to collect &lt;br&gt;
at checkout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name         (required, always on)
Email        (required, always on)
Phone        toggle on or off
Address      toggle on or off
Notes        toggle on or off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a restaurant taking delivery orders, turn on Phone and Address. For takeaway orders, just Phone is usually enough.&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%2Fxvn3bd9o3o4zegxy0290.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%2Fxvn3bd9o3o4zegxy0290.png" alt="First Screenshot of the cart settings showing order email field and checkout field toggles" width="800" height="447"&gt;&lt;/a&gt;&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%2Fi3kpr83yeik8v8659gxy.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%2Fi3kpr83yeik8v8659gxy.png" alt="Second Screenshot of the cart settings showing order email field and checkout field toggles" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Toggle &lt;strong&gt;Send confirmation email to customer&lt;/strong&gt; on. When a customer places an order, they will receive an automatic confirmation email with their order details.&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%2Fjk0x6j94yrt4a47iqqze.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%2Fjk0x6j94yrt4a47iqqze.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Copy Your Embed Code
&lt;/h2&gt;

&lt;p&gt;Click the &lt;strong&gt;Embed&lt;/strong&gt; tab.&lt;/p&gt;

&lt;p&gt;You will see your embed code. It looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
  &lt;span class="na"&gt;data-sheetrocket=&lt;/span&gt;&lt;span class="s"&gt;"your-widget-id"&lt;/span&gt;
  &lt;span class="na"&gt;data-template=&lt;/span&gt;&lt;span class="s"&gt;"menu-widget"&lt;/span&gt;
  &lt;span class="na"&gt;data-theme=&lt;/span&gt;&lt;span class="s"&gt;"light"&lt;/span&gt;
  &lt;span class="na"&gt;data-columns=&lt;/span&gt;&lt;span class="s"&gt;"3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script
  &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://sheetrocket.com/widget.js"&lt;/span&gt;
  &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Copy embed code&lt;/strong&gt;.&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%2Fh5bbzg20517hnbqb4pjb.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%2Fh5bbzg20517hnbqb4pjb.png" alt="Screenshot of the Embed tab with the two-line code block and the Copy embed code button" width="800" height="911"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 9: Build the Restaurant Website HTML
&lt;/h2&gt;

&lt;p&gt;Now you build the full restaurant website. &lt;br&gt;
The menu section uses the embed code you just &lt;br&gt;
copied.&lt;/p&gt;

&lt;p&gt;Create a new file called &lt;code&gt;index.html&lt;/code&gt; and paste &lt;br&gt;
in this complete template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Allen's Restaurant, Little Italy, New York&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Allen's Restaurant has been serving Little Italy since 1998. Handmade pasta, wood-fired pizza, and a table when you need one. Walk in or reserve."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preconnect"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://fonts.googleapis.com"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"preconnect"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://fonts.gstatic.com"&lt;/span&gt; &lt;span class="na"&gt;crossorigin&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://fonts.googleapis.com/css2?family=Karla:ital,wght@0,400;0,500;0,600;0,700;1,400&amp;amp;family=Playfair+Display:ital,wght@0,400;0,500;0,600;1,400;1,500&amp;amp;display=swap"&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;--paper&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#F3EDE2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--paper-2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#E8DFD0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--ink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1A1410&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--ink-soft&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#5A5248&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--wine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6E1C22&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--wine-dark&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#4E1318&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--olive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#3D4A32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--rule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#C9BBA5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--on-wine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#F3EDE2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--font-display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Playfair Display"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Georgia&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;"Times New Roman"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--font-body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Karla"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--ease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200ms&lt;/span&gt; &lt;span class="n"&gt;ease&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--z-nav&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--z-menu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--space-xs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--space-sm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--space-md&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--space-lg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--space-xl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;32px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--space-2xl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;48px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--space-3xl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1120px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;--gutter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1.25rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4vw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2.5rem&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="o"&gt;*,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;box-sizing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;border-box&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="py"&gt;scroll-behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;smooth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-reduced-motion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;scroll-behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;*,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;animation-duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.01ms&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;animation-iteration-count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;transition-duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.01ms&lt;/span&gt; &lt;span class="cp"&gt;!important&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="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--font-body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--ink&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--paper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.65&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;-webkit-font-smoothing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;antialiased&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="nd"&gt;::after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;pointer-events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;40&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.035&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)&lt;/span&gt;&lt;span class="s2"&gt;'/%3E%3C/svg%3E");
    }

    img {
      max-width: 100%;
      display: block;
    }

    a, button, label, select, summary {
      cursor: pointer;
    }

    a:focus-visible,
    button:focus-visible,
    input:focus-visible,
    select:focus-visible,
    textarea:focus-visible {
      outline: 2px solid var(--wine);
      outline-offset: 3px;
    }

    .skip-link {
      position: absolute;
      left: var(--gutter);
      top: -100px;
      z-index: 50;
      background: var(--wine);
      color: var(--on-wine);
      padding: 0.75rem 1.25rem;
      font-size: 0.875rem;
      font-weight: 600;
      text-decoration: none;
    }

    .skip-link:focus {
      top: 1rem;
    }

    .wrap {
      width: min(var(--content), calc(100% - var(--gutter) * 2));
      margin-inline: auto;
    }

    #about,
    #menu,
    #reserve,
    #visit,
    #main {
      scroll-margin-top: 80px;
    }

    /* Nav */
    .site-header {
      position: sticky;
      top: 0;
      z-index: var(--z-nav);
      background: var(--paper);
      border-bottom: 1px solid var(--rule);
    }

    .nav {
      display: flex;
      align-items: center;
      justify-content: space-between;
      height: 72px;
      gap: var(--space-lg);
    }

    .nav-panel {
      margin-left: auto;
    }

    .brand {
      font-family: var(--font-display);
      font-size: 1.625rem;
      font-style: italic;
      font-weight: 500;
      color: var(--ink);
      text-decoration: none;
      line-height: 1;
    }

    .brand span {
      display: block;
      font-family: var(--font-body);
      font-style: normal;
      font-size: 0.6875rem;
      font-weight: 500;
      letter-spacing: 0.12em;
      color: var(--ink-soft);
      margin-top: 4px;
    }

    .nav-links {
      display: flex;
      align-items: center;
      gap: 2rem;
      list-style: none;
    }

    .nav-links a {
      color: var(--ink);
      text-decoration: none;
      font-size: 0.9375rem;
      font-weight: 500;
      transition: color var(--ease);
    }

    .nav-links a:hover {
      color: var(--wine);
    }

    .nav-links a.nav-reserve {
      display: inline-block;
      background: var(--wine);
      color: var(--on-wine);
      text-decoration: none;
      font-size: 0.8125rem;
      font-weight: 600;
      padding: 0.7rem 1.15rem;
      border-radius: 2px;
      transition: background var(--ease);
    }

    .nav-links a.nav-reserve:hover {
      background: var(--wine-dark);
      color: var(--on-wine);
    }

    .nav-toggle {
      display: none;
      width: 44px;
      height: 44px;
      border: 0;
      background: transparent;
      position: relative;
    }

    .nav-toggle span,
    .nav-toggle span::before,
    .nav-toggle span::after {
      display: block;
      width: 20px;
      height: 1.5px;
      background: var(--ink);
      position: absolute;
      left: 12px;
      transition: transform var(--ease), opacity var(--ease);
    }

    .nav-toggle span { top: 21px; }
    .nav-toggle span::before,
    .nav-toggle span::after {
      content: "";
      left: 0;
    }
    .nav-toggle span::before { top: -6px; }
    .nav-toggle span::after { top: 6px; }

    .nav-toggle[aria-expanded="true"] span {
      background: transparent;
    }
    .nav-toggle[aria-expanded="true"] span::before {
      top: 0;
      transform: rotate(45deg);
    }
    .nav-toggle[aria-expanded="true"] span::after {
      top: 0;
      transform: rotate(-45deg);
    }

    /* Hero */
    .hero {
      display: grid;
      grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
      min-height: calc(100svh - 72px);
      border-bottom: 1px solid var(--rule);
    }

    .hero-copy {
      display: flex;
      flex-direction: column;
      justify-content: center;
      padding: clamp(2.5rem, 6vw, 5rem) 2.5rem clamp(2.5rem, 5vw, 4rem) max(var(--gutter), calc((100vw - var(--content)) / 2));
    }

    .hero-copy p.lede {
      font-size: 0.875rem;
      color: var(--olive);
      font-weight: 600;
      margin-bottom: 1.25rem;
    }

    .hero h1 {
      font-family: var(--font-display);
      font-weight: 500;
      font-size: clamp(2.5rem, 5.4vw, 4.25rem);
      line-height: 1.12;
      letter-spacing: -0.02em;
      margin-bottom: 1.25rem;
    }

    .hero h1 em {
      font-style: italic;
      font-weight: 400;
    }

    .hero-copy .intro {
      font-size: 1.125rem;
      color: var(--ink-soft);
      max-width: 28rem;
      margin-bottom: 2rem;
    }

    .hero-actions {
      display: flex;
      align-items: center;
      gap: 1.5rem;
      flex-wrap: wrap;
    }

    .btn {
      display: inline-block;
      background: var(--wine);
      color: var(--on-wine);
      text-decoration: none;
      font-size: 0.9375rem;
      font-weight: 600;
      padding: 0.9rem 1.5rem;
      border: 0;
      border-radius: 2px;
      font-family: inherit;
      transition: background var(--ease);
    }

    .btn:hover {
      background: var(--wine-dark);
    }

    .text-link {
      color: var(--ink);
      font-size: 0.9375rem;
      font-weight: 600;
      text-decoration: none;
      border-bottom: 1px solid var(--ink);
      padding-bottom: 2px;
      transition: color var(--ease), border-color var(--ease);
    }

    .text-link:hover {
      color: var(--wine);
      border-color: var(--wine);
    }

    .hero-photo {
      min-height: 320px;
      overflow: hidden;
    }

    .hero-photo img {
      width: 100%;
      height: 100%;
      object-fit: cover;
      min-height: 100%;
    }

    /* About */
    .about {
      padding: var(--space-3xl) 0;
    }

    .about-grid {
      display: grid;
      grid-template-columns: 1.05fr 0.95fr;
      gap: clamp(2rem, 5vw, 4.5rem);
      align-items: center;
    }

    .about-photo {
      overflow: hidden;
    }

    .about-photo img {
      width: 100%;
      height: 100%;
      aspect-ratio: 4 / 5;
      object-fit: cover;
    }

    .about-photo figcaption {
      font-size: 0.75rem;
      color: var(--ink-soft);
      margin-top: 0.75rem;
    }

    .about h2 {
      font-family: var(--font-display);
      font-weight: 500;
      font-size: clamp(2rem, 3.5vw, 2.75rem);
      line-height: 1.2;
      margin-bottom: 1.25rem;
    }

    .about h2 em {
      font-style: italic;
      font-weight: 400;
    }

    .about p {
      color: var(--ink-soft);
      font-size: 1.0625rem;
      margin-bottom: 1rem;
    }

    .pull {
      font-family: var(--font-display);
      font-style: italic;
      font-size: 1.375rem;
      line-height: 1.45;
      color: var(--ink);
      margin: 1.75rem 0 0;
      padding-top: 1.5rem;
      border-top: 1px solid var(--rule);
    }

    /* Visit band */
    .visit {
      background: var(--wine);
      color: var(--on-wine);
      padding: 3.5rem 0;
    }

    .visit-grid {
      display: grid;
      grid-template-columns: 1.2fr repeat(3, 1fr);
      gap: 2rem;
      align-items: start;
    }

    .visit h2 {
      font-family: var(--font-display);
      font-weight: 500;
      font-size: clamp(1.75rem, 3vw, 2.25rem);
      line-height: 1.2;
    }

    .visit h2 em {
      font-style: italic;
      font-weight: 400;
    }

    .visit h3 {
      font-size: 0.75rem;
      font-weight: 600;
      letter-spacing: 0.08em;
      text-transform: uppercase;
      color: rgba(243, 237, 226, 0.65);
      margin-bottom: 0.5rem;
    }

    .visit p,
    .visit a {
      font-size: 1rem;
      color: var(--on-wine);
      text-decoration: none;
      line-height: 1.55;
    }

    .visit a:hover {
      text-decoration: underline;
    }

    /* Menu */
    .menu {
      padding: var(--space-3xl) 0;
    }

    .menu-header {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 2rem;
      align-items: end;
      margin-bottom: 2.5rem;
      padding-bottom: 1.5rem;
      border-bottom: 1px solid var(--rule);
    }

    .menu h2 {
      font-family: var(--font-display);
      font-weight: 500;
      font-size: clamp(2rem, 3.5vw, 2.75rem);
    }

    .menu-header p {
      color: var(--ink-soft);
      font-size: 1.0625rem;
    }

    /* Reserve */
    .reserve {
      padding: 0 0 var(--space-3xl);
    }

    .reserve-grid {
      display: grid;
      grid-template-columns: 0.85fr 1.15fr;
      gap: clamp(2rem, 5vw, 4.5rem);
      padding-top: var(--space-3xl);
      border-top: 1px solid var(--rule);
    }

    .reserve h2 {
      font-family: var(--font-display);
      font-weight: 500;
      font-size: clamp(2rem, 3.5vw, 2.75rem);
      margin-bottom: 1rem;
    }

    .reserve .lede {
      color: var(--ink-soft);
      font-size: 1.0625rem;
      margin-bottom: 1.5rem;
    }

    .reserve-note {
      font-size: 0.9375rem;
      color: var(--ink-soft);
    }

    .form-row {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 1.25rem;
    }

    .field {
      margin-bottom: 1.25rem;
    }

    .field label {
      display: block;
      font-size: 0.8125rem;
      font-weight: 600;
      margin-bottom: 0.4rem;
    }

    .field input,
    .field select,
    .field textarea {
      width: 100%;
      font: 400 1rem/1.4 var(--font-body);
      color: var(--ink);
      background: transparent;
      border: 0;
      border-bottom: 1px solid var(--rule);
      border-radius: 0;
      padding: 0.65rem 0;
      appearance: none;
    }

    .field select {
      background-image: url("data:image/svg+xml,%3Csvg xmlns='&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;://&lt;/span&gt;&lt;span class="n"&gt;www&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;w3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;svg&lt;/span&gt;&lt;span class="s2"&gt;' width='&lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="s2"&gt;' height='&lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="s2"&gt;' viewBox='&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="s2"&gt;'%3E%3Cpath fill='&lt;/span&gt;&lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="s2"&gt;' stroke='&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="m"&gt;231&lt;/span&gt;&lt;span class="n"&gt;A1410&lt;/span&gt;&lt;span class="s2"&gt;' stroke-width='&lt;/span&gt;&lt;span class="m"&gt;1.4&lt;/span&gt;&lt;span class="s2"&gt;' d='&lt;/span&gt;&lt;span class="n"&gt;M1&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;l5&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="m"&gt;5-5&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;svg&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="err"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nl"&gt;background-repeat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;no-repeat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;background-position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;right&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;padding-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;.field&lt;/span&gt; &lt;span class="nt"&gt;textarea&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vertical&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;.field&lt;/span&gt; &lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="nd"&gt;:focus&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;.field&lt;/span&gt; &lt;span class="nt"&gt;select&lt;/span&gt;&lt;span class="nd"&gt;:focus&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;.field&lt;/span&gt; &lt;span class="nt"&gt;textarea&lt;/span&gt;&lt;span class="nd"&gt;:focus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;border-bottom-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--wine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;.form-status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--font-display&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nl"&gt;font-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;italic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.375rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;line-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.45&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;.form-status.is-visible&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="nc"&gt;.is-sent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;/* Footer */&lt;/span&gt;
    &lt;span class="nt"&gt;footer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;border-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--rule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;.footer-inner&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;space-between&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;flex-wrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;.footer-inner&lt;/span&gt; &lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.875rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--ink-soft&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;.footer-inner&lt;/span&gt; &lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--ink&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;.footer-inner&lt;/span&gt; &lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--wine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;860px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;.nav-toggle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nc"&gt;.nav-panel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;72px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--z-menu&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--paper&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nl"&gt;border-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--rule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gutter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nc"&gt;.nav-panel.is-open&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nc"&gt;.nav-links&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex-start&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nc"&gt;.nav-reserve&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nc"&gt;.hero&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nc"&gt;.hero-photo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;46vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;240px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nc"&gt;.hero-copy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--gutter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;2.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nc"&gt;.about-grid&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="nc"&gt;.menu-header&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="nc"&gt;.reserve-grid&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
      &lt;span class="nc"&gt;.form-row&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nc"&gt;.about-photo&lt;/span&gt; &lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nc"&gt;.visit-grid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.75rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nc"&gt;.visit-grid&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;:first-child&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;grid-column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;-1&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;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;520px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;.visit-grid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&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="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"skip-link"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#main"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Skip to content&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;header&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"site-header"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wrap nav"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"brand"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Allen's
        &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Restaurant · Est. 1998&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nav-toggle"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;aria-expanded=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="na"&gt;aria-controls=&lt;/span&gt;&lt;span class="s"&gt;"nav-panel"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Open menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nav-panel"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"nav-panel"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nav-links"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#about"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;About&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Menu&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#reserve"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Reservations&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"nav-reserve"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#reserve"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Reserve a table&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hero"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Introduction"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hero-copy"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"lede"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;123 Pasta Lane, Little Italy&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Pasta rolled this morning. &lt;span class="nt"&gt;&amp;lt;em&amp;gt;&lt;/span&gt;Sauce from yesterday.&lt;span class="nt"&gt;&amp;lt;/em&amp;gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"intro"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          A neighborhood kitchen for twenty-five years. Handmade pasta, wood-fired pizza, and a table when you need one.
        &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hero-actions"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#reserve"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Reserve a table&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-link"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;See the menu&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hero-photo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
          &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://images.unsplash.com/photo-1552566626-52f8b828add9?w=1600&amp;amp;q=80"&lt;/span&gt;
          &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"The dining room at Allen's Restaurant"&lt;/span&gt;
          &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1600"&lt;/span&gt;
          &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt;
          &lt;span class="na"&gt;fetchpriority=&lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"about"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"about"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wrap about-grid"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;figure&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"about-photo"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt;
            &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=1200&amp;amp;q=80"&lt;/span&gt;
            &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"The dining room at Allen's Restaurant"&lt;/span&gt;
            &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1200"&lt;/span&gt;
            &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"1500"&lt;/span&gt;
            &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;figcaption&amp;gt;&lt;/span&gt;The dining room on a Tuesday. Come as you are.&lt;span class="nt"&gt;&amp;lt;/figcaption&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/figure&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Three generations in &lt;span class="nt"&gt;&amp;lt;em&amp;gt;&lt;/span&gt;one kitchen.&lt;span class="nt"&gt;&amp;lt;/em&amp;gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
            Allen's Restaurant has been on Pasta Lane since 1998. Every dish is made from scratch, from recipes that started in a family kitchen outside Naples and settled here.
          &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;
            Tomatoes come from a family farm in Campania. The cheese is from a producer two hours upstate. The pasta is rolled every morning. If we run out, we run out.
          &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pull"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            “The tagliatelle is what my nonna would have made, if she had a wood oven in Little Italy.”
          &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"visit"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"visit"&lt;/span&gt; &lt;span class="na"&gt;aria-labelledby=&lt;/span&gt;&lt;span class="s"&gt;"visit-heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wrap visit-grid"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"visit-heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Walk in, or &lt;span class="nt"&gt;&amp;lt;em&amp;gt;&lt;/span&gt;call ahead.&lt;span class="nt"&gt;&amp;lt;/em&amp;gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;Address&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;123 Pasta Lane&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;Little Italy&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;New York, NY 10013&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;Hours&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Monday–Friday&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;12pm–10pm&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;Saturday–Sunday&lt;span class="nt"&gt;&amp;lt;br&amp;gt;&lt;/span&gt;11am–11pm&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;Phone&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"tel:+12125550142"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;(212) 555-0142&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wrap"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-header"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;What's cooking&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Made to order. Add to your cart and we'll have it ready for pickup or delivery.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
          &lt;span class="na"&gt;data-sheetrocket=&lt;/span&gt;&lt;span class="s"&gt;"cmt9z2b4a0008x8sw6hrif38p"&lt;/span&gt;
          &lt;span class="na"&gt;data-widget=&lt;/span&gt;&lt;span class="s"&gt;"menu-widget"&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://sheetrocket.com/catalog-widget.js"&lt;/span&gt; &lt;span class="na"&gt;async&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"reserve"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"reserve"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wrap reserve-grid"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Reserve a table&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"lede"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            Tell us when you're coming. We'll write back to confirm. Weekends fill up, so a day's notice helps.
          &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"reserve-note"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            For parties of eight or more, call the restaurant directly.
          &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-status"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"form-status"&lt;/span&gt; &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"reserve-form"&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt; &lt;span class="na"&gt;novalidate&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Name&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;autocomplete=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;autocomplete=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"date"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Date&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"date"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"date"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"date"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"time"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Time&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"time"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"time"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Choose a time&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;12:00 pm&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;12:30 pm&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;1:00 pm&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;1:30 pm&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;6:00 pm&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;6:30 pm&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;7:00 pm&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;7:30 pm&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;8:00 pm&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;8:30 pm&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;9:00 pm&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-row"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"party"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Party size&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"party"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"party"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;How many&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;1&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;2&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;3&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;4&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;5&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;6&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;option&amp;gt;&lt;/span&gt;7&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"phone"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Phone&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"phone"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"phone"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"tel"&lt;/span&gt; &lt;span class="na"&gt;autocomplete=&lt;/span&gt;&lt;span class="s"&gt;"tel"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"notes"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Anything we should know?&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notes"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"notes"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Allergies, a birthday, a quieter table…"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send request&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;footer&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"wrap footer-inner"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Allen's Restaurant · Little Italy · Est. 1998&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"tel:+12125550142"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;(212) 555-0142&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;toggle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;.nav-toggle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;panel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nav-panel&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;toggle&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;panel&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aria-expanded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aria-label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Close menu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Open menu&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-open&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;toggle&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;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aria-expanded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&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;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;link&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;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;setOpen&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="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="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reserve-form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;form-status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;dateField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;date&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dateField&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;month&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;dateField&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;month&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;form&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;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;function &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="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;preventDefault&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;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkValidity&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reportValidity&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;var&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button[type="submit"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt; &lt;span class="o"&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;submit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Sending…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-sent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Thanks. We'll confirm your table by email shortly.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is-visible&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="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="p"&gt;})();&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR-WIDGET-ID&lt;/code&gt; in the embed code with &lt;br&gt;
the actual widget ID from your SheetRocket &lt;br&gt;
Embed tab.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 10: Test the Full Website
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;index.html&lt;/code&gt; in your browser. You should see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A full restaurant website with hero, about, 
features, menu, contact, and footer&lt;/li&gt;
&lt;li&gt;Your live menu pulling data from your Google Sheet&lt;/li&gt;
&lt;li&gt;Search and category filter working on the menu&lt;/li&gt;
&lt;li&gt;Add to cart buttons on each menu card&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faimksw0npblx34mamzci.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%2Faimksw0npblx34mamzci.png" alt="Screenshot of the complete restaurant website in a browser with the hero section showing" width="800" height="444"&gt;&lt;/a&gt;&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%2Fjvkroqsjrbyadv9fsned.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%2Fjvkroqsjrbyadv9fsned.png" alt="Screenshot of the complete restaurant website in a browser with the menu section showing" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the add-to-cart button on a menu item to add it &lt;br&gt;
to the cart. A floating cart button will appear &lt;br&gt;
at the top right of the page.&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%2F9shsmjgwreg4tpat57db.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%2F9shsmjgwreg4tpat57db.png" alt="Screenshot of a food card with the circular cart button visible in the bottom right corner of the card" width="800" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click the floating cart button to open the cart &lt;br&gt;
modal. You will see your selected items, a quantity &lt;br&gt;
control, and a checkout button.&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%2Fvwzwzxyes1tcon0yh9a4.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%2Fvwzwzxyes1tcon0yh9a4.png" alt="Screenshot of the cart modal open with two items, quantities, a total, and a Checkout button" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Checkout&lt;/strong&gt; and fill in the order form with &lt;br&gt;
a name, email, phone number, and address. Click &lt;br&gt;
&lt;strong&gt;Place order&lt;/strong&gt;.&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%2Fctnsd6e3kmxwkn4yw4op.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%2Fctnsd6e3kmxwkn4yw4op.png" alt="Screenshot of the checkout form with name, email, phone, and address fields filled in" width="800" height="526"&gt;&lt;/a&gt;&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%2Fhil4i4vsrs9uym90rhg7.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%2Fhil4i4vsrs9uym90rhg7.png" alt="Screenshot of the confirmation dialog after checkout" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Within seconds, the restaurant owner receives &lt;br&gt;
an order notification email with the full &lt;br&gt;
order details.&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%2F9vnudgv3gs3i5c7le1ex.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%2F9vnudgv3gs3i5c7le1ex.png" alt="Screenshot of the order notification email in an inbox showing customer name, items ordered, and total" width="799" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The customer also receives a confirmation email &lt;br&gt;
automatically.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 11: Test the Live Update
&lt;/h2&gt;

&lt;p&gt;This is the part that makes SheetRocket worth &lt;br&gt;
using. Go back to your Google Sheet and change &lt;br&gt;
a price. Update "Margherita Pizza" from $12.99 &lt;br&gt;
to $13.99.&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%2Fdtechjdxv78urvbztpv4.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%2Fdtechjdxv78urvbztpv4.png" alt="Screenshot of the Google Sheet with the Margherita Pizza price being changed from 12.99 to 13.99" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now refresh your HTML page.&lt;/p&gt;

&lt;p&gt;The updated price appears on the menu immediately. &lt;br&gt;
No website edit. No redeploy. No developer.&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%2Fbmu27dlsrsnetxa7zr0i.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%2Fbmu27dlsrsnetxa7zr0i.png" alt="Screenshot of the menu widget showing the updated price of $13.99 on the Margherita Pizza card" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Add a new dish to your Google Sheet by adding &lt;br&gt;
a new row at the bottom. Fill in the name, price, category, description, and image URL. Set Available to Yes.&lt;/p&gt;

&lt;p&gt;Refresh the page. The new dish appears on the menu.&lt;/p&gt;

&lt;p&gt;Remove a dish by deleting the row from your sheet. &lt;br&gt;
Refresh the page. It is gone from the menu.&lt;/p&gt;

&lt;p&gt;The restaurant owner can manage the entire menu &lt;br&gt;
from their Google Sheet without ever touching &lt;br&gt;
the website.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 12: Deploy the Website
&lt;/h2&gt;

&lt;p&gt;Your HTML file is a static website. It can be &lt;br&gt;
hosted for free on any static hosting platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Pages (free):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new GitHub repository&lt;/li&gt;
&lt;li&gt;Upload your &lt;code&gt;index.html&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Go to Settings then Pages&lt;/li&gt;
&lt;li&gt;Set the source to the main branch&lt;/li&gt;
&lt;li&gt;Your site is live at 
&lt;code&gt;yourusername.github.io/repository-name&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Netlify (free):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to netlify.com and create a free account&lt;/li&gt;
&lt;li&gt;Drag your &lt;code&gt;index.html&lt;/code&gt; file onto the 
Netlify deploy area&lt;/li&gt;
&lt;li&gt;Your site is live instantly at a 
random Netlify URL&lt;/li&gt;
&lt;li&gt;Connect a custom domain in settings&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Vercel (free):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to vercel.com and create a free account&lt;/li&gt;
&lt;li&gt;Import your GitHub repository or drag 
your file&lt;/li&gt;
&lt;li&gt;Your site is live at a Vercel URL&lt;/li&gt;
&lt;li&gt;Connect a custom domain in settings&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a custom domain like &lt;code&gt;mariosrestaurant.com&lt;/code&gt; &lt;br&gt;
point your domain's DNS to your hosting provider &lt;br&gt;
and add it in their dashboard.&lt;/p&gt;


&lt;h2&gt;
  
  
  Live Demo
&lt;/h2&gt;

&lt;p&gt;The menu below is pulling live data from a &lt;br&gt;
Google Sheet. Search for dishes, filter by &lt;br&gt;
category, and add items to your cart to see &lt;br&gt;
the full ordering flow in action.&lt;/p&gt;



&lt;p&gt;
  data-sheetrocket="DEMO-WIDGET-ID"&lt;br&gt;
  data-template="menu-widget"&lt;br&gt;
  data-theme="light"&lt;br&gt;
  data-columns="3"&amp;gt;&lt;br&gt;
&lt;br&gt;

  src="https://sheetrocket.com/widget.js"&lt;br&gt;
  async&amp;gt;&lt;br&gt;
&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%2F4qe6n4yxwewvnj6uangl.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%2F4qe6n4yxwewvnj6uangl.png" alt="Screenshot of the live demo section on the blog post showing the SheetRocket menu widget with pizza and pasta cards visible" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Restaurant Owner Gets
&lt;/h2&gt;

&lt;p&gt;Once this is set up, the restaurant owner has &lt;br&gt;
complete control over their menu without &lt;br&gt;
touching the website:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Change a price:
  Edit one cell in Google Sheets
  Website updates automatically

Add a new dish:
  Add one row to the sheet
  Dish appears on the website

Remove a dish:
  Delete the row from the sheet
  Dish disappears from the website

Mark something as unavailable:
  Change Available from Yes to No
  Dish shows as unavailable
  or disappears from the menu

Add a daily special:
  Add a row with today's date
  Remove it at end of day
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No login to a website builder. No calling &lt;br&gt;
a developer. No waiting for someone to make the change. Just edit the spreadsheet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Taking It Further
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Add a second location:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a second widget for the second location &lt;br&gt;
connected to a different tab in the same Google &lt;br&gt;
Sheet. Embed it on a second page or on the &lt;br&gt;
same page below the first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seasonal menus:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a Summer Menu tab and a Winter Menu tab &lt;br&gt;
in your sheet. Switch the widget between tabs by changing one setting in SheetRocket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Daily specials:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add a Specials tab to your sheet. Create a &lt;br&gt;
second widget connected to that tab. Embed &lt;br&gt;
it above the main menu with a "Today's specials" &lt;br&gt;
heading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple languages:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Duplicate your sheet tab for each language. &lt;br&gt;
Create one widget per language. Show the &lt;br&gt;
correct widget based on a language selector &lt;br&gt;
in your HTML.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;A hardcoded restaurant menu is a maintenance &lt;br&gt;
problem waiting to happen. Every price change, every seasonal update, every dish removed requires a developer or someone comfortable &lt;br&gt;
editing HTML.&lt;/p&gt;

&lt;p&gt;A SheetRocket-powered menu puts control back &lt;br&gt;
with the restaurant owner. They already know &lt;br&gt;
how to use Google Sheets. This tutorial adds a live website connection to the spreadsheet they are already maintaining.&lt;/p&gt;

&lt;p&gt;The website is plain HTML. It loads fast, &lt;br&gt;
works on any hosting, and costs nothing to deploy. The menu updates automatically the moment the sheet is edited. Online orders arrive by email. No monthly platform fees. No complex integrations.&lt;/p&gt;

&lt;p&gt;If you are building this for a restaurant &lt;br&gt;
client, SheetRocket's Agency plan at $39/month covers unlimited restaurant sites. &lt;br&gt;
Each client manages their own Google Sheet. You manage everything from one SheetRocket account.&lt;/p&gt;

&lt;p&gt;Start free at &lt;a href="https://sheetrocket.com" rel="noopener noreferrer"&gt;sheetrocket.com&lt;/a&gt;. &lt;br&gt;
No credit card required.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I am the founder of SheetRocket, &lt;br&gt;
a tool that turns Google Sheets into websites, &lt;br&gt;
embeddable widgets, and REST APIs. Currently &lt;br&gt;
building in public from Ghana.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>css</category>
      <category>html</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>What Actually Makes a Software Engineer Great (It's Not the Framework You Know)</title>
      <dc:creator>Allen Jones</dc:creator>
      <pubDate>Tue, 25 Aug 2026 18:50:15 +0000</pubDate>
      <link>https://dev.to/allenarduino/what-actually-makes-a-software-engineer-great-its-not-the-framework-you-know-2m3l</link>
      <guid>https://dev.to/allenarduino/what-actually-makes-a-software-engineer-great-its-not-the-framework-you-know-2m3l</guid>
      <description>&lt;p&gt;Learn React. Learn a backend language. Build projects. Apply to jobs. That's the advice every developer who wants to level up hears. That advice isn't wrong, but it misses the thing that actually separates a senior engineer from someone who's been writing code for six months. It's not the framework in your resume. It's whether you understand why that framework exists in the first place, and whether you can reason about a problem well enough to pick the right tool instead of the familiar one.&lt;/p&gt;

&lt;p&gt;Here are the three things that actually move the needle.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Understand the "Why," Not Just the "How"
&lt;/h2&gt;

&lt;p&gt;If you're learning to code today, the common advice is "learn React." Fair enough, React is popular and in demand. But if you don't understand why React had to exist, how frontend development worked before it, and what problem it was solving, you're going to hit a ceiling.&lt;/p&gt;

&lt;p&gt;A quick example: before &lt;code&gt;async/await&lt;/code&gt; existed, asynchronous JavaScript was written with nested callbacks, and once you had a few dependent async operations in a row, that nesting turned into what everyone just called callback hell. Promises came along to flatten that out, and &lt;code&gt;async/await&lt;/code&gt; came after to make asynchronous code read almost like synchronous code. If you only learn &lt;code&gt;async/await&lt;/code&gt; as "the syntax you use for async stuff," you'll be lost the moment you open an older codebase full of &lt;code&gt;.then()&lt;/code&gt; chains or raw callbacks. Knowing what problem it replaced is what lets you actually read and work in code that predates it.&lt;/p&gt;

&lt;p&gt;This matters because tools get chosen for reasons, and those reasons change depending on the problem. A senior engineer doesn't reach for React (or Rails, or Django, or whatever is trending) by default. They look at what the project actually needs to solve, now and as it grows, and pick technology based on that.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Concrete Example: Node.js vs Python
&lt;/h3&gt;

&lt;p&gt;This "understand the why" idea gets a lot more concrete when you compare Node.js and Python, because the two languages handle concurrency in fundamentally different ways, and that difference decides what each one is actually good at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node.js runs on a single thread with a non-blocking event loop.&lt;/strong&gt; When a request comes in that needs to read a file, hit a database, or call another API, Node doesn't block that thread waiting for the response. It hands the operation off (to libuv under the hood), keeps handling other requests, and comes back to yours when the data is ready.&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;// Node.js: this doesn't block the event loop&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;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;/user/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM users WHERE id = ?&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;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes Node excellent at I/O bound workloads: REST APIs, real-time apps (chat, live notifications via WebSockets), streaming services, and anything where a request spends most of its time waiting on the network or disk rather than doing heavy computation. A single Node process can juggle thousands of concurrent connections because it isn't burning CPU cycles or a full thread stack per connection.&lt;/p&gt;

&lt;p&gt;Where Node struggles is CPU-bound work. Because everything runs on that one thread, a heavy synchronous computation (image processing, video encoding, complex encryption, large in-memory data crunching) blocks the event loop entirely. Every other request waiting on that same process just freezes until the computation finishes.&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;// This blocks every other request until it finishes&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;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;/process&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;heavyImageProcessing&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// blocks the event loop&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&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;Node does have &lt;code&gt;worker_threads&lt;/code&gt; and &lt;code&gt;child_process&lt;/code&gt; to push CPU-heavy work off the main thread, but that's a workaround you have to reach for deliberately; it's not the default execution model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python's story is almost the opposite.&lt;/strong&gt; Python has something called the GIL, the Global Interpreter Lock. It means that even if you spin up multiple threads in a single Python process, only one thread can execute Python bytecode at a time. So naive multi-threading in Python does not give you true parallelism for CPU-bound work. Adding more threads to a CPU-heavy Python task usually doesn't make it faster, because they're all waiting their turn for the same lock.&lt;/p&gt;

&lt;p&gt;This is why Python isn't the first choice for something like a high-concurrency real-time API server written from scratch, at least not without help. Pure multi-threaded Python doesn't scale CPU work the way you'd expect.&lt;/p&gt;

&lt;p&gt;But this is also where it gets interesting, because Python is still the dominant language in data science and machine learning, and that's not a contradiction. Two things make it work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;I/O bound work still benefits from Python's concurrency tools.&lt;/strong&gt; The GIL is released while a thread is waiting on I/O (network calls, file reads), so Python's &lt;code&gt;threading&lt;/code&gt; module or &lt;code&gt;asyncio&lt;/code&gt; (which mirrors Node's event loop model) still works well for I/O heavy workloads like web scraping or calling multiple APIs concurrently.&lt;/li&gt;
&lt;li&gt;** CPU-heavy libraries do their real work outside the GIL.** Libraries like NumPy, pandas, and PyTorch are built on C, C++, or CUDA under the hood. When you call a NumPy operation on a large array, Python hands the actual number crunching to compiled code that releases the GIL and can run in parallel or on a GPU. Python itself is just the orchestration layer; the heavy lifting happens outside the interpreter entirely.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For true CPU-bound parallelism in pure Python (not delegated to a C extension), you reach for &lt;code&gt;multiprocessing&lt;/code&gt;, which spins up separate processes, each with its own interpreter and its own GIL, and splits the work across CPU cores. It works, but it's heavier than threading because each process has its own memory space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where each one fits:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Good fit&lt;/th&gt;
&lt;th&gt;Weak fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Node.js&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;APIs and microservices with high concurrent I/O, real-time apps, streaming, chat backends, thin services with light CPU work&lt;/td&gt;
&lt;td&gt;Heavy synchronous computation, video/image processing, CPU-heavy encryption, ML model training&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Python&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Data science, machine learning, scientific computing, scripting and automation, I/O bound work via asyncio, backends where the ecosystem (FastAPI, Django) fits&lt;/td&gt;
&lt;td&gt;Raw CPU-bound parallelism using plain threads, extremely high-concurrency real-time servers without async tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Neither language is "better." They were built with different priorities, and picking between them for a project means understanding what kind of work that project actually does, not just which one you're more comfortable with. That's the same lesson as the React example, just at the language level instead of the framework level.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters More With AI Tools
&lt;/h3&gt;

&lt;p&gt;This "understand the why" skill matters even more now that AI coding agents can write most of the code for you. There's a real difference between telling an AI agent "add authentication to this app" and being able to tell it whether to use sessions or JWTs, how long tokens should live, whether refresh tokens belong in an httpOnly cookie or local storage, and what happens to existing logged-in users when the auth flow changes.&lt;/p&gt;

&lt;p&gt;The AI is doing the typing. The decisions still have to come from you. If your AI agent can make a technical decision without you being able to explain why it was the right one, that's worth noticing, because it means the reasoning skill isn't being built, only the code is.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Product Engineering: Go Beyond Writing Code
&lt;/h2&gt;

&lt;p&gt;The role of a software engineer is shifting. It used to be enough to be handed a spec and implement it well. Increasingly, engineers are expected to be part of figuring out what should be built in the first place.&lt;/p&gt;

&lt;p&gt;That means being comfortable with things that used to sit entirely with product managers: talking to users to understand the actual problem they're facing, translating that into a roadmap or a set of tickets, tying the work back to a business goal, and collaborating with product managers and designers on how the solution should work before a single line of code gets written.&lt;/p&gt;

&lt;p&gt;Once that planning is done, handing the implementation over to an AI agent (or just writing it yourself) becomes the easy part. The hard, valuable part is everything that happens before that: understanding the problem well enough to know what "done" actually looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. System Design: Know What's Underneath the Platform
&lt;/h2&gt;

&lt;p&gt;A lot of developers can push code to a managed hosting platform and have it live in minutes without ever thinking about what's happening behind that button. That's completely fine for shipping fast, but it becomes a real gap the moment you're handed infrastructure that nobody has already wired up for you.&lt;/p&gt;

&lt;p&gt;Picture a different scenario: you're told your application needs to handle ten times its current traffic with no downtime. Would you know whether the fix is adding more app servers behind a load balancer, moving to a bigger single server, or introducing a caching layer so the database stops doing repeated work? If terms like reverse proxy, connection pooling, or cache invalidation feel fuzzy, that's less about any one tool and more about a gap in system design fundamentals.&lt;/p&gt;

&lt;p&gt;The fundamentals worth understanding here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The difference between horizontal scaling (more machines) and vertical scaling (bigger machines), and when each makes sense&lt;/li&gt;
&lt;li&gt;What a load balancer and a reverse proxy actually do, and how requests get routed across multiple app servers&lt;/li&gt;
&lt;li&gt;Private networking and secure communication between services&lt;/li&gt;
&lt;li&gt;Database security, connection pooling, and access patterns at scale&lt;/li&gt;
&lt;li&gt;Managing infrastructure through code and version control versus clicking through a cloud dashboard by hand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using a managed hosting platform because it's fast and convenient is a completely reasonable choice. The problem is only when it's the only thing you know how to do, because the moment a project needs something that platform doesn't offer, you're stuck without the fundamentals to build it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;None of these three things (understanding the fundamentals behind your tools, thinking like a product engineer, and understanding system design) show up on a "learn React in 30 days" course outline. They're slower to build and harder to measure. But they're also the actual difference between an engineer who can be handed any problem and figure it out, and one who can only work within the exact stack and constraints they were taught.&lt;/p&gt;

&lt;p&gt;If you're early in your career, you don't need to master all three at once. Start by picking apart one tool or framework you already use every day and asking why it exists, what problem it solved, and what the alternative would have looked like without it. That habit, repeated across enough tools, is what builds the kind of judgment that AI can't replace for you.&lt;/p&gt;

</description>
      <category>softwareengineer</category>
      <category>dev</category>
      <category>backend</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>The N+1 Query Problem: Why Your API Might Be Making 101 Database Queries Instead of 2</title>
      <dc:creator>Allen Jones</dc:creator>
      <pubDate>Tue, 25 Aug 2026 15:55:47 +0000</pubDate>
      <link>https://dev.to/allenarduino/the-n1-query-problem-why-your-api-might-be-making-101-database-queries-instead-of-2-4j3g</link>
      <guid>https://dev.to/allenarduino/the-n1-query-problem-why-your-api-might-be-making-101-database-queries-instead-of-2-4j3g</guid>
      <description>&lt;p&gt;If your API feels fast in development but becomes painfully slow in production as your data grows, there's a good chance you're looking at the N+1 query problem. It's one of the most common backend performance issues, and the frustrating part is that the code usually looks completely fine at first glance.&lt;/p&gt;

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

&lt;p&gt;Say you have two tables: &lt;code&gt;customers&lt;/code&gt; and &lt;code&gt;orders&lt;/code&gt;. Each order belongs to a customer through a foreign key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="nb"&gt;NUMERIC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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;Here's what that relationship looks like. Each row in &lt;code&gt;orders&lt;/code&gt; points back to exactly one row in &lt;code&gt;customers&lt;/code&gt; through &lt;code&gt;customer_id&lt;/code&gt;, while a single customer can have many orders pointing at them. That's a classic one to many relationship.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;erDiagram
    customers ||--o{ orders : "has many"
    customers {
        int id PK
        varchar name
    }
    orders {
        int id PK
        int customer_id FK
        numeric total
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also picture it as two flat tables with the foreign key drawing the line between them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;customers                      orders
+----+---------+               +----+-------------+--------+
| id | name    |               | id | customer_id | total  |
+----+---------+               +----+-------------+--------+
| 1  | Sarah   |     &amp;lt;---------| 1  | 1           | 42.00  |
| 2  | James   |     &amp;lt;---+     | 2  | 1           | 15.50  |
| 3  | Emily   |     &amp;lt;-+ \-----| 3  | 2           | 99.99  |
+----+---------+       \-------| 4  | 3           | 27.30  |
                                +----+-------------+--------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every arrow is &lt;code&gt;orders.customer_id&lt;/code&gt; referencing &lt;code&gt;customers.id&lt;/code&gt;. That's the foreign key. It's what makes it possible to reconstruct "this customer, with these orders" without storing the order data twice.&lt;/p&gt;

&lt;p&gt;Now say you want to return every customer along with all the orders they've placed. A common first pass looks something like this (pseudocode, but it maps closely to a naive Prisma or ActiveRecord 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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customers&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM customers&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;customer&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM orders WHERE customer_id = ?&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;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;
  
  
  Why It's Called N+1
&lt;/h2&gt;

&lt;p&gt;This looks reasonable. You get all the customers, then loop through and grab each one's orders. But look at what's actually hitting the database:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One query to fetch all customers&lt;/li&gt;
&lt;li&gt;One additional query per customer to fetch that customer's orders&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you have 100 customers, that's 1 query for the customers plus 100 queries for their orders: 101 total queries. Hence "N+1," where N is the number of rows from the first query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant App
    participant DB

    App-&amp;gt;&amp;gt;DB: SELECT * FROM customers
    DB--&amp;gt;&amp;gt;App: 100 customers

    loop for each of the 100 customers
        App-&amp;gt;&amp;gt;DB: SELECT * FROM orders WHERE customer_id = ?
        DB--&amp;gt;&amp;gt;App: orders for that customer
    end

    Note over App,DB: Total: 1 + 100 = 101 queries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This scales linearly with your data, and not in a good way. At 100 customers, 101 queries might not even show up as a bottleneck. At 100,000 customers, that's 100,001 queries per request, and your database (and your server) will not thank you for it. If this endpoint is something like an admin dashboard or a customer list page that gets hit often, that cost repeats on every single request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: A Single SQL JOIN
&lt;/h2&gt;

&lt;p&gt;Instead of fetching customers and orders separately, you can pull both in one query using a JOIN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns every customer paired with each of their orders in a single result set, one round trip to the database instead of N+1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sequenceDiagram
    participant App
    participant DB

    App-&amp;gt;&amp;gt;DB: SELECT customers.*, orders.* FROM customers LEFT JOIN orders ON orders.customer_id = customers.id
    DB--&amp;gt;&amp;gt;App: all customers with their orders, in one result set

    Note over App,DB: Total: 1 query, regardless of customer count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your application code then just needs to group the flat rows back into a nested structure (customers with an &lt;code&gt;orders&lt;/code&gt; array), which is a cheap in-memory operation compared to hundreds of network round trips to the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Different Frameworks Solve This
&lt;/h2&gt;

&lt;p&gt;Every major ORM has a built-in way to do this join for you, they just name it differently:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;Feature name&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Django&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;select_related&lt;/code&gt; / &lt;code&gt;prefetch_related&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Customer.objects.prefetch_related('orders')&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laravel (Eloquent)&lt;/td&gt;
&lt;td&gt;Eager loading&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Customer::with('orders')-&amp;gt;get()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prisma&lt;/td&gt;
&lt;td&gt;&lt;code&gt;include&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;prisma.customer.findMany({ include: { orders: true } })&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rails (ActiveRecord)&lt;/td&gt;
&lt;td&gt;Eager loading&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Customer.includes(:orders)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Under the hood, they're all doing roughly the same thing: either issuing a single JOIN query, or batching the "get related records" step into one &lt;code&gt;WHERE customer_id IN (...)&lt;/code&gt; query instead of N separate ones. Either approach collapses N+1 queries down to 1 or 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Catch This in Your Own Code
&lt;/h2&gt;

&lt;p&gt;The tricky part about N+1 is that the code reads fine. The problem only shows up when you look at what's actually hitting the database. A few ways to catch it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turn on query logging in development and count how many queries a single request generates. If that number scales with the size of your dataset, that's the smell. A "get all customers" endpoint that fires 1 query with 5 test customers and 51 queries with 50 customers is the tell.&lt;/li&gt;
&lt;li&gt;Most ORMs have a debug or profiling mode (Prisma's query logging, Laravel Debugbar, Django Debug Toolbar) that will show you the exact query count per request.&lt;/li&gt;
&lt;li&gt;Load test with realistic data volumes, not 5 rows in a dev database. N+1 is invisible at small scale and brutal at large scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  It Gets Worse With Nested Relationships
&lt;/h2&gt;

&lt;p&gt;The two table example is the simplest case, but N+1 compounds fast once you add another level of relationships. Say each order also has line items:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customers&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM customers&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;customer&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM orders WHERE customer_id = ?&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;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&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;order&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM order_items WHERE order_id = ?&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;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you're not just paying N+1, you're paying N (customers) plus M (orders per customer) queries, nested inside each other. With 100 customers averaging 5 orders each, that's 1 query for customers, 100 queries for orders, and 500 more queries for order items: 601 queries for what should be 3.&lt;/p&gt;

&lt;p&gt;This is exactly the shape of bug that shows up in GraphQL resolvers too. A resolver for &lt;code&gt;orders&lt;/code&gt; that runs a query per parent &lt;code&gt;customer&lt;/code&gt;, and a resolver for &lt;code&gt;items&lt;/code&gt; that runs a query per parent &lt;code&gt;order&lt;/code&gt;, will happily reproduce N+1 at every level of the graph unless something batches those calls. This is why libraries like &lt;code&gt;dataloader&lt;/code&gt; exist in the GraphQL/Node ecosystem: they collect all the individual "get orders for customer X" calls that fire within a single tick, merge them into one &lt;code&gt;WHERE customer_id IN (1, 2, 3, ...)&lt;/code&gt; query, and hand each caller back its slice of the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Rough Sense of the Cost
&lt;/h2&gt;

&lt;p&gt;Query count is only part of the story, latency per query matters too. If a single query takes even 2ms round trip (a reasonable number for a local or same region database), the difference looks like this as your customer table grows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Customers&lt;/th&gt;
&lt;th&gt;N+1 queries&lt;/th&gt;
&lt;th&gt;Approx. time at 2ms/query&lt;/th&gt;
&lt;th&gt;Join queries&lt;/th&gt;
&lt;th&gt;Approx. time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;101&lt;/td&gt;
&lt;td&gt;~200ms&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~2ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;1,001&lt;/td&gt;
&lt;td&gt;~2s&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~2ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;10,001&lt;/td&gt;
&lt;td&gt;~20s&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~2ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100,000&lt;/td&gt;
&lt;td&gt;100,001&lt;/td&gt;
&lt;td&gt;~200s&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~2ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those numbers are simplified (real databases pipeline and cache some of this), but the trend is the real point: N+1 turns your response time into a function of your data size, while a JOIN keeps it roughly constant regardless of how many customers you have. For something like an order history page or a checkout flow, that's the difference between a page that loads instantly and one that times out under real traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't Forget the Index
&lt;/h2&gt;

&lt;p&gt;A JOIN only stays fast if the foreign key column is indexed. Without an index on &lt;code&gt;orders.customer_id&lt;/code&gt;, the database has to scan the entire &lt;code&gt;orders&lt;/code&gt; table for every customer it's joining against, which can turn your one query fix into a slow one query fix on large tables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_customer_id&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most ORMs create this index automatically when you define a foreign key relationship, but it's worth checking on tables that were set up by hand or migrated from another system, since a missing index here is a common reason a JOIN based fix still feels slow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;N+1 is one of those bugs that doesn't look like a bug. The code is readable, it passes tests, and it works fine with a handful of records. It's only when your data grows that the query count explodes and starts dragging down response times. The fix is almost always the same: replace the loop of individual queries with a single JOIN or your ORM's eager loading feature, and let the database do the work it's actually good at.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>systemdesign</category>
      <category>database</category>
    </item>
    <item>
      <title>What an idempotency key actually does</title>
      <dc:creator>Allen Jones</dc:creator>
      <pubDate>Fri, 21 Aug 2026 00:34:23 +0000</pubDate>
      <link>https://dev.to/allenarduino/what-an-idempotency-key-actually-does-2ban</link>
      <guid>https://dev.to/allenarduino/what-an-idempotency-key-actually-does-2ban</guid>
      <description>&lt;p&gt;A client sends a transfer request. The server debits the sender, credits the receiver, commits, and starts writing the response. The connection drops half a millisecond before the client reads it. The client, seeing no response, does the only reasonable thing a retry policy knows how to do: sends the same request again.&lt;/p&gt;

&lt;p&gt;The server has no idea any of that happened. It just sees a second, perfectly well-formed request to move money. So it does it again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client                          Server
  |                                |
  |----- POST /transfers --------&amp;gt;|
  |                                | debit sender
  |                                | credit receiver
  |                                | commit
  |        X  (connection drops)  |
  |                                |
  | (timeout, no response seen)   |
  |                                |
  |----- POST /transfers --------&amp;gt;|  &amp;lt;-- identical request
  |                                | debit sender  (again)
  |                                | credit receiver  (again)
  |                                | commit
  |&amp;lt;---------- 200 OK -------------|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole problem. Not a rare one either; it's the default behavior of almost every HTTP client library the moment you add a timeout and a retry. The client did nothing wrong. It genuinely cannot tell the difference between "the server never got my request" and "the server got it, handled it, and I just never heard back."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "just check the balance first" doesn't work
&lt;/h2&gt;

&lt;p&gt;The instinct is to guard the transfer with a check: does the sender have enough balance, is the receiver valid, and so on. None of that helps here. The retried request is not invalid. It's a completely legitimate transfer request that happens to be a duplicate of one that already succeeded. Every validation check passes, twice.&lt;/p&gt;

&lt;p&gt;You also can't fix this by making the operation "fast enough" that retries become rare. Networks time out for reasons that have nothing to do with your server's speed: a client-side timeout fires, a proxy in the middle drops the connection, a mobile client loses signal for two seconds. The request already fully executed on your server. The client just never found out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix is a key, not a check
&lt;/h2&gt;

&lt;p&gt;An idempotency key is a value the client generates once per logical operation, before it ever calls the endpoint, and sends on every attempt of that same operation, retries included.&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;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;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;/api/transfers&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;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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&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="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;fromAccount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toAccount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the request fails or times out, the client retries with the same key. Not a new one. The key represents "this transfer," not "this HTTP request."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client                          Server
  |                                |
  | key = "6f2a-...-91"           |
  |----- POST /transfers --------&amp;gt;|
  |     Idempotency-Key: 6f2a     | key seen? no
  |                                | debit sender
  |                                | credit receiver
  |                                | store result under 6f2a
  |        X  (connection drops)  |
  |                                |
  |----- POST /transfers --------&amp;gt;|  &amp;lt;-- same key, same body
  |     Idempotency-Key: 6f2a     | key seen? yes
  |                                | skip transfer logic entirely
  |&amp;lt;---------- 200 OK -------------|  return the stored result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the server, before doing anything with money, check whether that key has been seen before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;idempotency_keys&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;request_hash&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;response_status&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;response_body&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleTransfer&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TransferRequest&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;existing&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM idempotency_keys WHERE key = $1&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;key&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="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;response_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="nx"&gt;result&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;executeTransfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`INSERT INTO idempotency_keys (key, request_hash, response_status, response_body)
     VALUES ($1, $2, $3, $4)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;hashPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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="nx"&gt;result&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;result&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;Second attempt with the same key finds the stored row and returns the original result. It never touches the transfer logic again. The money moves exactly once, no matter how many times the network makes the client ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same key, different payload, is a bug, not a replay
&lt;/h2&gt;

&lt;p&gt;There's a failure mode this simple version misses. What if the same key shows up with a different amount, because of a bug on the client, or someone reusing a key by accident? A naive lookup would happily return the stored response from a completely different transfer and call it correct.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;request_hash&lt;/code&gt; in the table is for. On the second request, before trusting the cached response, compare the hash of the incoming payload against what was stored the first time:&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;incomingHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hashPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;request_hash&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;incomingHash&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;ConflictError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Idempotency key reused with a different request&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;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;response_body&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;A mismatch here should be a hard error, not a silent overwrite in either direction. It means either the client has a bug generating keys, or something worse. Either way, replaying a different transfer under an old key is exactly the kind of thing idempotency exists to prevent, so the check has to be as strict as the thing it's protecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three states, not two
&lt;/h2&gt;

&lt;p&gt;The lookup above only really distinguishes "never seen" from "seen and done." There's a third state hiding in between: seen, but still running. It shows up the moment two attempts overlap in time instead of arriving one after another.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              INSERT reserves the key
                        |
                        v
        +---------------------------+
        |          PENDING          |   response_body IS NULL
        |  (transfer in progress)   |
        +---------------------------+
                        |
          executeTransfer() finishes
                        |
                        v
        +---------------------------+
        |         COMPLETED         |   response_body populated
        |   (safe to replay freely) |
        +---------------------------+

        A different payload hash arriving
        against an existing key, in either
        state above, is neither of these.
        It is a CONFLICT, and should be
        rejected outright.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A lookup that only checks "does a row exist" can't tell a &lt;code&gt;PENDING&lt;/code&gt; row from a &lt;code&gt;COMPLETED&lt;/code&gt; one. Reading a &lt;code&gt;PENDING&lt;/code&gt; row's &lt;code&gt;response_body&lt;/code&gt; gets you &lt;code&gt;NULL&lt;/code&gt;, not "please wait." The read path has to check which state it landed in and behave differently for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  The race no one thinks about until it bites
&lt;/h2&gt;

&lt;p&gt;Everything above assumes the two attempts arrive one after another. They don't have to. A client with an aggressive retry policy can fire the retry before the first attempt has finished, especially if the timeout it's reacting to was on the client side, not because the server was actually slow. Now two requests with the same key are running concurrently, and both can pass the "have I seen this key" check before either has written a row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Time  Request A                    Request B
----  --------------------------   --------------------------
 t0   check key "6f2a"  -&amp;gt; none
 t1                                check key "6f2a"  -&amp;gt; none
 t2   begin transfer
 t3                                begin transfer      &amp;lt;-- !!
 t4   debit sender
 t5                                debit sender         &amp;lt;-- both proceed
 t6   credit receiver
 t7                                credit receiver
 t8   INSERT key "6f2a"  -&amp;gt; ok
 t9                                INSERT key "6f2a"  -&amp;gt; UNIQUE VIOLATION
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both proceed to execute the transfer. The unique constraint on &lt;code&gt;key&lt;/code&gt; will stop the second &lt;code&gt;INSERT&lt;/code&gt; from succeeding, but by then the second request may have already moved money, because the transfer logic and the idempotency check aren't happening atomically. The constraint catches the write collision after the damage, not before it.&lt;/p&gt;

&lt;p&gt;The fix is to reserve the key before doing the work, not after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleTransfer&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TransferRequest&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;incomingHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hashPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`INSERT INTO idempotency_keys (key, request_hash, response_status, response_body)
       VALUES ($1, $2, NULL, NULL)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;incomingHash&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isUniqueViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitForCompletedResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;incomingHash&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;executeTransfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`UPDATE idempotency_keys
     SET response_status = 200, response_body = $2
     WHERE key = $1`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&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;result&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;Re-run the same race with the reservation moved first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Time  Request A                    Request B
----  --------------------------   --------------------------
 t0   INSERT key "6f2a"  -&amp;gt; ok
 t1                                INSERT key "6f2a"  -&amp;gt; UNIQUE VIOLATION
 t2   begin transfer
 t3                                wait for row to complete
 t4   debit sender
 t5   credit receiver
 t6   UPDATE key "6f2a" -&amp;gt; done
 t7                                read completed row -&amp;gt; return result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only one request ever touches the transfer logic. The other one blocks on the database's own uniqueness guarantee, which is enforced by Postgres itself, not by application code hoping nothing runs in between two steps. Whether "wait" means polling the row every hundred milliseconds or something fancier depends on how much concurrency you're actually expecting. For a personal project, polling is a perfectly honest starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency is not a transaction
&lt;/h2&gt;

&lt;p&gt;Worth being precise about this, because the two get blurred together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------------------------+
|                 One HTTP request                    |
|                                                       |
|   +-----------------------------------------------+ |
|   |         Database transaction (ACID)            | |
|   |                                                 | |
|   |   debit sender  --&amp;gt;  credit receiver  --&amp;gt;  ok   | |
|   |   (both happen, or neither does)               | |
|   +-----------------------------------------------+ |
|                                                       |
+-----------------------------------------------------+
                          ^
                          |
        Idempotency key wraps the OUTSIDE of this box,
        making it safe to ask for the whole box again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A database transaction makes a single operation atomic: the debit and the credit inside &lt;code&gt;executeTransfer&lt;/code&gt; either both happen or neither does. Idempotency is a different guarantee, layered on top: it makes repeating the same request from the outside safe, regardless of how many times it's repeated. You need both. The transaction protects the money during one execution. The idempotency key protects the money across retries of that execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keys don't live forever
&lt;/h2&gt;

&lt;p&gt;One thing I glossed over: the &lt;code&gt;idempotency_keys&lt;/code&gt; table grows with every request, forever, if nothing ever removes old rows. Most systems only need a key to stay valid for as long as a client might plausibly still be retrying an hour, maybe twenty-four, not indefinitely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;idempotency_keys&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'24 hours'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run on a schedule, this keeps the table from becoming a second, ever-growing ledger sitting next to the real one. It also means a key can safely be reused after that window closes, which matters if client code ever generates keys from something less random than a UUID, a timestamp plus a user ID, say.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I check now
&lt;/h2&gt;

&lt;p&gt;Before I add a new "money moving" endpoint anywhere, I check:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does the client generate the key &lt;strong&gt;before&lt;/strong&gt; the first attempt, and reuse it on every retry, never a fresh key per retry?&lt;/li&gt;
&lt;li&gt;Is the key reserved with a unique constraint &lt;strong&gt;before&lt;/strong&gt; the operation runs, not just checked and then trusted?&lt;/li&gt;
&lt;li&gt;Does a key collision with a different payload hash &lt;strong&gt;fail loudly&lt;/strong&gt;, instead of silently returning the wrong stored result?&lt;/li&gt;
&lt;li&gt;Does the read path distinguish &lt;strong&gt;pending from completed&lt;/strong&gt;, instead of treating "row exists" as one single state?&lt;/li&gt;
&lt;li&gt;Am I storing the actual response, so a retry gets back the same answer the original call would have given, not just a generic "already done"?&lt;/li&gt;
&lt;li&gt;Do old keys &lt;strong&gt;expire&lt;/strong&gt;, so the table isn't a permanent second ledger?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The transfer logic was never the hard part. Making it safe to ask twice was.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>systemdesign</category>
      <category>idempotency</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Embeddings are just arrays. Retrieval is the work.</title>
      <dc:creator>Allen Jones</dc:creator>
      <pubDate>Thu, 20 Aug 2026 14:56:45 +0000</pubDate>
      <link>https://dev.to/allenarduino/embeddings-are-just-arrays-retrieval-is-the-work-1ebf</link>
      <guid>https://dev.to/allenarduino/embeddings-are-just-arrays-retrieval-is-the-work-1ebf</guid>
      <description>&lt;p&gt;I delayed actually using embeddings because the word sounds like a research paper. It is not. An embedding is a list of numbers that stands in for a piece of text. Same length every time. Nearby lists mean nearby meaning, if the model is any good.&lt;/p&gt;

&lt;p&gt;That is the whole trick. The rest is plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I hold in my head
&lt;/h2&gt;

&lt;p&gt;Take a sentence. Send it through a model. Get back 1536 floats (or 384, or 768; the number is a property of the model, not a law of nature). Store that array next to the original text.&lt;/p&gt;

&lt;p&gt;At query time, embed the question the same way. Compare arrays. Return the texts whose arrays are closest.&lt;/p&gt;

&lt;p&gt;No search index is required to understand this. A nested loop over ten chunks in a test file is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cosineSimilarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&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="nx"&gt;b&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="kr"&gt;number&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Vector length mismatch&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;dot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;magA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;magB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;dot&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nx"&gt;magA&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nx"&gt;magB&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;dot&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;magA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;magB&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;When I could explain that function without looking it up, the libraries stopped feeling like magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two arrays from two different models are not comparable
&lt;/h2&gt;

&lt;p&gt;This one cost me an afternoon. I had a batch of chunks embedded with one model, then switched providers mid experiment and re-embedded only the new chunks with a different model. Same dimension count, coincidentally. The code ran. No errors. The similarity scores were just quietly wrong, every query returning a confident, meaningless top match.&lt;/p&gt;

&lt;p&gt;Two embeddings are only comparable if the same model produced both. Not the same dimension count, the same model, ideally the same version of it. A 1536 length array from one model and a 1536 length array from another are just two arrays that happen to be the same size. They do not share a coordinate system. Cosine similarity between them is a number, but it is not a meaningful one.&lt;/p&gt;

&lt;p&gt;The fix was boring: store the model name and version next to every embedding, and refuse to compare across a mismatch instead of letting it fail silently.&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;type&lt;/span&gt; &lt;span class="nx"&gt;EmbeddingRecord&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;vector&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="nl"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;createdAt&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I change embedding models later, I re-embed everything. There is no partial migration where old and new vectors coexist in the same comparison. That single row of metadata is what makes that decision obvious the next time, instead of a debugging session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chunking is the product decision
&lt;/h2&gt;

&lt;p&gt;The array is only as useful as the text you fed the model. I first embedded whole markdown files. Retrieval returned "the right document" and the wrong paragraph. Then I split on headings, then on overlapping windows of ~400 tokens.&lt;/p&gt;

&lt;p&gt;There is no universal chunk size. There is only: what question will a user ask, and how much surrounding context does an answer need?&lt;/p&gt;

&lt;p&gt;I am treating chunking as a product decision, not a preprocessing detail. The table schema should remember that:&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;type&lt;/span&gt; &lt;span class="nx"&gt;Chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;docId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;headingPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;embedding&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;code&gt;headingPath&lt;/code&gt; has already saved me once, when two chunks were numerically close and only the section title told me which one belonged in the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking at what actually gets retrieved
&lt;/h2&gt;

&lt;p&gt;Before I let anything call an LLM, I wrote a script that does nothing but print the top five matches for a hardcoded question, with their scores, and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;debugRetrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;question&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;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&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;queryVector&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;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;question&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="nx"&gt;chunks&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;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;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;cosineSimilarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryVector&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="nx"&gt;embedding&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="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;k&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="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="nx"&gt;score&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;of&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&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="nx"&gt;headingPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &amp;gt; &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="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;chunk&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="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120&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;Running this against a dozen questions I already knew the answers to caught more bugs than any amount of staring at the ingestion code did. It's how I found a chunking script that was silently producing empty &lt;code&gt;headingPath&lt;/code&gt; arrays for every chunk in one document, because the regex assumed headings started at the beginning of a line and one file used a different line ending. Every chunk still embedded fine. The score still returned a number. Nothing errored. It just made every match from that document indistinguishable from every other match from that document, and I would not have noticed without reading the raw output.&lt;/p&gt;

&lt;p&gt;An LLM sitting on top of bad retrieval will still write a fluent, confident sentence. That is exactly why I don't trust it to tell me the retrieval is broken. I have to look at the arrays and the text myself, before the LLM ever gets involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Brute force works until it doesn't
&lt;/h2&gt;

&lt;p&gt;The nested loop above is O(n), it checks every stored vector against the query vector, every time. For ten chunks that's instant. For a few hundred, still instant. I am not optimizing this yet, on purpose.&lt;/p&gt;

&lt;p&gt;The reason people reach for pgvector's &lt;code&gt;ivfflat&lt;/code&gt; or &lt;code&gt;hnsw&lt;/code&gt; indexes, or a dedicated vector database, is that brute force stops being instant somewhere in the tens or hundreds of thousands of rows, depending on hardware. Those indexes trade a small amount of accuracy (they're approximate nearest neighbor, not exact) for a large amount of speed at that scale.&lt;/p&gt;

&lt;p&gt;I don't have that problem yet. Adding an index before you need one just adds a second thing that can be subtly wrong, on top of the chunking and the model mismatch and everything else already in the pipeline. The honest brute force loop is also the easiest thing to verify by hand, which matters more right now than the query taking two milliseconds instead of two hundred.&lt;/p&gt;

&lt;h2&gt;
  
  
  The embedding call is a network call
&lt;/h2&gt;

&lt;p&gt;Easy to forget, since it's wrapped in a function named something friendly like &lt;code&gt;embed()&lt;/code&gt;. It's an HTTP request to someone else's server. It has latency, it has a rate limit, and it costs a fraction of a cent every time you make it.&lt;/p&gt;

&lt;p&gt;Re-embedding the same chunk twice because a script re-ran is a real way to burn through a rate limit for no reason. The fix is a hash of the chunk's text, stored alongside the embedding:&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;type&lt;/span&gt; &lt;span class="nx"&gt;Chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;docId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;headingPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;embedding&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="nl"&gt;contentHash&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before embedding a chunk, hash its body and compare against what's stored. Same hash, skip the call. Different hash, the text changed, re-embed it. It's a caching problem wearing an AI costume.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am not doing yet
&lt;/h2&gt;

&lt;p&gt;I am not fine-tuning. I am not swapping models every afternoon. I am not building a vector database product. I am not indexing for scale I don't have. I am trying to get one retrieval path honest: same model in, same model out, scores I can explain, chunks a human could have picked, and a debug script I actually run before trusting the output.&lt;/p&gt;

&lt;p&gt;The arrays are the easy part. Making the surrounding system tell the truth is the work.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>machinelearning</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
