<?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: Aman Kumar Singh</title>
    <description>The latest articles on DEV Community by Aman Kumar Singh (@moose978).</description>
    <link>https://dev.to/moose978</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%2F4039092%2Ff2930aaf-8b44-48d7-aeb4-7927c36f735c.png</url>
      <title>DEV Community: Aman Kumar Singh</title>
      <link>https://dev.to/moose978</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/moose978"/>
    <language>en</language>
    <item>
      <title>The Production Readiness Checklist</title>
      <dc:creator>Aman Kumar Singh</dc:creator>
      <pubDate>Tue, 21 Jul 2026 13:08:40 +0000</pubDate>
      <link>https://dev.to/moose978/the-production-readiness-checklist-p4b</link>
      <guid>https://dev.to/moose978/the-production-readiness-checklist-p4b</guid>
      <description>&lt;p&gt;In the &lt;a href="https://amanksingh.com/blog/performance-tuning-before-launch" rel="noopener noreferrer"&gt;last article&lt;/a&gt;, I walked through finding and fixing the slow parts of a SaaS before launch: N+1 queries, missing indexes, unbounded payloads, the usual suspects. That work makes the product fast. It says nothing about whether the product is actually ready to run unattended, at 3am, when something inevitably goes wrong.&lt;/p&gt;

&lt;p&gt;This is the closing article of Module 4 in the &lt;a href="https://amanksingh.com/blog/choosing-the-right-tech-stack-for-saas" rel="noopener noreferrer"&gt;Full Stack SaaS Masterclass&lt;/a&gt;, and it's meant as a checklist you go through before flipping the switch on a real launch, not a tutorial you follow once and forget. Every item on it is something I've seen skipped on a first release, and every skip eventually became an incident.&lt;/p&gt;

&lt;p&gt;"Production ready" doesn't mean gold-plated. It means the gaps that turn a minor bug into a multi-hour outage have been closed, and the ones that remain are gaps you've consciously accepted rather than ones nobody thought about. That distinction is the whole point of a checklist: it forces the conscious part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration and secrets are locked down
&lt;/h2&gt;

&lt;p&gt;The most common first-launch mistake isn't a code bug. It's a config that quietly behaved differently than everyone assumed. A &lt;code&gt;.env&lt;/code&gt; file with production credentials committed by accident. A CORS policy left wide open because it was easier during development. A database connection pool sized for a laptop, not for concurrent traffic.&lt;/p&gt;

&lt;p&gt;Before launch, walk through every environment variable your app reads and confirm three things: it has a real value in production, that value came from a secrets manager rather than a plaintext file in the repo, and there's a documented fallback (or a hard failure) if it's missing.&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;z&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;zod&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;envSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;staging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;CORS_ORIGIN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;SENTRY_DSN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&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;type&lt;/span&gt; &lt;span class="nx"&gt;Env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;envSchema&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Env&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="nx"&gt;envSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid environment configuration:&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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&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="nf"&gt;exit&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="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="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;Wiring this into NestJS's &lt;code&gt;ConfigModule&lt;/code&gt; at bootstrap means a missing or malformed secret fails the deploy. It doesn't fail silently at 2pm on a Tuesday when a customer hits the one code path that touches it. The tradeoff is a slightly stricter local dev setup, since every new environment variable now needs a schema entry. That's a cost worth paying; a schema that drifts from reality is a bug waiting to be discovered by a user, not by CI.&lt;/p&gt;

&lt;p&gt;The pitfall I see most often here is treating staging and production as identical except for the URL. They rarely are. Different rate limits on a third-party API, different database sizes, different feature flags. Document the differences explicitly rather than assuming staging behavior is a preview of production behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Health, readiness, and graceful shutdown
&lt;/h2&gt;

&lt;p&gt;Your orchestrator, whether it's ECS, Kubernetes, or a plain load balancer, needs a reliable way to know if an instance can take traffic. Without it, a deploy can route requests to a container that's still booting, or keep sending traffic to one that's stuck.&lt;/p&gt;

&lt;p&gt;The three things worth having are a liveness check (is the process alive), a readiness check (can it actually serve requests, meaning the database and cache connections are up), and a graceful shutdown handler that stops accepting new work before the process exits.&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;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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enableShutdownHooks&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SIGTERM&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="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="s1"&gt;SIGTERM received, draining connections&lt;/span&gt;&lt;span class="dl"&gt;'&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;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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;bootstrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, a rolling deploy or an autoscaler scaling down can kill an instance mid-request, and the client gets a connection reset instead of a response. The fix costs almost nothing to implement. It removes an entire class of intermittent, hard-to-reproduce errors that show up as noise in your error tracker rather than as a clear signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups exist and, more importantly, restores are tested
&lt;/h2&gt;

&lt;p&gt;Every team says they have backups. Far fewer have actually restored from one. A backup you've never restored is a hypothesis, not a safety net. Automated snapshots from a managed Postgres provider are a reasonable starting point, but they don't protect you from a bad migration that corrupts data quietly over a week, or a schema change that makes an old backup incompatible with current application code.&lt;/p&gt;

&lt;p&gt;The practice worth adopting before launch is a quarterly restore drill: take a recent backup, restore it to a scratch environment, and run your application's smoke tests against it. This surfaces two things people usually get wrong: a restore that silently drops a table due to permission issues, and a restore time that's much longer than anyone assumed, which matters a great deal when you're calculating an acceptable recovery time objective.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_restore &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRATCH_DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--clean&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--if-exists&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-owner&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jobs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="se"&gt;\&lt;/span&gt;
  ./backups/latest.dump
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tradeoff is time. A quarterly drill takes an afternoon and produces nothing customer-visible, which makes it easy to deprioritize against feature work. I'd argue that's exactly why it needs to be scheduled rather than left to "whenever there's time," because that time never quite arrives on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability that answers "what broke" in minutes, not hours
&lt;/h2&gt;

&lt;p&gt;By the time you're running in production, you should already have metrics, structured logs, and error tracking wired up from earlier articles in this module. The readiness question here is narrower: when something breaks, can an on-call engineer go from alert to root cause without pulling in three other people?&lt;/p&gt;

&lt;p&gt;That means alerts are tied to symptoms a customer would notice (elevated error rate, rising latency, a queue backing up) rather than to infrastructure internals nobody can act on at 3am. Logs need to carry a request ID that ties a customer-facing error back to the exact backend trace that produced it. And the on-call rotation needs real access to the dashboards and log queries they'd require, tested before the first real incident rather than during 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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NestMiddleware&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&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;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&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;Response&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="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;RequestIdMiddleware&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="o"&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;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-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;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&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;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-id&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="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-request-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;requestId&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;A pitfall worth naming directly: alert fatigue. A team that gets paged for every 500 error, including expected ones like a client submitting invalid input, learns to ignore alerts within a month. Reserve pages for things that need a human right now, and route everything else to a dashboard someone checks during business hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  A rollback path that doesn't require a hero
&lt;/h2&gt;

&lt;p&gt;Launch day confidence usually comes from "we tested it in staging," which is necessary but not sufficient. The question a production readiness review should force is: if this release breaks something for real users an hour after deploy, what's the actual sequence of commands to get back to the last known good state, and has anyone run that sequence before?&lt;/p&gt;

&lt;p&gt;For a stateless application, rolling back the deployed image is usually fast if your CI/CD pipeline tags releases by commit SHA and your orchestrator supports a one-command rollback. The harder case is a release that shipped alongside a database migration. If the migration is backward compatible, meaning the old application version still works against the new schema, a rollback is safe. If it isn't, you're stuck rolling forward with a fix instead, which takes longer and is riskier under pressure.&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="c1"&gt;-- Backward compatible: old code ignores the new nullable column&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;plan_tier&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Not backward compatible: old code will fail on the NOT NULL constraint&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;organizations&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;plan_tier&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The practical rule is to ship schema changes in two steps: add the column nullable, deploy, backfill, then add the constraint in a later deploy once the old code path is gone. It's more deploys for the same feature, and that's the cost. What you get in exchange is a rollback option that actually works, which matters far more the one time you need it than it costs on every other release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security basics that are easy to defer and expensive to skip
&lt;/h2&gt;

&lt;p&gt;None of these are exotic. Rate limiting on authentication and password reset endpoints, so a credential-stuffing attempt doesn't take down your database. Dependency scanning in CI, so a known vulnerability doesn't sit in production for months because nobody was watching. HTTPS enforced everywhere, including internal service-to-service calls if they cross a network boundary you don't fully control. Least-privilege IAM roles, so a compromised container doesn't have write access to every S3 bucket in the account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GitHub Actions: dependency vulnerability scan on every PR&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security-audit&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;audit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm audit --audit-level=high&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tradeoff with &lt;code&gt;npm audit --audit-level=high&lt;/code&gt; gating a PR is the occasional false positive: a vulnerability in a transitive dependency that doesn't actually apply to how you use the package. That's a legitimate annoyance, and the fix is an explicit, reviewed suppression list, not disabling the check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Config validation that fails fast at boot catches an entire category of "worked in staging, broke in production" incidents before they reach a user.&lt;/li&gt;
&lt;li&gt;Health checks and graceful shutdown remove intermittent errors caused by deploys and autoscaling, not just visible outages.&lt;/li&gt;
&lt;li&gt;A backup nobody has restored is unverified. Schedule restore drills; don't wait for an incident to test them.&lt;/li&gt;
&lt;li&gt;Observability should be built around what a human needs to diagnose an incident in minutes, with alerts tied to customer-visible symptoms.&lt;/li&gt;
&lt;li&gt;A rollback plan is only real if it's been exercised, and backward-compatible migrations are what make rollback possible at all.&lt;/li&gt;
&lt;li&gt;Security basics (rate limiting, dependency scanning, least privilege) are cheap before launch and expensive to retrofit after an incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What does "production ready" actually mean for a SaaS?&lt;/strong&gt;&lt;br&gt;
It means the failure modes that turn a small bug into a long outage have been addressed deliberately: validated configuration, health checks, tested backups, actionable alerts, and a working rollback path. It doesn't mean every edge case is handled; it means the team knows which ones aren't, on purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often should I test my backup restore process?&lt;/strong&gt;&lt;br&gt;
Quarterly is a reasonable baseline for most SaaS products. The exact cadence matters less than making it a scheduled, repeatable drill rather than something that only happens after an incident forces it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should every database migration be backward compatible?&lt;/strong&gt;&lt;br&gt;
Ideally, yes, especially for anything running multiple instances or doing rolling deploys. Splitting a schema change into an additive step and a later cleanup step costs an extra deploy but keeps rollback available if the release causes problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I avoid alert fatigue without missing real incidents?&lt;/strong&gt;&lt;br&gt;
Page only for symptoms a customer would notice: elevated error rates, rising latency, a queue backing up. Route lower-severity signals to a dashboard reviewed during business hours instead of paging for every anomaly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a full incident response process before launching?&lt;/strong&gt;&lt;br&gt;
You need the minimum version: someone is on call, they have access to logs and dashboards, and there's a documented rollback procedure. A formal postmortem template and severity taxonomy can come once you've had a few real incidents to learn from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is a production readiness checklist a one-time gate or an ongoing practice?&lt;/strong&gt;&lt;br&gt;
Treat the first pass as a gate before launch, then revisit the checklist before any major architectural change: a new region, a new data store, a significant traffic increase. The checklist doesn't change much, but what counts as "done" against it does as the system grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous: &lt;a href="https://amanksingh.com/blog/performance-tuning-before-launch" rel="noopener noreferrer"&gt;Performance Tuning Before Launch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Start of series: &lt;a href="https://amanksingh.com/blog/choosing-the-right-tech-stack-for-saas" rel="noopener noreferrer"&gt;Choosing the Right Tech Stack for Your SaaS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Aman Singh&lt;/strong&gt; — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.&lt;/p&gt;

&lt;p&gt;I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://amanksingh.com" rel="noopener noreferrer"&gt;https://amanksingh.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/amansingh1501" rel="noopener noreferrer"&gt;https://github.com/amansingh1501&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product: &lt;a href="https://vowerole.com" rel="noopener noreferrer"&gt;https://vowerole.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email: &lt;a href="mailto:aman97aman@gmail.com"&gt;aman97aman@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>saas</category>
      <category>devops</category>
      <category>backend</category>
    </item>
    <item>
      <title>A Git Workflow for Small Teams</title>
      <dc:creator>Aman Kumar Singh</dc:creator>
      <pubDate>Tue, 21 Jul 2026 08:02:24 +0000</pubDate>
      <link>https://dev.to/moose978/a-git-workflow-for-small-teams-1f7n</link>
      <guid>https://dev.to/moose978/a-git-workflow-for-small-teams-1f7n</guid>
      <description>&lt;p&gt;The last few articles set up the &lt;a href="https://amanksingh.com/blog/fullstack-saas-folder-structure" rel="noopener noreferrer"&gt;folder structure that scales&lt;/a&gt; for a full stack SaaS: where the frontend and backend live, how a monorepo shares types, where config sits. None of that matters much if the team can't move code through Git without stepping on each other. This is the last piece of Module 1 in the &lt;strong&gt;Full Stack SaaS Masterclass&lt;/strong&gt;, the series that started with &lt;a href="https://amanksingh.com/blog/choosing-the-right-tech-stack-for-saas" rel="noopener noreferrer"&gt;choosing the stack&lt;/a&gt; and has been building the foundation ever since.&lt;/p&gt;

&lt;p&gt;Git workflow is one of those topics every team has an opinion on, usually inherited from wherever they worked last. Some of those opinions come from companies with fifty engineers and three release trains a day. Applying that process to a team of two or three people building a SaaS product is a good way to spend more time managing branches than writing features.&lt;/p&gt;

&lt;p&gt;I've settled on a workflow that's boring on purpose. It borrows the useful parts of GitFlow and trunk-based development, drops the ceremony neither needs, and scales down cleanly to a solo founder and up reasonably far before it needs to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trunk-based, not GitFlow
&lt;/h2&gt;

&lt;p&gt;GitFlow gives you &lt;code&gt;develop&lt;/code&gt;, &lt;code&gt;release/*&lt;/code&gt;, &lt;code&gt;hotfix/*&lt;/code&gt;, and long-lived feature branches that merge back through a maze of rules. It was designed for teams shipping infrequently, with formal release cycles and multiple versions in production at once. A SaaS with one production environment and continuous deploys doesn't have that problem, so most of GitFlow's structure is solving something you don't have.&lt;/p&gt;

&lt;p&gt;What I use instead is trunk-based development: one long-lived branch, &lt;code&gt;main&lt;/code&gt;, that is always deployable. Every feature branches off &lt;code&gt;main&lt;/code&gt;, gets reviewed, and merges back into &lt;code&gt;main&lt;/code&gt; within a day or two. There's no &lt;code&gt;develop&lt;/code&gt; branch acting as a staging area, because that staging area just becomes a second place bugs can hide and a second merge conflict waiting to happen.&lt;/p&gt;

&lt;p&gt;The rule that makes this work is short-lived branches. A feature branch that lives for two weeks accumulates drift from &lt;code&gt;main&lt;/code&gt;, and the merge at the end becomes its own project. A feature branch that lives for a day or two barely drifts, so the merge is quick and the diff is reviewable. If a feature is big enough that it can't land in a day or two, that's a signal to break it into smaller, independently mergeable pieces, not a signal to let the branch live longer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout main
git pull origin main
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; feat/invoice-pdf-export
&lt;span class="c"&gt;# work, commit, push&lt;/span&gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin feat/invoice-pdf-export
&lt;span class="c"&gt;# open PR, get it reviewed, merge same day if possible&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Branch naming and what actually goes in a commit
&lt;/h2&gt;

&lt;p&gt;Branch names are cheap to standardize and useful once you have more than one branch open. I use a &lt;code&gt;type/short-description&lt;/code&gt; format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;feat/invoice-pdf-export
fix/tenant-scoping-in-search
chore/upgrade-nestjs-10
refactor/extract-billing-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prefix tells anyone scanning the branch list what kind of change it is before they open a single file. It also maps directly onto commit message conventions, which is where the real payoff shows up.&lt;/p&gt;

&lt;p&gt;I use Conventional Commits for the commit message itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat(billing): add PDF export for invoices"&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"fix(auth): scope refresh token lookup to tenant"&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore(deps): bump nestjs to v10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type and optional scope in parentheses aren't decoration. They're what a tool like semantic-release or changelog generator reads to decide whether a merge is a patch, minor, or major version bump, and what goes in the changelog under which heading. Even if you're not auto-generating changelogs yet, writing commits this way costs nothing and means you can add that tooling later without rewriting history.&lt;/p&gt;

&lt;p&gt;The message itself should describe the why when the diff doesn't make it obvious. "fix(auth): scope refresh token lookup to tenant" tells you what and where. If the fix involved a non-obvious tradeoff, that belongs in the commit body, not just the PR description, because the PR description doesn't travel with &lt;code&gt;git blame&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull requests as the actual quality gate
&lt;/h2&gt;

&lt;p&gt;The pull request, not the branch, is where quality gets enforced. On a small team it's tempting to skip review because "everyone knows what's going on," but a PR does two things that verbal context doesn't: it creates a written record of the decision, and it forces the CI pipeline to run before the code reaches &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A minimal PR template keeps reviews fast because the reviewer isn't reconstructing context from scratch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## What changed&lt;/span&gt;
&lt;span class="gu"&gt;## Why&lt;/span&gt;
&lt;span class="gu"&gt;## How to test&lt;/span&gt;
&lt;span class="gu"&gt;## Screenshots (if UI)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four sections, no essay required. On a two-person team, "why" is often one sentence. That's fine. The point is making sure the next person (including future you) doesn't have to guess.&lt;/p&gt;

&lt;p&gt;Branch protection on &lt;code&gt;main&lt;/code&gt; is worth setting up on day one, not after the first incident caused by a direct push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GitHub branch protection settings (not a workflow file, just documenting intent)&lt;/span&gt;
&lt;span class="na"&gt;required_status_checks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;lint&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;typecheck&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="na"&gt;required_pull_request_reviews&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;required_approving_review_count&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;enforce_admins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a team of two, &lt;code&gt;required_approving_review_count: 1&lt;/code&gt; means you review each other. On a team of one, you can relax that specific rule, but keep the required status checks. CI catching a broken build before it reaches &lt;code&gt;main&lt;/code&gt; doesn't need a second human; it just needs the pipeline to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merge strategy: squash, and why it matters more than it sounds
&lt;/h2&gt;

&lt;p&gt;This is a small decision with an outsized effect on how usable your Git history stays. I squash-merge every PR into &lt;code&gt;main&lt;/code&gt;. The feature branch might have eleven commits, six of which say "wip" or "fix typo," but the squashed commit that lands on &lt;code&gt;main&lt;/code&gt; is one clean entry with the PR's title and description.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# GitHub: set the merge button to "Squash and merge" as the repository default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payoff is that &lt;code&gt;git log --oneline main&lt;/code&gt; reads like a changelog: one line per shipped feature or fix, not a scroll of intermediate commits. When you need to find when something broke, &lt;code&gt;git bisect&lt;/code&gt; walks through meaningful units of change instead of half-finished intermediate states, which makes each bisect step actually testable in isolation.&lt;/p&gt;

&lt;p&gt;The tradeoff is that you lose the granular commit history within a feature branch once it's merged. For a small team, that history was rarely useful after the fact anyway. If you genuinely need fine-grained history for a specific piece of work (a complex migration, say), that's a case for a well-organized set of commits within the PR and a merge commit instead of squash for that one PR, not a reason to abandon squash-merge as the default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hotfixes and the one exception to the branching rule
&lt;/h2&gt;

&lt;p&gt;Trunk-based development assumes &lt;code&gt;main&lt;/code&gt; is always deployable, which holds until it doesn't: a bug ships, it's affecting real users, and you need a fix out now. The instinct to skip process here is understandable and also how a rushed fix introduces a second bug.&lt;/p&gt;

&lt;p&gt;The pattern that holds up is the same one used for every other change, just faster: branch from &lt;code&gt;main&lt;/code&gt;, fix, PR, merge, deploy. The speed comes from urgency and focus, not from skipping review.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout main
git pull origin main
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; fix/expired-token-500
&lt;span class="c"&gt;# minimal, targeted fix&lt;/span&gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin fix/expired-token-500
&lt;span class="c"&gt;# fast review, one approval, merge, deploy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your CI pipeline and deploy process are fast (and for a small SaaS they should be), a hotfix through this path takes minutes, not hours. If it doesn't, that's a sign to invest in deploy speed, not to build a separate emergency process that bypasses review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production pitfalls worth naming explicitly
&lt;/h2&gt;

&lt;p&gt;A few failure modes show up repeatedly on small teams, mostly from copying process that doesn't fit the team's size:&lt;/p&gt;

&lt;p&gt;Long-lived feature branches are the single biggest source of painful merges. If a branch has been open more than a few days, rebase it onto &lt;code&gt;main&lt;/code&gt; regularly rather than letting the drift compound, or split the work into smaller PRs.&lt;/p&gt;

&lt;p&gt;Force-pushing to a shared branch after someone else has pulled it rewrites history out from under them. &lt;code&gt;git push --force-with-lease&lt;/code&gt; on your own feature branch, after a rebase, is fine. Force-pushing &lt;code&gt;main&lt;/code&gt; is not something a small-team workflow should ever need.&lt;/p&gt;

&lt;p&gt;Committing directly to &lt;code&gt;main&lt;/code&gt; "just this once" erodes the one guarantee the whole workflow depends on: that &lt;code&gt;main&lt;/code&gt; only changes through a reviewed PR with passing CI. It only takes one exception for that guarantee to stop being trustworthy.&lt;/p&gt;

&lt;p&gt;Skipping the PR description because "it's a small change" is a habit that costs you exactly when you need it: six months later, trying to understand why a particular line exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Trunk-based development with short-lived feature branches fits small teams better than GitFlow, which solves problems (multiple release trains, long release cycles) most SaaS teams don't have.&lt;/li&gt;
&lt;li&gt;Consistent branch naming (&lt;code&gt;type/description&lt;/code&gt;) and Conventional Commits cost little and pay off the moment you add changelogs or semantic versioning.&lt;/li&gt;
&lt;li&gt;Pull requests are the quality gate, not a formality; branch protection with required status checks enforces that even on a team of one or two.&lt;/li&gt;
&lt;li&gt;Squash-merging keeps &lt;code&gt;main&lt;/code&gt;'s history readable and &lt;code&gt;git bisect&lt;/code&gt; useful, at the cost of granular in-branch history you rarely need later.&lt;/li&gt;
&lt;li&gt;Hotfixes should follow the same reviewed path as every other change, just faster; skipping review under pressure is how a fix becomes a second incident.&lt;/li&gt;
&lt;li&gt;The workflow should scale down to a solo founder and up a fair distance before it needs to change; adding process ahead of team size is the same mistake as adding infrastructure ahead of scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Should a small team use GitFlow or trunk-based development?&lt;/strong&gt;&lt;br&gt;
Trunk-based development, for most small SaaS teams. GitFlow's extra branches (&lt;code&gt;develop&lt;/code&gt;, &lt;code&gt;release/*&lt;/code&gt;, &lt;code&gt;hotfix/*&lt;/code&gt;) solve problems around multiple release trains and long release cycles that a continuously deployed SaaS with one production environment usually doesn't have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the right commit message format for a small team?&lt;/strong&gt;&lt;br&gt;
Conventional Commits (&lt;code&gt;type(scope): description&lt;/code&gt;) works well because it's cheap to adopt and unlocks tooling like automated changelogs and semantic versioning later, without requiring you to rewrite history to get there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I squash-merge or use a merge commit for pull requests?&lt;/strong&gt;&lt;br&gt;
Squash-merge as the default. It keeps &lt;code&gt;main&lt;/code&gt;'s history to one entry per shipped change, which makes &lt;code&gt;git log&lt;/code&gt; and &lt;code&gt;git bisect&lt;/code&gt; far more useful. Use a merge commit only for the rare PR where the internal commit history is genuinely worth preserving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How should a small team handle production hotfixes in Git?&lt;/strong&gt;&lt;br&gt;
The same way as any other change, just faster: branch from &lt;code&gt;main&lt;/code&gt;, make a minimal fix, open a PR, get one review, merge, deploy. Skipping review under pressure is a common way a rushed fix introduces a second bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long should a feature branch stay open before merging?&lt;/strong&gt;&lt;br&gt;
A day or two, ideally. Branches that live longer accumulate drift from &lt;code&gt;main&lt;/code&gt;, which turns the eventual merge into its own project. If a feature can't land in a day or two, break it into smaller, independently mergeable pieces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need branch protection rules on a two-person team?&lt;/strong&gt;&lt;br&gt;
Yes, at least for required status checks (lint, typecheck, test, build). Required review count can be relaxed for a solo founder, but a CI gate that blocks a broken build from reaching &lt;code&gt;main&lt;/code&gt; doesn't need a second human to be worth having.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous: &lt;a href="https://amanksingh.com/blog/fullstack-saas-folder-structure" rel="noopener noreferrer"&gt;A Folder Structure That Scales for Full Stack SaaS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Next: &lt;a href="https://amanksingh.com/blog/jwt-authentication-nestjs" rel="noopener noreferrer"&gt;JWT Authentication in NestJS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Start of series: &lt;a href="https://amanksingh.com/blog/choosing-the-right-tech-stack-for-saas" rel="noopener noreferrer"&gt;Choosing the Right Tech Stack for Your SaaS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Aman Singh&lt;/strong&gt; — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.&lt;/p&gt;

&lt;p&gt;I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://amanksingh.com" rel="noopener noreferrer"&gt;https://amanksingh.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/amansingh1501" rel="noopener noreferrer"&gt;https://github.com/amansingh1501&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product: &lt;a href="https://vowerole.com" rel="noopener noreferrer"&gt;https://vowerole.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email: &lt;a href="mailto:aman97aman@gmail.com"&gt;aman97aman@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>git</category>
      <category>webdev</category>
      <category>saas</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A Folder Structure That Scales for Full Stack SaaS</title>
      <dc:creator>Aman Kumar Singh</dc:creator>
      <pubDate>Tue, 21 Jul 2026 08:02:20 +0000</pubDate>
      <link>https://dev.to/moose978/a-folder-structure-that-scales-for-full-stack-saas-39e6</link>
      <guid>https://dev.to/moose978/a-folder-structure-that-scales-for-full-stack-saas-39e6</guid>
      <description>&lt;p&gt;Your &lt;code&gt;.env&lt;/code&gt; files are validated and out of Git by now, thanks to the last article on &lt;a href="https://amanksingh.com/blog/environment-variables-and-secrets" rel="noopener noreferrer"&gt;managing environment variables and secrets&lt;/a&gt;. The app boots with the right config in every environment. What it doesn't have yet is a shape that survives more than a handful of features.&lt;/p&gt;

&lt;p&gt;This is part of the &lt;a href="https://amanksingh.com/series/full-stack-saas-masterclass" rel="noopener noreferrer"&gt;Full Stack SaaS Masterclass&lt;/a&gt;, a build-it-for-real series that takes a multi-tenant SaaS from an empty folder to production. Today's topic sounds boring compared to auth or payments, but I've seen more codebases rot from folder structure than from any single bad library choice.&lt;/p&gt;

&lt;p&gt;Here's the thing about structure: nobody notices it on day one, when you have three routes and two services. Everybody notices it eighteen months in, when adding a field to an invoice means touching twelve files scattered across four unrelated folders. This article is about setting the shape early enough that it never becomes a rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structure by feature, not by file type
&lt;/h2&gt;

&lt;p&gt;The instinct most engineers start with is grouping by technical role: a &lt;code&gt;controllers/&lt;/code&gt; folder, a &lt;code&gt;services/&lt;/code&gt; folder, a &lt;code&gt;dtos/&lt;/code&gt; folder, a &lt;code&gt;models/&lt;/code&gt; folder. It looks tidy in a diagram. It falls apart the moment your app has more than about five features, because every change to one feature touches four different folders and you spend more time navigating than coding.&lt;/p&gt;

&lt;p&gt;The alternative is grouping by feature, sometimes called vertical slicing. Everything related to &lt;code&gt;invoices&lt;/code&gt; lives under &lt;code&gt;invoices/&lt;/code&gt;: the controller, the service, the DTOs, the entity, the tests. Everything related to &lt;code&gt;organizations&lt;/code&gt; lives under &lt;code&gt;organizations/&lt;/code&gt;. When you add a field to an invoice, you open one folder, not four.&lt;/p&gt;

&lt;p&gt;Here's how that looks inside the NestJS API from &lt;code&gt;apps/api&lt;/code&gt;, which we set up a few articles back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/api/src/
├── modules/
│   ├── invoices/
│   │   ├── dto/
│   │   │   ├── create-invoice.dto.ts
│   │   │   └── update-invoice.dto.ts
│   │   ├── entities/
│   │   │   └── invoice.entity.ts
│   │   ├── invoices.controller.ts
│   │   ├── invoices.service.ts
│   │   ├── invoices.module.ts
│   │   └── invoices.service.spec.ts
│   ├── organizations/
│   │   ├── dto/
│   │   ├── entities/
│   │   ├── organizations.controller.ts
│   │   ├── organizations.service.ts
│   │   └── organizations.module.ts
│   └── auth/
│       ├── guards/
│       ├── strategies/
│       ├── auth.controller.ts
│       ├── auth.service.ts
│       └── auth.module.ts
├── common/
│   ├── decorators/
│   ├── filters/
│   ├── guards/
│   ├── interceptors/
│   └── pipes/
├── config/
│   └── configuration.ts
├── app.module.ts
└── main.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NestJS already nudges you toward this. A module is a feature boundary by design: it declares its own controllers, providers, and imports, and the framework doesn't care how many other modules exist alongside it. Fighting that and going back to a &lt;code&gt;controllers/&lt;/code&gt; folder means fighting the framework's own conventions for no real benefit.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;common/&lt;/code&gt; folder is deliberately small and deliberately generic. It holds things with no feature owner: a global exception filter, a logging interceptor, a &lt;code&gt;@CurrentUser()&lt;/code&gt; decorator. If something belongs to one feature, it goes in that feature's folder even if you think you might reuse it later. Premature sharing creates coupling between features that didn't need to know about each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same idea in Next.js, with the App Router doing half the work
&lt;/h2&gt;

&lt;p&gt;Next.js's App Router structure already forces feature-adjacent thinking because routing is filesystem-based. Route groups, the folders in parentheses, let you organize by area of the product without changing the URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/web/src/app/
├── (marketing)/
│   ├── page.tsx
│   ├── pricing/
│   │   └── page.tsx
│   └── layout.tsx
├── (dashboard)/
│   ├── layout.tsx
│   ├── invoices/
│   │   ├── page.tsx
│   │   ├── [invoiceId]/
│   │   │   └── page.tsx
│   │   └── _components/
│   │       ├── invoice-table.tsx
│   │       └── invoice-status-badge.tsx
│   └── settings/
│       └── page.tsx
├── (auth)/
│   ├── login/
│   │   └── page.tsx
│   └── signup/
│       └── page.tsx
├── api/
│   └── webhooks/
│       └── stripe/
│           └── route.ts
└── layout.tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;(marketing)&lt;/code&gt; group gets its own layout without a header full of app navigation. The &lt;code&gt;(dashboard)&lt;/code&gt; group gets an authenticated layout with a sidebar. Neither prefix appears in the URL: &lt;code&gt;/pricing&lt;/code&gt; and &lt;code&gt;/invoices&lt;/code&gt; both resolve exactly as you'd expect, because parenthesized segments are organizational only.&lt;/p&gt;

&lt;p&gt;The underscore folder, &lt;code&gt;_components/&lt;/code&gt;, is Next.js's way of saying "not a route." Anything under an underscore-prefixed folder is opted out of routing entirely. That's exactly what you want for components, hooks, or utilities that belong to one route but shouldn't accidentally become one. Colocating &lt;code&gt;invoice-table.tsx&lt;/code&gt; next to the &lt;code&gt;invoices/&lt;/code&gt; route it serves means you don't go hunting through a global &lt;code&gt;components/&lt;/code&gt; folder to find what renders a specific page. Only promote a component to a shared location once a second, unrelated route actually needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What earns a place in packages/, and what doesn't
&lt;/h2&gt;

&lt;p&gt;The monorepo article set up &lt;code&gt;apps/&lt;/code&gt; for deployables and &lt;code&gt;packages/&lt;/code&gt; for shared code. Folder structure inside each app is one problem; deciding what crosses that boundary into a shared package is a different one, and it's where I've watched teams overcorrect the most.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;packages/types&lt;/code&gt; folder is an easy call. It has zero business logic, just interfaces, and both apps need the exact same shape for a &lt;code&gt;User&lt;/code&gt; or an &lt;code&gt;Invoice&lt;/code&gt;. A &lt;code&gt;packages/config&lt;/code&gt; folder for shared ESLint and TypeScript config is another easy call, since divergence there is pure friction with no upside.&lt;/p&gt;

&lt;p&gt;Where it gets tempting, and often wrong, is a &lt;code&gt;packages/utils&lt;/code&gt; grab bag. The moment you write a date-formatting helper in the frontend, it's tempting to move it to a shared package "in case the backend needs it too." Resist that until the backend actually needs it. A shared package you maintain speculatively is a tax you pay on every change, since now two apps depend on it and a "small" edit needs a version bump and a rebuild across the graph. Let duplication happen once or twice. If the same logic shows up a third time in two apps, that's real evidence. Moving it out now pays off a debt instead of prepaying one that might never come due.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/types/src/invoice.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Invoice&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;organizationId&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;amountCents&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;currency&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;void&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;dueDate&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;That's the shape both &lt;code&gt;apps/api&lt;/code&gt; and &lt;code&gt;apps/web&lt;/code&gt; import. It has no logic to drift, so there's nothing to keep in sync beyond the field list itself, and TypeScript enforces even that for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production pitfalls worth naming early
&lt;/h2&gt;

&lt;p&gt;A few structural mistakes are easy to make and expensive to unwind once real code depends on them.&lt;/p&gt;

&lt;p&gt;The first is barrel files everywhere: an &lt;code&gt;index.ts&lt;/code&gt; in every folder that re-exports everything inside it. They feel convenient for imports, but they quietly encourage circular dependencies, since it becomes easy for module A to import from module B's barrel, which imports from module A's barrel, without either author noticing. Use barrels sparingly, mainly at package boundaries like &lt;code&gt;packages/types&lt;/code&gt;, not inside every feature folder of the API or the app.&lt;/p&gt;

&lt;p&gt;The second is letting &lt;code&gt;common/&lt;/code&gt; (or a &lt;code&gt;shared/&lt;/code&gt; folder in the frontend) become a dumping ground. If more than a small fraction of your code lives there, the "shared" folder has quietly become an unstructured second app sitting inside your structured one. Revisit it periodically and move anything with a single obvious owner back into that feature's folder.&lt;/p&gt;

&lt;p&gt;The third is inconsistency between the two apps. If &lt;code&gt;apps/api&lt;/code&gt; groups by feature but &lt;code&gt;apps/web&lt;/code&gt; groups by technical type, every engineer pays a small tax switching mental models depending on which side of the stack they're touching. Picking the same organizing principle on both sides, feature first, keeps the switching cost near zero.&lt;/p&gt;

&lt;p&gt;The fourth, specific to NestJS, is a module that imports half the app because a controller in &lt;code&gt;invoices&lt;/code&gt; needs one small piece of logic from &lt;code&gt;organizations&lt;/code&gt;. That's usually a sign the boundary is drawn in the wrong place, or that the shared piece belongs in a smaller, more focused shared module rather than a direct cross-import between two big feature modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Group code by feature (vertical slices), not by technical layer, on both the NestJS API and the Next.js frontend. It matches how features actually get changed.&lt;/li&gt;
&lt;li&gt;NestJS modules are a feature boundary by design; let each module own its controller, service, DTOs, and entities.&lt;/li&gt;
&lt;li&gt;Next.js route groups (&lt;code&gt;(name)&lt;/code&gt;) organize by area of the product without changing the URL; underscore folders (&lt;code&gt;_components&lt;/code&gt;) opt out of routing for colocated code.&lt;/li&gt;
&lt;li&gt;Only promote code into &lt;code&gt;packages/&lt;/code&gt; once real duplication shows up twice or more; a speculative shared package is a tax on every future change.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;common/&lt;/code&gt;/&lt;code&gt;shared/&lt;/code&gt; folders small and generic; if they grow large, that's a sign features are being drawn in the wrong place.&lt;/li&gt;
&lt;li&gt;Use the same organizing principle (feature-first) across both apps so engineers don't switch mental models when moving between frontend and backend.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Should I structure a NestJS project by feature or by layer?&lt;/strong&gt;&lt;br&gt;
By feature. Group each feature's controller, service, DTOs, and entity together in one module folder. A &lt;code&gt;controllers/&lt;/code&gt;, &lt;code&gt;services/&lt;/code&gt;, &lt;code&gt;dtos/&lt;/code&gt; split by technical layer looks organized at first but forces every feature change to touch multiple unrelated folders as the app grows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between &lt;code&gt;packages/&lt;/code&gt; and &lt;code&gt;apps/common&lt;/code&gt;/&lt;code&gt;shared&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;packages/&lt;/code&gt; is for code shared across separately deployed apps in the monorepo, like &lt;code&gt;packages/types&lt;/code&gt; used by both the API and the frontend. A &lt;code&gt;common/&lt;/code&gt; folder inside a single app is for cross-feature code within that one app, like a global exception filter or logging interceptor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do Next.js route groups work, and do they affect the URL?&lt;/strong&gt;&lt;br&gt;
A folder wrapped in parentheses, like &lt;code&gt;(dashboard)&lt;/code&gt;, groups routes and can hold a shared layout, but the parenthesized name never appears in the URL. &lt;code&gt;(dashboard)/invoices/page.tsx&lt;/code&gt; still resolves to &lt;code&gt;/invoices&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When should I move code into a shared package instead of duplicating it?&lt;/strong&gt;&lt;br&gt;
Once the same logic shows up in two apps for the third time, not the first. Two occurrences are often coincidence; a third is a real pattern. Moving code out too early creates a shared dependency you now have to version and rebuild for every small change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a barrel file, and why can it cause circular dependency issues?&lt;/strong&gt;&lt;br&gt;
A barrel file is an &lt;code&gt;index.ts&lt;/code&gt; that re-exports everything in a folder. Overusing them across feature folders makes it easy for two modules to import from each other's barrels indirectly, creating circular dependencies that are hard to trace back to their source. Keep barrels mainly at package boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know my folder structure has stopped scaling?&lt;/strong&gt;&lt;br&gt;
A common signal is a single small feature change touching more than two or three folders, or a &lt;code&gt;common&lt;/code&gt;/&lt;code&gt;shared&lt;/code&gt; folder that's grown as large as your actual feature folders. Both mean the boundaries need revisiting before the next feature makes it worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous: &lt;a href="https://amanksingh.com/blog/environment-variables-and-secrets" rel="noopener noreferrer"&gt;Managing Environment Variables and Secrets&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Next: &lt;a href="https://amanksingh.com/blog/git-workflow-for-small-teams" rel="noopener noreferrer"&gt;A Git Workflow for Small Teams&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Start of series: &lt;a href="https://amanksingh.com/blog/choosing-the-right-tech-stack-for-saas" rel="noopener noreferrer"&gt;Choosing the Right Tech Stack for Your SaaS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Aman Singh&lt;/strong&gt; — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.&lt;/p&gt;

&lt;p&gt;I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://amanksingh.com" rel="noopener noreferrer"&gt;https://amanksingh.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/amansingh1501" rel="noopener noreferrer"&gt;https://github.com/amansingh1501&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product: &lt;a href="https://vowerole.com" rel="noopener noreferrer"&gt;https://vowerole.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email: &lt;a href="mailto:aman97aman@gmail.com"&gt;aman97aman@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>nestjs</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Managing Environment Variables and Secrets</title>
      <dc:creator>Aman Kumar Singh</dc:creator>
      <pubDate>Tue, 21 Jul 2026 07:57:11 +0000</pubDate>
      <link>https://dev.to/moose978/managing-environment-variables-and-secrets-342f</link>
      <guid>https://dev.to/moose978/managing-environment-variables-and-secrets-342f</guid>
      <description>&lt;p&gt;Last time, in &lt;a href="https://amanksingh.com/blog/docker-local-development-setup" rel="noopener noreferrer"&gt;Setting Up Docker for Local Development&lt;/a&gt;, we got Postgres and Redis running the same way on every machine on the team. That solved "works on my machine" for the infrastructure. It didn't solve the other half of the problem: every one of those services needs credentials, and those credentials need to be different in local dev, staging, and production without anyone copy-pasting a database password into a Slack message.&lt;/p&gt;

&lt;p&gt;This is part of the &lt;strong&gt;Full Stack SaaS Masterclass&lt;/strong&gt;, the series where we're building a multi-tenant SaaS from an &lt;a href="https://amanksingh.com/blog/choosing-the-right-tech-stack-for-saas" rel="noopener noreferrer"&gt;empty repo&lt;/a&gt; to something production-ready. Environment variables feel like the most boring topic in the whole series, and that's exactly why teams get them wrong. Nobody schedules time to think about config until a secret leaks into a git history or a staging database gets pointed at by a production deploy.&lt;/p&gt;

&lt;p&gt;You don't need a giant secrets-management platform on day one. A handful of habits, applied consistently, scale fine from a solo project to a team of engineers, and they keep you from being one careless &lt;code&gt;git add .&lt;/code&gt; away from a real incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why config deserves its own discipline
&lt;/h2&gt;

&lt;p&gt;Every real app has values that change between environments: database URLs, API keys, JWT secrets, third-party credentials, feature flags. The twelve-factor app methodology calls this out directly: config should live in the environment, not in the code, because code is the same across every deploy and config is not. If your &lt;code&gt;DATABASE_URL&lt;/code&gt; is hardcoded, you can't run the same build against staging and production without editing source files, which defeats the entire point of having a build artifact.&lt;/p&gt;

&lt;p&gt;The second reason is a security one. Code gets reviewed, diffed, and often made public. Secrets don't get that scrutiny in the same way, and they shouldn't be visible in the same places code is. Once a secret is committed to git, it's in history forever unless you rewrite it. By then it's usually already been rotated in panic, not by plan.&lt;/p&gt;

&lt;p&gt;The third reason is less obvious: config bugs are some of the hardest to debug precisely because the code is correct. A missing &lt;code&gt;REDIS_URL&lt;/code&gt; in staging doesn't throw a compile error. It throws a runtime error three requests deep into a job queue, at 2am, after the person who set up staging has moved teams. Treating configuration as a first-class, validated part of your app instead of an afterthought is what prevents that class of bug entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate config at startup, not at first use
&lt;/h2&gt;

&lt;p&gt;The single highest-leverage habit here is validating every environment variable when the app boots, not the first time some deep service tries to read &lt;code&gt;process.env.STRIPE_SECRET_KEY&lt;/code&gt; and gets &lt;code&gt;undefined&lt;/code&gt;. Fail fast, with a clear error, before the app accepts a single request.&lt;/p&gt;

&lt;p&gt;In NestJS this slots naturally into &lt;code&gt;ConfigModule&lt;/code&gt;. Instead of &lt;code&gt;forRoot({ isGlobal: true })&lt;/code&gt; with no validation, define a schema and pass it in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/config/env.validation.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&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;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;envSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;staging&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coerce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&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="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk_&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;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EnvConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;envSchema&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;validateEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;EnvConfig&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="nx"&gt;envSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&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;issues&lt;/span&gt; &lt;span class="o"&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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&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;issue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;issue&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Invalid environment configuration:\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;issues&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="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="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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app.module.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Module&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ConfigModule&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/config&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;validateEnv&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;./config/env.validation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;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;ConfigModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;isGlobal&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;validate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;validateEnv&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="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;With this in place, a missing &lt;code&gt;JWT_SECRET&lt;/code&gt; or a malformed &lt;code&gt;DATABASE_URL&lt;/code&gt; crashes the app on boot with a message that names the exact variable, not a stack trace from a random service two weeks later. That trade is almost always worth it: a loud failure in CI or on deploy beats a silent one in production.&lt;/p&gt;

&lt;p&gt;The same idea applies on the frontend, though Next.js handles it differently since env vars there get baked into the client bundle at build time if you're not careful. Anything prefixed &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; ships to the browser. Everything else stays server-only. Keep a small, explicit list of what's actually public:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/web/src/env.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publicEnv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;apiUrl&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;NEXT_PUBLIC_API_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;stripePublishableKey&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;NEXT_PUBLIC_STRIPE_PUBLISHABLE_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;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;key&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="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;publicEnv&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;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;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Missing required public env var for: &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="s2"&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="k"&gt;export&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;publicEnv&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Naming the boundary explicitly like this means nobody accidentally prefixes a real secret with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; just because it was convenient during a late-night fix. That single prefix decides whether a value ends up in a browser's network tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local development: .env files done right
&lt;/h2&gt;

&lt;p&gt;For local dev, a &lt;code&gt;.env&lt;/code&gt; file loaded by &lt;code&gt;dotenv&lt;/code&gt; (or NestJS's built-in loader) is still the right tool. The discipline is in what goes in git and what doesn't.&lt;/p&gt;

&lt;p&gt;Commit an &lt;code&gt;.env.example&lt;/code&gt; with every variable name and a placeholder or safe default, never a real value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env.example&lt;/span&gt;
&lt;span class="nv"&gt;NODE_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;development
&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql://postgres:postgres@localhost:5432/saas_dev
&lt;span class="nv"&gt;REDIS_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redis://localhost:6379
&lt;span class="nv"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;replace-with-a-long-random-string
&lt;span class="nv"&gt;STRIPE_SECRET_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk_test_replace_me
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ignore the real one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .gitignore&lt;/span&gt;
.env
.env.local
.env.&lt;span class="k"&gt;*&lt;/span&gt;.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a new engineer joins, the onboarding step is &lt;code&gt;cp .env.example .env&lt;/code&gt; and fill in real local values, most of which come straight from the Docker Compose file we set up previously. &lt;code&gt;.env.example&lt;/code&gt; doubles as documentation: anyone can read it and know exactly what the app needs to run, without grepping the codebase for every &lt;code&gt;process.env&lt;/code&gt; reference.&lt;/p&gt;

&lt;p&gt;One habit worth adding here: run a pre-commit check (a simple secret-scanning tool works fine) that blocks a commit containing anything that looks like a live key. It catches the case where &lt;code&gt;.env&lt;/code&gt; gets accidentally un-ignored, or someone pastes a real key into a config file while debugging and forgets to remove it before committing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets in staging and production
&lt;/h2&gt;

&lt;p&gt;Local &lt;code&gt;.env&lt;/code&gt; files don't belong anywhere near a deployed environment. Once you're off a laptop, the secret needs to live somewhere with access control, an audit trail, and rotation support. A plaintext file gives you none of that.&lt;/p&gt;

&lt;p&gt;On AWS, that's Secrets Manager or SSM Parameter Store, both of which integrate cleanly with ECS, Lambda, and EC2. The pattern is the same regardless of which one you pick: the secret's value never sits in your deploy config or CI logs, only its identifier does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/config/secrets.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;SecretsManagerClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;GetSecretValueCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/client-secrets-manager&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;client&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;SecretsManagerClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;region&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;AWS_REGION&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretId&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="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;response&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;client&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetSecretValueCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;SecretId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;secretId&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SecretString&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="s2"&gt;`Secret &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;secretId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has no string value`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SecretString&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 practice, you'd call this once during bootstrap, before &lt;code&gt;ConfigModule&lt;/code&gt; validates anything, and inject the resolved values into &lt;code&gt;process.env&lt;/code&gt; or directly into your config object. The app code never knows or cares whether a value came from a local &lt;code&gt;.env&lt;/code&gt; file or Secrets Manager. That's the point: the same validated config shape works in both places, only the source changes.&lt;/p&gt;

&lt;p&gt;For CI/CD, GitHub Actions repository and environment secrets cover most needs. Scope secrets to the environment that needs them (production secrets shouldn't be readable from a PR build), and never &lt;code&gt;echo&lt;/code&gt; a secret in a workflow step for debugging. GitHub masks known secret values in logs, but that masking only works for exact string matches, so a secret that gets base64-encoded or partially transformed before printing slips right through the mask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls worth naming directly
&lt;/h2&gt;

&lt;p&gt;A few mistakes show up often enough to call out on their own.&lt;/p&gt;

&lt;p&gt;Logging the entire &lt;code&gt;process.env&lt;/code&gt; object "just to check config" is a common one, usually left in during debugging and forgotten. It's an easy way to leak every secret the process holds into a log aggregator that a much wider set of people can read than the codebase itself.&lt;/p&gt;

&lt;p&gt;Reusing the same secret across environments is another. If staging and production share a &lt;code&gt;JWT_SECRET&lt;/code&gt;, a token issued by one is valid on the other, which turns a staging compromise into a production one.&lt;/p&gt;

&lt;p&gt;Long-lived secrets that never rotate are a slow accumulation of risk. Every engineer who's ever had access to a secret, and every system that's ever logged it by accident, remains a potential exposure point for as long as that secret stays valid. Rotation doesn't need to be constant, but it should be possible without a redeploy freeze, which is itself a good test of whether your secrets architecture is sound.&lt;/p&gt;

&lt;p&gt;And a subtler one: config drift between environments, where staging quietly ends up with a different flag setting than production because someone made a one-off change and never wrote it down. A validated schema, checked into source control even though the values aren't, keeps every environment honest about what it needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Config belongs in the environment, not in code, per the twelve-factor model, and secrets deserve stricter handling than ordinary config.&lt;/li&gt;
&lt;li&gt;Validate every environment variable at startup with a schema (Zod, Joi, or similar) so missing or malformed config fails loudly before the app serves traffic.&lt;/li&gt;
&lt;li&gt;Commit &lt;code&gt;.env.example&lt;/code&gt; with placeholder values; never commit a real &lt;code&gt;.env&lt;/code&gt;. Add it to &lt;code&gt;.gitignore&lt;/code&gt; from day one.&lt;/li&gt;
&lt;li&gt;On the frontend, treat the &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; prefix as a hard security boundary between server-only and browser-visible values.&lt;/li&gt;
&lt;li&gt;Use a managed secrets store (Secrets Manager, SSM Parameter Store) in staging and production instead of plaintext files or CI variables alone.&lt;/li&gt;
&lt;li&gt;Never reuse secrets across environments, and design for rotation from the start rather than bolting it on after an incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between an environment variable and a secret?&lt;/strong&gt;&lt;br&gt;
An environment variable is any config value that changes between environments, like a port number or a feature flag. A secret is a specific kind of environment variable whose exposure causes harm, such as an API key or database password. Every secret is an environment variable, but not every environment variable needs secret-level handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I use dotenv in production?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;dotenv&lt;/code&gt; and similar loaders are fine for local development, where the file lives only on your machine. In staging and production, prefer a managed secrets store or your platform's native secret injection (ECS task definitions, Kubernetes secrets, GitHub Actions environment secrets) so values are access-controlled and auditable rather than sitting in a plaintext file on a server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I stop secrets from leaking into Next.js client bundles?&lt;/strong&gt;&lt;br&gt;
Only prefix a variable with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; if it's genuinely safe to expose in the browser, such as a publishable API key. Keep everything else unprefixed and read it only in server components, API routes, or server actions. Treat the prefix as an explicit, reviewed decision rather than a naming convention you apply casually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What should happen when a secret leaks into git history?&lt;/strong&gt;&lt;br&gt;
Rotate the secret immediately; assume it's compromised the moment it's committed, regardless of whether the repo is public. Removing it from git history (with tools like &lt;code&gt;git filter-repo&lt;/code&gt;) matters for hygiene, but it doesn't undo the exposure, since the value may already be cached or cloned elsewhere. Rotation is the step that actually closes the risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How often should secrets be rotated?&lt;/strong&gt;&lt;br&gt;
There's no universal schedule; it depends on the sensitivity of the secret and how it's used. What matters more than a fixed cadence is that rotation is possible without downtime or a redeploy freeze. If rotating a database password or JWT signing key requires a maintenance window, that's a sign the architecture needs work, independent of how often you actually do it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a secrets manager for a small side project?&lt;/strong&gt;&lt;br&gt;
Probably not on day one. A well-ignored &lt;code&gt;.env&lt;/code&gt; file and platform-native secrets (like your host's environment variable settings) are enough until you have a team, multiple environments, or compliance requirements. Add a dedicated secrets manager when the coordination cost of plaintext values across people and environments starts to outweigh the setup cost of the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous: &lt;a href="https://amanksingh.com/blog/docker-local-development-setup" rel="noopener noreferrer"&gt;Setting Up Docker for Local Development&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Next: &lt;a href="https://amanksingh.com/blog/fullstack-saas-folder-structure" rel="noopener noreferrer"&gt;A Folder Structure That Scales for Full Stack SaaS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Start of series: &lt;a href="https://amanksingh.com/blog/choosing-the-right-tech-stack-for-saas" rel="noopener noreferrer"&gt;Choosing the Right Tech Stack for Your SaaS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Aman Singh&lt;/strong&gt; — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.&lt;/p&gt;

&lt;p&gt;I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://amanksingh.com" rel="noopener noreferrer"&gt;https://amanksingh.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/amansingh1501" rel="noopener noreferrer"&gt;https://github.com/amansingh1501&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product: &lt;a href="https://vowerole.com" rel="noopener noreferrer"&gt;https://vowerole.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email: &lt;a href="mailto:aman97aman@gmail.com"&gt;aman97aman@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>security</category>
      <category>node</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>Setting Up Docker for Local Development</title>
      <dc:creator>Aman Kumar Singh</dc:creator>
      <pubDate>Tue, 21 Jul 2026 07:57:09 +0000</pubDate>
      <link>https://dev.to/moose978/setting-up-docker-for-local-development-3n4e</link>
      <guid>https://dev.to/moose978/setting-up-docker-for-local-development-3n4e</guid>
      <description>&lt;p&gt;The &lt;a href="https://amanksingh.com/blog/prisma-or-typeorm-nestjs" rel="noopener noreferrer"&gt;Prisma or TypeORM&lt;/a&gt; article settled how the API talks to Postgres. What's still missing is the thing that makes all of it reproducible. A way for anyone on the team, including future you on a new laptop, to run the whole stack with one command and get the exact same result.&lt;/p&gt;

&lt;p&gt;That's what this article is about. We already reached for Docker to run Postgres in the &lt;a href="https://amanksingh.com/blog/postgresql-setup-guide" rel="noopener noreferrer"&gt;PostgreSQL setup&lt;/a&gt;, so the container habit is half-formed already. Here's what we add: a &lt;code&gt;docker-compose.yml&lt;/code&gt; that runs the API, the web app, Postgres, and Redis together. Dockerfiles that support fast local iteration without turning into a production liability. And the handful of pitfalls that waste the most time when a team first containerizes its dev environment.&lt;/p&gt;

&lt;p&gt;This is part six of the &lt;a href="https://amanksingh.com/series/full-stack-saas-masterclass" rel="noopener noreferrer"&gt;Full Stack SaaS Masterclass&lt;/a&gt;, continuing from the data-access layer we picked last time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why containerize local development at all
&lt;/h2&gt;

&lt;p&gt;Docker in local dev doesn't need to match production infrastructure. Your production deployment (ECS, Kubernetes, whatever you land on later in the series) will look different from a laptop compose file regardless. The real case for it is narrower and more practical: it removes an entire category of setup problems.&lt;/p&gt;

&lt;p&gt;Without containers, "clone the repo and run it" quietly depends on whatever's already installed on that machine. A specific Node version. A specific Postgres major version. A Redis binary that may or may not exist. Whatever environment quirks have accumulated over years on that developer's laptop. Every one of those is a source of "works on my machine" bugs that have nothing to do with the code.&lt;/p&gt;

&lt;p&gt;With containers, the dependency list shrinks to Docker itself. The API doesn't care whether the host has Node 18 or Node 22 installed, because the container brings its own. A new hire runs &lt;code&gt;docker compose up&lt;/code&gt; and has a working Postgres, Redis, API, and frontend in minutes, without touching a version manager or an install guide.&lt;/p&gt;

&lt;p&gt;The tradeoff is real and worth naming honestly. Containers add a layer of indirection: file changes go through a bind mount, networking goes through Docker's internal DNS instead of &lt;code&gt;localhost&lt;/code&gt; in the way you're used to, and debugging occasionally means reasoning about the container boundary instead of just your code. For a small team building fast, that cost is worth paying once setup pain starts showing up as time lost to environment drift rather than actual bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The docker-compose.yml for the whole stack
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;postgres&lt;/code&gt; service from the previous article stays as it was. Here's the full file with Redis and both apps added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;saas&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local_dev_only&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;saas_dev&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5432:5432'&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;saas'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;6379:6379'&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redisdata:/data&lt;/span&gt;

  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/api/Dockerfile&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;development&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apps/api/.env&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3000:3000'&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./apps/api:/app/apps/api&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./packages:/app/packages&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api_node_modules:/app/node_modules&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_started&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run start:dev&lt;/span&gt;

  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/web/Dockerfile&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;development&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apps/web/.env&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3001:3000'&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./apps/web:/app/apps/web&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./packages:/app/packages&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;web_node_modules:/app/node_modules&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run dev&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;redisdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api_node_modules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web_node_modules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few decisions in there are doing real work. The &lt;code&gt;healthcheck&lt;/code&gt; on Postgres plus &lt;code&gt;depends_on.condition: service_healthy&lt;/code&gt; on the API means the API waits for Postgres to actually accept connections, not just for the container to start. Those are different things, and the gap between them is a classic source of flaky first-run errors. The named volumes for &lt;code&gt;node_modules&lt;/code&gt; exist specifically to avoid mounting the host's &lt;code&gt;node_modules&lt;/code&gt; into the container, which we'll get to next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dockerfiles built for iteration, not just shipping
&lt;/h2&gt;

&lt;p&gt;The same Dockerfile ends up serving two different jobs: fast local iteration and a lean production image. Multi-stage builds let one file do both without compromise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# apps/api/Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; packages ./packages&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; apps/api/package.json ./apps/api/package.json&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;development&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; apps/api ./apps/api&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["npm", "run", "start:dev"]&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;build&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; apps/api ./apps/api&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build &lt;span class="nt"&gt;--workspace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;apps/api

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; NODE_ENV=production&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/apps/api/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/apps/api/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=build /app/apps/api/package.json ./package.json&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/main.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;development&lt;/code&gt; target intentionally uses &lt;code&gt;npm install&lt;/code&gt; instead of &lt;code&gt;npm ci&lt;/code&gt; and skips the build step entirely. It's optimized for the compose file overwriting &lt;code&gt;/app/apps/api&lt;/code&gt; with a live bind mount and running &lt;code&gt;start:dev&lt;/code&gt; with a file watcher. The &lt;code&gt;production&lt;/code&gt; target does the opposite: a clean &lt;code&gt;npm ci&lt;/code&gt;, a real build, and only the compiled output copied into a fresh, smaller base image. Development speed and production leanness pull in different directions, so give them different targets rather than compromising on one file that does neither well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The node_modules volume trick, and why it matters
&lt;/h2&gt;

&lt;p&gt;Bind-mounting your whole app directory into a container is what gives you hot reload: edit a file on the host, the container sees the change instantly. The problem is &lt;code&gt;node_modules&lt;/code&gt;. If you bind-mount the project root as-is, the container's &lt;code&gt;node_modules&lt;/code&gt; (built for the container's OS and architecture) gets shadowed by whatever &lt;code&gt;node_modules&lt;/code&gt; exists on the host, which was likely installed for a different platform, especially painful on Apple Silicon Macs installing packages with native bindings.&lt;/p&gt;

&lt;p&gt;The fix is the named volume already in the compose file above: mount the source directory, then mount an anonymous or named volume specifically over &lt;code&gt;/app/node_modules&lt;/code&gt; so it takes precedence over anything the bind mount would otherwise put there. Docker resolves the more specific mount path last, so &lt;code&gt;node_modules&lt;/code&gt; keeps living inside the container's own filesystem while everything else stays live-editable from the host.&lt;/p&gt;

&lt;p&gt;Skipping this step is the single most common reason a team's "just run &lt;code&gt;docker compose up&lt;/code&gt;" onboarding breaks on the first new machine that tries it. The error is usually a cryptic native-module message that has nothing to do with the actual code change someone made.&lt;/p&gt;

&lt;h2&gt;
  
  
  Networking, environment variables, and the localhost trap
&lt;/h2&gt;

&lt;p&gt;Inside the compose network, services reach each other by service name, not &lt;code&gt;localhost&lt;/code&gt;. The API's &lt;code&gt;DATABASE_URL&lt;/code&gt; inside the container should point at &lt;code&gt;postgres:5432&lt;/code&gt;, not &lt;code&gt;localhost:5432&lt;/code&gt;, because &lt;code&gt;localhost&lt;/code&gt; inside a container refers to the container itself, and there's no Postgres running there.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# apps/api/.env used by the api service
&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;=&lt;span class="n"&gt;postgresql&lt;/span&gt;://&lt;span class="n"&gt;saas&lt;/span&gt;:&lt;span class="n"&gt;local_dev_only&lt;/span&gt;@&lt;span class="n"&gt;postgres&lt;/span&gt;:&lt;span class="m"&gt;5432&lt;/span&gt;/&lt;span class="n"&gt;saas_dev&lt;/span&gt;
&lt;span class="n"&gt;REDIS_URL&lt;/span&gt;=&lt;span class="n"&gt;redis&lt;/span&gt;://&lt;span class="n"&gt;redis&lt;/span&gt;:&lt;span class="m"&gt;6379&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This trips people up constantly the first time, especially since the exact same &lt;code&gt;.env&lt;/code&gt; file, when running the API directly on the host without Docker, needs &lt;code&gt;localhost&lt;/code&gt; instead. Keep a &lt;code&gt;.env.docker&lt;/code&gt; and a &lt;code&gt;.env.local&lt;/code&gt; (or similar) if the team moves between both modes, and be explicit in the README about which one applies where. The failure mode otherwise is a connection refused error that looks like a Postgres problem and is actually a networking-context problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production pitfalls to avoid
&lt;/h2&gt;

&lt;p&gt;A few mistakes are easy to make once Docker is in the daily workflow, and none of them show up until later.&lt;/p&gt;

&lt;p&gt;Don't reuse the development compose file for anything resembling production. It bind-mounts source code, runs a file watcher, and has no resource limits. Production needs its own compose file or, more likely by the time you get there, a real orchestrator, which we cover later in the Cloud &amp;amp; DevOps phase of the series.&lt;/p&gt;

&lt;p&gt;Don't let the image grow unbounded. A &lt;code&gt;.dockerignore&lt;/code&gt; that excludes &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;.git&lt;/code&gt;, and build artifacts keeps build context small and build times sane; forgetting it is a quiet tax on every single build.&lt;/p&gt;

&lt;p&gt;Don't skip &lt;code&gt;depends_on&lt;/code&gt; conditions and assume startup order is deterministic. Compose starts containers in dependency order, but "started" and "ready to accept connections" are different states. Skip the healthcheck and you get an API that crash-loops for the first few seconds of every &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally, watch Docker Desktop's resource allocation on macOS and Windows. A stack running Postgres, Redis, and two Node apps with file watchers active can genuinely strain the default memory and CPU limits. The symptom is usually "everything feels slow" rather than an obvious error, which makes it easy to blame the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Containerizing local dev removes environment drift, not because it mirrors production exactly, but because it collapses the dependency list down to Docker itself.&lt;/li&gt;
&lt;li&gt;One compose file can run Postgres, Redis, the API, and the frontend together, with healthchecks controlling real startup order rather than container-start order.&lt;/li&gt;
&lt;li&gt;Multi-stage Dockerfiles let one file serve fast local iteration and a lean production build without compromising either.&lt;/li&gt;
&lt;li&gt;Mount a named volume over &lt;code&gt;node_modules&lt;/code&gt; inside the container so a host-installed &lt;code&gt;node_modules&lt;/code&gt; never shadows the container's own dependencies.&lt;/li&gt;
&lt;li&gt;Inside the compose network, services address each other by service name, not &lt;code&gt;localhost&lt;/code&gt;; keep separate env files for in-container and on-host runs.&lt;/li&gt;
&lt;li&gt;Never point the dev compose file at production. Its bind mounts and lack of resource limits make it unsafe for anything but local iteration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Should I run Postgres and Redis in Docker even if I develop the API directly on my host machine?&lt;/strong&gt;&lt;br&gt;
Yes, that's a common and reasonable split. Running the stateful services (Postgres, Redis) in containers while running the API directly on the host with &lt;code&gt;npm run start:dev&lt;/code&gt; gives you faster debugger attachment and native tooling, while still avoiding the pain of installing and version-matching database servers by hand. Just remember your &lt;code&gt;DATABASE_URL&lt;/code&gt; needs &lt;code&gt;localhost&lt;/code&gt; in that mode, not the service name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does my API container fail to connect to Postgres even though both are running?&lt;/strong&gt;&lt;br&gt;
Two common causes. Either the connection string still says &lt;code&gt;localhost&lt;/code&gt; instead of the Postgres service name, which only works when both processes run on the host, or the API started before Postgres finished initializing. A &lt;code&gt;depends_on&lt;/code&gt; healthcheck condition on the Postgres service fixes the second case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need Docker Compose if I'm the only developer on the project?&lt;/strong&gt;&lt;br&gt;
It's still worth it for consistency between your machine and CI, and for making onboarding trivial if the team grows. If the project stays genuinely solo and short-lived, running services natively is a fair simplification; add Compose back once a second person or a CI pipeline needs the same setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is file watching so slow inside a Docker container on macOS?&lt;/strong&gt;&lt;br&gt;
Bind mounts on macOS go through a virtualization layer that's slower than native filesystem access, and file-watching mechanisms like &lt;code&gt;inotify&lt;/code&gt; don't cross that boundary as efficiently as on Linux. Docker Desktop's newer virtualization backends (VirtioFS on macOS) reduce this significantly, so keeping Docker Desktop current matters more here than most people expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use the same Dockerfile for development and production?&lt;/strong&gt;&lt;br&gt;
Yes, through multi-stage builds with separate targets, as shown above. Trying to use one undifferentiated stage for both usually means either a slow production image full of dev dependencies, or a development setup that requires a full rebuild on every code change, since it lacks the bind-mount and watch behavior a dev loop needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between depends_on and a healthcheck?&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;depends_on&lt;/code&gt; alone only guarantees Docker starts one container before another; it says nothing about whether the first service is actually ready to accept traffic. Adding a &lt;code&gt;healthcheck&lt;/code&gt; to the dependency and a &lt;code&gt;condition: service_healthy&lt;/code&gt; on the dependent service makes Compose wait for real readiness, which matters most for databases that take a moment to accept connections after the process starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous: &lt;a href="https://amanksingh.com/blog/prisma-or-typeorm-nestjs" rel="noopener noreferrer"&gt;Choosing Prisma or TypeORM for Your NestJS API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Next: &lt;a href="https://amanksingh.com/blog/environment-variables-and-secrets" rel="noopener noreferrer"&gt;Managing Environment Variables and Secrets&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Start of series: &lt;a href="https://amanksingh.com/blog/choosing-the-right-tech-stack-for-saas" rel="noopener noreferrer"&gt;Choosing the Right Tech Stack for Your SaaS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Aman Singh&lt;/strong&gt; — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.&lt;/p&gt;

&lt;p&gt;I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://amanksingh.com" rel="noopener noreferrer"&gt;https://amanksingh.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/amansingh1501" rel="noopener noreferrer"&gt;https://github.com/amansingh1501&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product: &lt;a href="https://vowerole.com" rel="noopener noreferrer"&gt;https://vowerole.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email: &lt;a href="mailto:aman97aman@gmail.com"&gt;aman97aman@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>docker</category>
      <category>node</category>
      <category>devops</category>
      <category>backend</category>
    </item>
    <item>
      <title>Choosing Prisma or TypeORM for Your NestJS API</title>
      <dc:creator>Aman Kumar Singh</dc:creator>
      <pubDate>Tue, 21 Jul 2026 07:51:59 +0000</pubDate>
      <link>https://dev.to/moose978/choosing-prisma-or-typeorm-for-your-nestjs-api-3a63</link>
      <guid>https://dev.to/moose978/choosing-prisma-or-typeorm-for-your-nestjs-api-3a63</guid>
      <description>&lt;p&gt;Last time, &lt;a href="https://amanksingh.com/blog/postgresql-setup-guide" rel="noopener noreferrer"&gt;setting up PostgreSQL the right way&lt;/a&gt;, I left one thing unresolved on purpose: the database has a schema and a connection string, but nothing in the API talks to it yet. That gap is filled by a data-access layer, and in the NestJS world the two real contenders are Prisma and TypeORM.&lt;/p&gt;

&lt;p&gt;This is part six of the &lt;strong&gt;&lt;a href="https://amanksingh.com/series/full-stack-saas-masterclass" rel="noopener noreferrer"&gt;Full Stack SaaS Masterclass&lt;/a&gt;&lt;/strong&gt;, and it's the kind of decision that's easy to treat as a coin flip but actually shapes how your team writes queries for years. Get it wrong for your team's style and you'll spend a lot of time fighting the abstraction instead of shipping features.&lt;/p&gt;

&lt;p&gt;Here's how each one actually works in a NestJS app, where they genuinely differ, and the production pitfalls that don't show up in the getting-started docs, plus which one I reach for and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this decision is bigger than it looks
&lt;/h2&gt;

&lt;p&gt;An ORM, or in Prisma's case a query builder with ORM-like ergonomics, sits between every request your API handles and the database that backs it. That's a lot of surface area for a decision made in week one.&lt;/p&gt;

&lt;p&gt;Three things make this choice consequential rather than cosmetic:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migrations become part of your deploy pipeline.&lt;/strong&gt; Both tools generate and run migrations, but they do it differently enough that switching later means rewriting your entire migration history, not just swapping an import.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type safety at the query boundary compounds.&lt;/strong&gt; Loosely typed query results leak looseness into every service and controller downstream. Strictly typed results turn a schema change into a compile error in the right place instead of a runtime surprise in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The abstraction shapes how your team thinks about SQL.&lt;/strong&gt; Some tools want you to write SQL-adjacent code and get out of the way. Others hide SQL almost entirely behind a generated client. Picking one changes what "normal" looks like in code review for the next few years.&lt;/p&gt;

&lt;p&gt;None of this needs weeks of debate. It deserves the twenty minutes this article takes, rather than picking whichever one showed up first in a tutorial search.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prisma: schema-first, with a generated client
&lt;/h2&gt;

&lt;p&gt;Prisma's model is a single source of truth: a &lt;code&gt;schema.prisma&lt;/code&gt; file that describes your tables, and a code generation step that turns it into a fully typed client.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Organization {
  id        String   @id @default(uuid())
  name      String
  users     User[]
  createdAt DateTime @default(now()) @map("created_at")

  @@map("organizations")
}

model User {
  id             String       @id @default(uuid())
  email          String       @unique
  name           String
  organization   Organization @relation(fields: [organizationId], references: [id])
  organizationId String       @map("organization_id")
  createdAt      DateTime     @default(now()) @map("created_at")

  @@map("users")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;npx prisma migrate dev&lt;/code&gt; reads that file, diffs it against the current database state, generates a SQL migration, and applies it. Running &lt;code&gt;npx prisma generate&lt;/code&gt; produces a TypeScript client with a type for every model and every query shape, including relations.&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 migrate dev &lt;span class="nt"&gt;--name&lt;/span&gt; add_organizations
npx prisma generate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wiring it into NestJS is a single injectable service that extends &lt;code&gt;PrismaClient&lt;/code&gt; and manages its own connection lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/prisma/prisma.service.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OnModuleInit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OnModuleDestroy&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&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;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;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="k"&gt;implements&lt;/span&gt; &lt;span class="nx"&gt;OnModuleInit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OnModuleDestroy&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="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;onModuleDestroy&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;$disconnect&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="c1"&gt;// src/modules/users/users.service.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaService&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;../../prisma/prisma.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;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;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="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="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;organization&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;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 return type of &lt;code&gt;findById&lt;/code&gt; is inferred correctly, &lt;code&gt;organization&lt;/code&gt; included and all, with no manual type annotation. That's Prisma's strongest pitch: the schema is the type. The client can't drift from what the database actually looks like, because it's generated from a migration history that ran against a real database.&lt;/p&gt;

&lt;h2&gt;
  
  
  TypeORM: entities, decorators, and the repository pattern
&lt;/h2&gt;

&lt;p&gt;TypeORM takes the more familiar object-relational route. Tables are TypeScript classes decorated with metadata, and a repository object gives you query methods scoped to that entity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/modules/users/entities/user.entity.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PrimaryGeneratedColumn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ManyToOne&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;CreateDateColumn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JoinColumn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;typeorm&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;Organization&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;../../organizations/entities/organization.entity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Entity&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;User&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;PrimaryGeneratedColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uuid&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="nd"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;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;Column&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;ManyToOne&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;Organization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;org&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;org&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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;JoinColumn&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="s1"&gt;organization_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;organization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Organization&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Column&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="s1"&gt;organization_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;organizationId&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;CreateDateColumn&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="s1"&gt;created_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&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;Registering it in the app module and pulling in a repository looks like 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="c1"&gt;// src/app.module.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Module&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;TypeOrmModule&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/typeorm&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;ConfigModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ConfigService&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/config&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;UsersModule&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;./modules/users/users.module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;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;ConfigModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;isGlobal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;TypeOrmModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRootAsync&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ConfigService&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;useFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ConfigService&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;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;DATABASE_URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/**/*.entity{.ts,.js}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;migrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/migrations/*{.ts,.js}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;synchronize&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="c1"&gt;// never true outside a throwaway sandbox&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="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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/modules/users/users.service.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Injectable&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;InjectRepository&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/typeorm&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;Repository&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;typeorm&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;User&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;./entities/user.entity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Injectable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;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="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;InjectRepository&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="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Repository&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="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;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="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;users&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="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="na"&gt;relations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;organization&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;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;TypeORM's migrations are written and run separately from your entity definitions. You either hand-write a migration in SQL or SQL-adjacent TypeScript, or generate one from a diff against your current entities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx typeorm migration:generate src/migrations/AddOrganizations &lt;span class="nt"&gt;-d&lt;/span&gt; src/data-source.ts
npx typeorm migration:run &lt;span class="nt"&gt;-d&lt;/span&gt; src/data-source.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;synchronize: false&lt;/code&gt; line in the config above is doing real work. TypeORM's &lt;code&gt;synchronize: true&lt;/code&gt; will happily mutate your schema to match your entities on every boot, which is fine for a local sandbox and a genuinely dangerous default anywhere real data lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tradeoffs that actually matter
&lt;/h2&gt;

&lt;p&gt;Once you've written a few queries in each, the differences that matter in production stop being about syntax and start being about these:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type inference depth.&lt;/strong&gt; Prisma's generated client infers return shapes precisely, including nested &lt;code&gt;include&lt;/code&gt;s, without you writing a single interface by hand. TypeORM's typing is real but shallower: relations you forget to load show up as &lt;code&gt;undefined&lt;/code&gt; at runtime with a type that still claims they exist, unless you're careful with strict null settings and consistent &lt;code&gt;relations&lt;/code&gt; options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration authorship.&lt;/strong&gt; Prisma's migration files are auto-generated SQL you review and commit; you rarely hand-edit them. TypeORM expects you to be comfortable reading and occasionally writing the generated migration yourself, a feature if your team is SQL-fluent and friction if it isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escape hatches to raw SQL.&lt;/strong&gt; Both support raw queries when the query builder can't express what you need: &lt;code&gt;$queryRaw&lt;/code&gt; in Prisma, &lt;code&gt;query()&lt;/code&gt; on the data source in TypeORM. Prisma's raw escape hatch feels like a deliberate exception. TypeORM's query builder sits closer to SQL by default, so you reach for raw queries less often, at the cost of more verbose everyday code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Active record versus data mapper.&lt;/strong&gt; TypeORM supports an active-record style, where entities carry their own &lt;code&gt;save()&lt;/code&gt; and &lt;code&gt;remove()&lt;/code&gt; methods, alongside the repository pattern shown above. Prisma has no active-record mode; every query goes through the client. Teams that like a model instance managing its own persistence get that option with TypeORM. Prisma keeps persistence logic in services by design, with no alternative path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transactions.&lt;/strong&gt; Prisma wraps a set of operations in &lt;code&gt;$transaction&lt;/code&gt;, either as an array of promises or an interactive callback. TypeORM offers the same through &lt;code&gt;dataSource.transaction()&lt;/code&gt; or a manually managed &lt;code&gt;QueryRunner&lt;/code&gt; when you need row-level locking across several steps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Prisma: interactive transaction&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="nf"&gt;$transaction&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;tx&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;org&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;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organization&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="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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Acme&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;await&lt;/span&gt; &lt;span class="nx"&gt;tx&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;create&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;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;owner@acme.com&lt;/span&gt;&lt;span class="dl"&gt;'&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="s1"&gt;Owner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;org&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TypeORM: transaction via the data source&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;dataSource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&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;manager&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;org&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;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Organization&lt;/span&gt;&lt;span class="p"&gt;,&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="s1"&gt;Acme&lt;/span&gt;&lt;span class="dl"&gt;'&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;manager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;owner@acme.com&lt;/span&gt;&lt;span class="dl"&gt;'&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="s1"&gt;Owner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;org&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;Neither approach is meaningfully safer than the other. Both give you a real transaction boundary, which is what matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production pitfalls worth knowing before you commit
&lt;/h2&gt;

&lt;p&gt;A few things look fine in a tutorial and turn into real problems once real traffic hits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;N+1 queries hide differently in each tool.&lt;/strong&gt; Prisma's &lt;code&gt;include&lt;/code&gt; and TypeORM's &lt;code&gt;relations&lt;/code&gt; both prevent the classic N+1 pattern when used up front, but it's easy to lazily reach into a relation later in code that assumed it was loaded. The fix is the same regardless of ORM: decide what a query needs to return before you write it, load it explicitly, and treat unexpected extra queries in your logs as a bug, not a curiosity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection pooling needs a plan beyond the defaults.&lt;/strong&gt; A serverless deployment target opens and closes many short-lived connections, and neither tool's default pool handles that well without something like PgBouncer in front of Postgres. Confirm your deployment target before assuming defaults are fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration drift between environments is a process problem, not a tooling one.&lt;/strong&gt; Both tools can produce a migration that works locally and fails in CI because someone edited the database by hand, the same discipline problem flagged in the previous article. Treat every schema change as a migration file, checked in and reviewed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema-first and entity-first genuinely diverge on refactors.&lt;/strong&gt; Renaming a column in Prisma means editing one line in &lt;code&gt;schema.prisma&lt;/code&gt; and regenerating a migration; the client updates automatically everywhere. Renaming a column with TypeORM entities means updating the decorator, generating or writing the migration, and trusting that every place referencing the old property name gets caught by the compiler. It usually does, but test that assumption before relying on it for a large refactor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Testing needs a real strategy either way.&lt;/strong&gt; Mocking a Prisma client or a TypeORM repository works for unit tests, but joins, transactions, and constraint violations only get proven against a real Postgres instance. Plan for an integration suite against a containerized database rather than trusting mocks alone.&lt;/p&gt;

&lt;p&gt;If I'm choosing today, for a new SaaS with a small team, I reach for Prisma. The generated types remove a category of drift bugs for free, and the schema-first workflow keeps the whole team looking at one file to understand the data model. TypeORM earns its place when the team is already SQL-fluent and wants active record or fine-grained query builder control, or when the project has legacy TypeORM entities not worth migrating away from. Neither choice is wrong. Just don't switch mid-project once real migrations exist; that cost is the one thing both tools genuinely make expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prisma is schema-first: one &lt;code&gt;schema.prisma&lt;/code&gt; file generates both migrations and a fully typed client, so query results are inferred without hand-written types.&lt;/li&gt;
&lt;li&gt;TypeORM is entity-first: decorated classes define your tables, with a repository or active-record API and separately authored or generated migrations.&lt;/li&gt;
&lt;li&gt;Type inference is deeper and more automatic in Prisma; TypeORM is real but needs discipline around loaded relations and strict null settings.&lt;/li&gt;
&lt;li&gt;Both support transactions and raw SQL escape hatches; the difference is how often you reach for the escape hatch in everyday code.&lt;/li&gt;
&lt;li&gt;N+1 queries, connection pooling under serverless, and migration drift are pitfalls in both tools, not unique to either.&lt;/li&gt;
&lt;li&gt;Pick based on team fluency and how schema-first versus entity-first fits how your team already thinks, and treat switching mid-project as an expensive, rare event.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is Prisma or TypeORM better for a NestJS project?&lt;/strong&gt;&lt;br&gt;
Neither is universally better. Prisma's schema-first model and generated client give deeper type safety with less manual work, which suits most new SaaS projects. TypeORM's entity and repository model suits teams that want SQL-adjacent control, active-record persistence, or already have an existing TypeORM codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Prisma and TypeORM together in the same NestJS app?&lt;/strong&gt;&lt;br&gt;
Technically yes, since both can coexist as separate connections, but it's rarely worth the complexity of maintaining two migration histories and two mental models for the same database. Pick one for the whole app unless you have a specific, isolated reason not to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Prisma support raw SQL when the query builder isn't enough?&lt;/strong&gt;&lt;br&gt;
Yes, through &lt;code&gt;$queryRaw&lt;/code&gt; and &lt;code&gt;$executeRaw&lt;/code&gt;, both of which stay typed when you tag the result shape. It's an occasional escape hatch, not a primary way of writing queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why shouldn't I use TypeORM's synchronize: true in production?&lt;/strong&gt;&lt;br&gt;
It mutates your live schema to match your entity definitions on every application boot, with no migration file, no review step, and no rollback path. Convenient for a disposable local sandbox, and a serious risk anywhere real data exists, since a single bad entity change can alter or drop a column silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do migrations work differently between the two tools?&lt;/strong&gt;&lt;br&gt;
Prisma generates a migration by diffing your schema file against the database's current state, and you rarely edit the generated SQL by hand. TypeORM either generates a migration from an entity diff or expects you to write one directly in SQL-adjacent TypeScript, trading manual control for manual responsibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which ORM handles multi-tenant SaaS schemas better?&lt;/strong&gt;&lt;br&gt;
Both handle the shared-schema, &lt;code&gt;organization_id&lt;/code&gt;-per-row approach shown in this article equally well; the pattern is a modeling decision, not something one ORM does natively and the other doesn't. Row-level security or per-tenant schemas, if you go that route later, add configuration work regardless of which tool you're using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous: &lt;a href="https://amanksingh.com/blog/postgresql-setup-guide" rel="noopener noreferrer"&gt;Setting Up PostgreSQL the Right Way&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Next: &lt;a href="https://amanksingh.com/blog/docker-local-development-setup" rel="noopener noreferrer"&gt;Setting Up Docker for Local Development&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Start of series: &lt;a href="https://amanksingh.com/blog/choosing-the-right-tech-stack-for-saas" rel="noopener noreferrer"&gt;Choosing the Right Tech Stack for Your SaaS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Aman Singh&lt;/strong&gt; — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.&lt;/p&gt;

&lt;p&gt;I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://amanksingh.com" rel="noopener noreferrer"&gt;https://amanksingh.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/amansingh1501" rel="noopener noreferrer"&gt;https://github.com/amansingh1501&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product: &lt;a href="https://vowerole.com" rel="noopener noreferrer"&gt;https://vowerole.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email: &lt;a href="mailto:aman97aman@gmail.com"&gt;aman97aman@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>nestjs</category>
      <category>prisma</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
    <item>
      <title>Setting Up PostgreSQL the Right Way</title>
      <dc:creator>Aman Kumar Singh</dc:creator>
      <pubDate>Tue, 21 Jul 2026 07:51:56 +0000</pubDate>
      <link>https://dev.to/moose978/setting-up-postgresql-the-right-way-570g</link>
      <guid>https://dev.to/moose978/setting-up-postgresql-the-right-way-570g</guid>
      <description>&lt;p&gt;The NestJS API from last time can validate a request and return a typed &lt;code&gt;User&lt;/code&gt;, but right now that user lives nowhere. It's time to give the app a memory. In this series that memory is PostgreSQL, and the way you set it up in the first hour decides whether your data layer stays boring for years or turns into a source of 2am incidents.&lt;/p&gt;

&lt;p&gt;This article covers the foundation: getting Postgres running the same way for everyone on the team, connecting the API to it safely, and thinking about your first schema without painting yourself into a corner. Query tuning and partitioning can wait.&lt;/p&gt;

&lt;p&gt;This is part five of the &lt;a href="https://amanksingh.com/series/full-stack-saas-masterclass" rel="noopener noreferrer"&gt;Full Stack SaaS Masterclass&lt;/a&gt;, continuing from the &lt;a href="https://amanksingh.com/blog/nestjs-project-setup-guide" rel="noopener noreferrer"&gt;NestJS API&lt;/a&gt; in &lt;code&gt;apps/api&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it in Docker, not on your laptop
&lt;/h2&gt;

&lt;p&gt;The first decision is where Postgres actually runs during development, and the answer is a container. Installing Postgres directly on your machine works right up until a teammate has a different version, or you need a second project on a different major release, and then you're debugging the database instead of building features.&lt;/p&gt;

&lt;p&gt;We already committed to Docker for local reproducibility back in the stack article. Here's the &lt;code&gt;postgres&lt;/code&gt; service in &lt;code&gt;docker-compose.yml&lt;/code&gt; at the repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;saas&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local_dev_only&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;saas_dev&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5432:5432'&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgdata:/var/lib/postgresql/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;docker-compose up&lt;/code&gt; and every teammate has Postgres 16 on port 5432 with the same database name. The named &lt;code&gt;pgdata&lt;/code&gt; volume means your data survives a container restart, and pinning &lt;code&gt;postgres:16&lt;/code&gt; rather than &lt;code&gt;postgres:latest&lt;/code&gt; means an image update never silently changes your major version underneath you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect the API without leaking secrets
&lt;/h2&gt;

&lt;p&gt;The API reaches Postgres through a connection string. Keep it in the environment, never in code, and give it a sensible shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# apps/api/.env
&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;=&lt;span class="n"&gt;postgresql&lt;/span&gt;://&lt;span class="n"&gt;saas&lt;/span&gt;:&lt;span class="n"&gt;local_dev_only&lt;/span&gt;@&lt;span class="n"&gt;localhost&lt;/span&gt;:&lt;span class="m"&gt;5432&lt;/span&gt;/&lt;span class="n"&gt;saas_dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;local_dev_only&lt;/code&gt; password is fine to commit to an &lt;code&gt;.env.example&lt;/code&gt; because it only ever guards a throwaway local container. Production credentials are a different story, and we'll handle environment variables and secrets properly in their own article shortly. The habit to build now: the code reads &lt;code&gt;DATABASE_URL&lt;/code&gt; through &lt;code&gt;@nestjs/config&lt;/code&gt;, and the value changes per environment while the code never does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// reading it the right way, via injected config&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;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;DATABASE_URL&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;h2&gt;
  
  
  Think in schema, not in tables
&lt;/h2&gt;

&lt;p&gt;Before writing a single &lt;code&gt;CREATE TABLE&lt;/code&gt;, it helps to decide how you'll reason about your data. For a multi-tenant SaaS, the question that shapes everything is: how does a row know which organization it belongs to? We'll design multi-tenancy in depth later, but the choice you make here ripples through every table, so it's worth naming early.&lt;/p&gt;

&lt;p&gt;For the foundation, keep it simple and correct. A &lt;code&gt;users&lt;/code&gt; table with a real primary key, a unique constraint where the data demands one, timestamps, and columns that match the shared &lt;code&gt;User&lt;/code&gt; type the frontend and backend both import:&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;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;email&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="k"&gt;UNIQUE&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;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;created_at&lt;/span&gt;  &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&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;p&gt;A few choices in that small block earn their keep. &lt;code&gt;UUID&lt;/code&gt; primary keys instead of auto-incrementing integers mean IDs aren't guessable and don't leak how many users you have, which matters the moment IDs appear in URLs. The &lt;code&gt;UNIQUE&lt;/code&gt; constraint on &lt;code&gt;email&lt;/code&gt; pushes the rule into the database, so two requests racing to register the same address can't both win. And &lt;code&gt;TIMESTAMPTZ&lt;/code&gt; rather than a plain timestamp stores the time zone, saving you a category of bugs that only surface once you have users in a second country.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrations from the very first table
&lt;/h2&gt;

&lt;p&gt;Never change a schema by hand in a running database and hope everyone else runs the same commands. From day one, every schema change is a migration: a versioned, checked-in file that moves the database from one known state to the next, and that runs the same way on your laptop, in CI, and in production.&lt;/p&gt;

&lt;p&gt;The tool you use for migrations usually comes with your data-access layer, which is the subject of the next article. The principle holds regardless of tool. A schema change that isn't a committed migration is a change that will drift between environments and eventually break a deploy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/api/migrations/
├── 0001_create_users.sql
└── 0002_add_organizations.sql   # each one moves the schema forward
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Numbered, ordered, forward-only in spirit. When something goes wrong in production, you want to point at an exact migration, not guess which of five people ran which SQL by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to defer
&lt;/h2&gt;

&lt;p&gt;Postgres has a deep feature set, and almost none of it belongs in your first week. You don't need read replicas or connection pooling infrastructure at zero users; a single instance handles far more traffic than a new SaaS will see for a long time. You don't need partitioning until a table is genuinely large. You don't need to reach for JSONB everywhere just because it's flexible; model relational data relationally and use JSONB for the parts that are actually schemaless.&lt;/p&gt;

&lt;p&gt;The foundation is small on purpose: a containerized Postgres, a connection string in the environment, a clean first schema with real constraints, and migrations from the start. That's enough to build real features on, and everything else is a decision you'll make better later, with real data in front of you.&lt;/p&gt;

&lt;p&gt;Next in the series, we put a data-access layer on top of this: choosing between Prisma and TypeORM, and wiring it into the NestJS app so those migrations and queries are typed end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Run Postgres in Docker with a pinned major version and a named volume, so every environment matches and data survives restarts.&lt;/li&gt;
&lt;li&gt;Keep the connection string in the environment and read it through config; the value changes per environment, the code never does.&lt;/li&gt;
&lt;li&gt;Design your first schema with real constraints: UUID keys, unique constraints, and &lt;code&gt;TIMESTAMPTZ&lt;/code&gt; for time.&lt;/li&gt;
&lt;li&gt;Treat every schema change as a versioned, committed migration from the very first table.&lt;/li&gt;
&lt;li&gt;Defer replicas, partitioning, and JSONB-everywhere until real data justifies them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Should I run PostgreSQL locally or in Docker for development?&lt;/strong&gt;&lt;br&gt;
Use Docker. A pinned image such as &lt;code&gt;postgres:16&lt;/code&gt; with a named volume gives every teammate the same version and persistent data, and avoids the version mismatches that come from installing Postgres directly on each machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why use UUIDs instead of auto-incrementing IDs?&lt;/strong&gt;&lt;br&gt;
UUIDs aren't sequential, so they don't reveal how many records you have and aren't guessable when they appear in URLs. Auto-incrementing integers are fine internally but leak information once exposed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the right way to manage schema changes?&lt;/strong&gt;&lt;br&gt;
Migrations: versioned, checked-in files that move the schema forward and run identically across laptop, CI, and production. Editing a live database by hand causes drift between environments and eventually breaks deploys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I store connection credentials in code?&lt;/strong&gt;&lt;br&gt;
No. Keep the connection string in an environment variable and read it through your config layer. Only a throwaway local password belongs in a committed &lt;code&gt;.env.example&lt;/code&gt;; real credentials stay out of the repo entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When should I worry about replicas and partitioning?&lt;/strong&gt;&lt;br&gt;
Later. A single Postgres instance serves a new SaaS for a long time. Add read replicas, connection pooling, and partitioning when real traffic or table size makes them necessary, not in anticipation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous: &lt;a href="https://amanksingh.com/blog/nestjs-project-setup-guide" rel="noopener noreferrer"&gt;Setting Up NestJS the Right Way&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Next: &lt;a href="https://amanksingh.com/blog/prisma-or-typeorm-nestjs" rel="noopener noreferrer"&gt;Choosing Prisma or TypeORM for Your NestJS API&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Start of series: &lt;a href="https://amanksingh.com/blog/choosing-the-right-tech-stack-for-saas" rel="noopener noreferrer"&gt;Choosing the Right Tech Stack for Your SaaS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Aman Singh&lt;/strong&gt; — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.&lt;/p&gt;

&lt;p&gt;I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://amanksingh.com" rel="noopener noreferrer"&gt;https://amanksingh.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/amansingh1501" rel="noopener noreferrer"&gt;https://github.com/amansingh1501&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product: &lt;a href="https://vowerole.com" rel="noopener noreferrer"&gt;https://vowerole.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email: &lt;a href="mailto:aman97aman@gmail.com"&gt;aman97aman@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>postgres</category>
      <category>database</category>
      <category>docker</category>
      <category>backend</category>
    </item>
    <item>
      <title>Two Pointers &amp; Sliding Window, Explained Simply</title>
      <dc:creator>Aman Kumar Singh</dc:creator>
      <pubDate>Tue, 21 Jul 2026 07:08:50 +0000</pubDate>
      <link>https://dev.to/moose978/two-pointers-sliding-window-explained-simply-31ip</link>
      <guid>https://dev.to/moose978/two-pointers-sliding-window-explained-simply-31ip</guid>
      <description>&lt;p&gt;The first time someone showed me the sliding window technique, my reaction was mild annoyance. I'd been solving a substring problem with nested loops, feeling clever, and they collapsed it into a single pass with two variables. My O(n²) turned into O(n) and I felt slightly robbed.&lt;/p&gt;

&lt;p&gt;That's how two pointers and sliding window work. They look like tricks until they click, and then they feel obvious. They're the answer to a huge slice of array and string problems, especially the ones where a hash map would burn memory you don't need to spend.&lt;/p&gt;

&lt;p&gt;This is part three of the &lt;a href="https://amanksingh.com/series/senior-engineer-handbook" rel="noopener noreferrer"&gt;Senior Engineer Handbook series&lt;/a&gt;. We've covered interview strategy and the hashing patterns. Now the two techniques I reach for whenever the input is sorted or the problem mentions a "substring" or "subarray."&lt;/p&gt;

&lt;h2&gt;
  
  
  Two pointers: walking from both ends
&lt;/h2&gt;

&lt;p&gt;The setup: a sorted array, and you want a pair that satisfies some condition. The naive move is to check every pair, which is O(n²). Two pointers uses the sortedness to skip almost all of that work.&lt;/p&gt;

&lt;p&gt;Put one pointer at the start, one at the end. Look at the sum. Too big? The largest element is too heavy, so move the right pointer left. Too small? Move the left pointer right. Every step eliminates a whole set of pairs you no longer need to check.&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;twoSumSorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nums&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;target&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="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="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;left&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;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nums&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;right&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;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;right&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;sum&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;right&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;sum&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// need a bigger sum, move up from the small end&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;right&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;               &lt;span class="c1"&gt;// need a smaller sum, move down from the big end&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;null&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 this uses zero extra memory. On a sorted array this beats the hash-map version from the last article: same O(n) time, but O(1) space instead of O(n). This is exactly the "the input's sorted, so I'll use two pointers instead of a map" move I mentioned earlier. Saying that in an interview shows you're choosing tools, not reaching for the same one every time.&lt;/p&gt;

&lt;p&gt;Two pointers isn't only start-and-end, though. Sometimes both pointers move the same direction at different speeds. That's how you remove duplicates from a sorted array in place, or detect a cycle in a linked list. The unifying idea is: two indices, each moving under its own rule, replacing a nested loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sliding window: the expanding and shrinking frame
&lt;/h2&gt;

&lt;p&gt;Now the one I love. Sliding window is for problems about a &lt;em&gt;contiguous&lt;/em&gt; stretch: the longest substring with some property, the smallest subarray that hits a sum, the max of every window of size k.&lt;/p&gt;

&lt;p&gt;Picture a frame over the array. You grow it from the right, and when it violates a constraint, you shrink it from the left. The window slides forward, never backtracking, so the whole array is processed in one pass.&lt;/p&gt;

&lt;p&gt;Here's the canonical one: longest substring without repeating characters.&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;longestUniqueSubstring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&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="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastSeen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// char -&amp;gt; most recent index&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;start&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="c1"&gt;// left edge of the window&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;best&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;end&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;end&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;s&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;end&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="c1"&gt;// If we've seen this char inside the current window, jump start past it.&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;lastSeen&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;ch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;lastSeen&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;ch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lastSeen&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;ch&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="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;lastSeen&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;ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;best&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;best&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 &lt;code&gt;end&lt;/code&gt; pointer expands the window every iteration. The moment we hit a repeat, &lt;code&gt;start&lt;/code&gt; jumps forward to kick the duplicate out. The window is always valid, and its widest size is the answer. One pass, O(n).&lt;/p&gt;

&lt;p&gt;The part that took me longest to trust: &lt;code&gt;start&lt;/code&gt; only ever moves forward. It never resets. That's what keeps it linear: each character enters the window once and leaves at most once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixed vs dynamic windows
&lt;/h2&gt;

&lt;p&gt;Two flavors, and knowing which you're in makes the code fall out naturally.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;fixed window&lt;/strong&gt; has a known size k: "maximum sum of any k consecutive elements." You slide a frame of constant width: add the new element, drop the one that fell off the back.&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;maxSumWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nums&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;k&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;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;k&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="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;nums&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="c1"&gt;// first window&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sum&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="nx"&gt;k&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;nums&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="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;nums&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;nums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// slide: add front, drop back&lt;/span&gt;
    &lt;span class="nx"&gt;best&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sum&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;best&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 &lt;code&gt;sum += nums[i] - nums[i - k]&lt;/code&gt; line is the whole trick. Instead of re-summing k elements every step, you adjust by one in and one out. O(n) instead of O(n·k).&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;dynamic window&lt;/strong&gt; grows and shrinks based on a condition: "smallest subarray with sum ≥ target." You expand until the condition is met, then shrink while it stays met, tracking the best. The unique-substring example above is dynamic. The shape is: expand in the loop, shrink in an inner &lt;code&gt;while&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to know it's a window problem
&lt;/h2&gt;

&lt;p&gt;The keywords give it away more than almost any other pattern. If I see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"longest / shortest / maximum / minimum &lt;strong&gt;substring&lt;/strong&gt; or &lt;strong&gt;subarray&lt;/strong&gt;"&lt;/li&gt;
&lt;li&gt;"contiguous"&lt;/li&gt;
&lt;li&gt;"window of size k"&lt;/li&gt;
&lt;li&gt;"at most / at least k of something"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;my hand is already reaching for a sliding window. If instead it's a sorted array and I want pairs or a partition point, it's two pointers. If it's neither sorted nor contiguous, I go back to hashing from the last article.&lt;/p&gt;

&lt;p&gt;That triage, narrowing to a pattern from the phrasing before I write anything, is the single habit that made interviews stop feeling like luck.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake almost everyone makes
&lt;/h2&gt;

&lt;p&gt;Backtracking the left pointer. People shrink the window, then later move &lt;code&gt;start&lt;/code&gt; backward when a new element comes in, and suddenly it's O(n²) again and they can't figure out why it's slow. The whole point of these techniques is that each pointer marches forward and never retreats. If you catch yourself wanting to move a pointer back, the pattern is probably wrong for the problem. Stop and reconsider.&lt;/p&gt;

&lt;p&gt;The other one: off-by-one errors in the window width. &lt;code&gt;end - start + 1&lt;/code&gt; is the size of an inclusive window. I've written &lt;code&gt;end - start&lt;/code&gt; under pressure more times than I'd like to admit and watched my answer come out one short. Test a two-character example by hand before you trust it.&lt;/p&gt;

&lt;p&gt;These two patterns plus the hashing ones from last time cover a genuinely large fraction of what you'll be asked. Next in the series, we go into binary search, including the "search on the answer" trick that solves problems that don't look like search problems at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Two pointers exploits a sorted array to find pairs or partitions in O(n) time and O(1) space, with no hash map needed.&lt;/li&gt;
&lt;li&gt;Sliding window handles contiguous substring and subarray problems in a single forward pass.&lt;/li&gt;
&lt;li&gt;Fixed windows adjust by one-in-one-out; dynamic windows expand to satisfy a condition, then shrink while it holds.&lt;/li&gt;
&lt;li&gt;Both pointers only ever move forward. Backtracking a pointer is the bug that quietly turns O(n) back into O(n²).&lt;/li&gt;
&lt;li&gt;Triage by phrasing: "substring/subarray/contiguous/window" means sliding window; "sorted + pairs" means two pointers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When should I use two pointers vs a hash map?&lt;/strong&gt;&lt;br&gt;
Two pointers when the array is sorted and you want pairs, partitions, or in-place work with O(1) space. A hash map when the data is unsorted and you need to look up values you've already seen. On a sorted array, two pointers usually beats a map on memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I recognize a sliding window problem?&lt;/strong&gt;&lt;br&gt;
Look for words like longest, shortest, maximum, minimum paired with substring, subarray, contiguous, or "window of size k." Those almost always point to a sliding window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between a fixed and dynamic sliding window?&lt;/strong&gt;&lt;br&gt;
A fixed window has a constant size k and slides one element at a time. A dynamic window grows until a condition is satisfied and shrinks while it stays satisfied, so its size changes as it moves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does the sliding window stay O(n) if there's a nested while loop?&lt;/strong&gt;&lt;br&gt;
Because each element enters the window once and leaves at most once. The inner loop's total work across the whole run is bounded by n, so it's still linear overall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the most common sliding window bug?&lt;/strong&gt;&lt;br&gt;
Backtracking a pointer, which reintroduces O(n²) behavior, and off-by-one mistakes on window size. Remember an inclusive window is &lt;code&gt;end - start + 1&lt;/code&gt; wide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous: &lt;a href="https://amanksingh.com/blog/arrays-hashing-interview-patterns" rel="noopener noreferrer"&gt;Arrays &amp;amp; Hashing: The Interview Patterns You Must Know&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Start of series: &lt;a href="https://amanksingh.com/blog/senior-engineer-coding-interviews-guide" rel="noopener noreferrer"&gt;The Senior Engineer's Guide to Coding Interviews&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Aman Singh&lt;/strong&gt; — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.&lt;/p&gt;

&lt;p&gt;I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://amanksingh.com" rel="noopener noreferrer"&gt;https://amanksingh.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/amansingh1501" rel="noopener noreferrer"&gt;https://github.com/amansingh1501&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product: &lt;a href="https://vowerole.com" rel="noopener noreferrer"&gt;https://vowerole.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email: &lt;a href="mailto:aman97aman@gmail.com"&gt;aman97aman@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>algorithms</category>
      <category>dsa</category>
      <category>typescript</category>
      <category>programming</category>
    </item>
    <item>
      <title>Arrays &amp; Hashing: The Interview Patterns You Must Know</title>
      <dc:creator>Aman Kumar Singh</dc:creator>
      <pubDate>Tue, 21 Jul 2026 07:03:42 +0000</pubDate>
      <link>https://dev.to/moose978/arrays-hashing-the-interview-patterns-you-must-know-6fp</link>
      <guid>https://dev.to/moose978/arrays-hashing-the-interview-patterns-you-must-know-6fp</guid>
      <description>&lt;p&gt;If I had to bet on what your next coding interview opens with, I'd put money on an array problem that's secretly a hashing problem. They're everywhere. Two-sum, group anagrams, subarray sums, "find the duplicate." Different clothes, same skeleton underneath.&lt;/p&gt;

&lt;p&gt;The engineers who breeze through these aren't faster typists. They've just learned to see the hash map hiding inside the array question. Once you see it, you can't unsee it, and a whole category of problems stops being scary.&lt;/p&gt;

&lt;p&gt;This is the second article in the &lt;a href="https://amanksingh.com/series/senior-engineer-handbook" rel="noopener noreferrer"&gt;Senior Engineer Handbook series&lt;/a&gt;. Last time we covered how to approach interviews. Now we get specific.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why hashing shows up so often
&lt;/h2&gt;

&lt;p&gt;Most array problems are slow for one reason: you're searching. "Have I seen this value before?" "Does its complement exist?" "How many times has this appeared?" The naive answer to every one of those is a nested loop: scan the array again for each element. That's O(n²), and O(n²) is the thing the interviewer is quietly waiting for you to kill.&lt;/p&gt;

&lt;p&gt;A hash map turns "search the whole array" into "check one key." O(n) lookups collapse to O(1). That single move is the difference between the brute-force answer and the one that gets you a callback.&lt;/p&gt;

&lt;p&gt;So when a problem involves &lt;em&gt;searching, counting, or matching&lt;/em&gt; inside an array, my first instinct is: what would I store in a map to make the lookup instant?&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: complement lookup
&lt;/h2&gt;

&lt;p&gt;The classic. You're looking for two things that combine to a target. Instead of checking every pair, store what you've seen and ask whether the piece you &lt;em&gt;need&lt;/em&gt; is already there.&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;twoSum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nums&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;target&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="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="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&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;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// value -&amp;gt; index&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;nums&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="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;need&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;nums&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&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;need&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;seen&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;need&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&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;seen&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;nums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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 mental model: as I walk the array, I keep a record of everything behind me, so when I land on a number I can instantly ask "did its partner already go by?" No second loop. One pass.&lt;/p&gt;

&lt;p&gt;This same shape solves far more than two-sum. "Find if any two elements differ by k." "Does a pair exist that sums to a value?" Same skeleton, different arithmetic on the key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: frequency counting
&lt;/h2&gt;

&lt;p&gt;The second workhorse. Whenever a problem cares about &lt;em&gt;how many times&lt;/em&gt; something appears (most frequent element, first non-repeating character, "can these two strings be anagrams"), you're building a frequency map.&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;firstUniqueChar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&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="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;count&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;ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&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;ch&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="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;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;s&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="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;count&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;s&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="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="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="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two passes, both O(n). First pass tallies, second pass reads the tally. I like this one in interviews because it's easy to narrate cleanly: "count everything, then find the first thing with a count of one." The interviewer follows you the whole way.&lt;/p&gt;

&lt;p&gt;A tell for anagram problems specifically: two strings are anagrams if their frequency maps are identical. You don't need to sort: sorting is O(n log n) and counting is O(n). If you reach for &lt;code&gt;.sort()&lt;/code&gt; here, expect the interviewer to ask "can you do better?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: grouping by a computed key
&lt;/h2&gt;

&lt;p&gt;This one trips people up because the key isn't the value itself. It's something you &lt;em&gt;derive&lt;/em&gt; from it. Group anagrams is the poster child: the key is the sorted version of each word, and all words sharing that key belong 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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;groupAnagrams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;words&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="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;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Words that are anagrams collapse to the same sorted key.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;sort&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="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bucket&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;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&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;Worth saying out loud: "I need a signature that's the same for every anagram and different for everything else." Sorted letters is one such signature. A 26-length letter-count array is another (and faster). Naming that idea ("I need a canonical key") is exactly the kind of reasoning senior interviews reward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4: prefix sums with a map
&lt;/h2&gt;

&lt;p&gt;Now the one that separates people. "How many subarrays sum to k?" The brute force checks every subarray, which is O(n²). The trick is a running total plus a hash map of totals you've seen.&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;subarraySumEqualsK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nums&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;k&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="o"&gt;&amp;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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]);&lt;/span&gt; &lt;span class="c1"&gt;// prefixSum -&amp;gt; how many times&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;running&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;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;nums&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// If (running - k) appeared before, every such prefix marks a valid subarray end here.&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;seen&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;running&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&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;seen&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;running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&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;running&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="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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;count&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;I won't pretend this one is obvious. It took me a few sittings to really &lt;em&gt;get&lt;/em&gt; why &lt;code&gt;running - k&lt;/code&gt; is the thing to look up. The idea: if the total up to here is &lt;code&gt;running&lt;/code&gt;, and some earlier point had total &lt;code&gt;running - k&lt;/code&gt;, then the stretch between them sums to exactly &lt;code&gt;k&lt;/code&gt;. The map counts how many such earlier points exist.&lt;/p&gt;

&lt;p&gt;If you only memorize one non-trivial pattern from this article, make it this one. Prefix-sum-plus-map shows up constantly and always looks impressive because most people don't see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a hash map is the wrong answer
&lt;/h2&gt;

&lt;p&gt;Reaching for a map reflexively is its own trap, so let me push back on my own advice.&lt;/p&gt;

&lt;p&gt;Maps cost memory. If the problem says "solve it in O(1) space," a hash map is off the table and they probably want two pointers or in-place index marking instead. If the array is sorted, a map throws away information you could've used; two pointers or binary search is often cleaner and uses no extra space. And for tiny fixed alphabets (lowercase letters), a plain 26-slot array beats a &lt;code&gt;Map&lt;/code&gt; on both speed and clarity.&lt;/p&gt;

&lt;p&gt;Part of sounding senior is knowing when &lt;em&gt;not&lt;/em&gt; to use the tool you like. If you say "a hash map would work, but since the input's sorted I can do this in O(1) space with two pointers," you've just shown range.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to recognize which pattern you're in
&lt;/h2&gt;

&lt;p&gt;A quick field guide for the pressure of the moment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Looking for pairs/complements that hit a target → &lt;strong&gt;complement lookup&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Counting occurrences, or comparing multisets/anagrams → &lt;strong&gt;frequency map&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Bucketing items that share a derived property → &lt;strong&gt;group by computed key&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Anything about contiguous subarray sums or counts → &lt;strong&gt;prefix sum + map&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Sorted input, or an O(1)-space constraint → step back, consider &lt;strong&gt;two pointers&lt;/strong&gt; instead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Say the category out loud when you spot it. "This is a frequency-counting problem" tells the interviewer you've mapped it to a known shape, and it steadies your own hands.&lt;/p&gt;

&lt;p&gt;Next in the series, we leave hashing behind for the two techniques that dominate sorted-array and substring problems: two pointers and the sliding window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Most slow array solutions are slow because they search; a hash map turns O(n) searches into O(1) lookups.&lt;/li&gt;
&lt;li&gt;Four patterns cover the majority of interview questions: complement lookup, frequency counting, grouping by a computed key, and prefix sum with a map.&lt;/li&gt;
&lt;li&gt;For anagrams, compare frequency maps instead of sorting; O(n) beats O(n log n).&lt;/li&gt;
&lt;li&gt;Prefix-sum-plus-map (subarrays summing to k) is the highest-value non-obvious pattern; learn it cold.&lt;/li&gt;
&lt;li&gt;A hash map is the wrong choice when space must be O(1) or the array is already sorted; reach for two pointers there.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When should I use a hash map in an array problem?&lt;/strong&gt;&lt;br&gt;
When you're searching, counting, or matching: anything where the naive answer is a nested loop scanning the array again. A map makes those lookups O(1) and drops the overall time from O(n²) to O(n).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not just sort the array instead of hashing?&lt;/strong&gt;&lt;br&gt;
Sorting is O(n log n) and destroys the original order. Hashing is O(n) and preserves order. Sort only when the problem genuinely needs ordering or when a sorted array unlocks two pointers with O(1) space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the prefix sum pattern and when do I use it?&lt;/strong&gt;&lt;br&gt;
Keep a running total and store each total in a map. For "count subarrays summing to k," look up &lt;code&gt;running - k&lt;/code&gt; to find where valid subarrays start. Use it for any contiguous-subarray sum or count question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I check if two strings are anagrams efficiently?&lt;/strong&gt;&lt;br&gt;
Build a frequency count for each and compare, or use a single 26-length array of letter counts. Both are O(n), faster than sorting both strings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are hash map solutions always the best?&lt;/strong&gt;&lt;br&gt;
No. They cost memory. If the problem demands O(1) space or the input is sorted, two pointers or binary search is usually cleaner and lighter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous: &lt;a href="https://amanksingh.com/blog/senior-engineer-coding-interviews-guide" rel="noopener noreferrer"&gt;The Senior Engineer's Guide to Coding Interviews&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Next: &lt;a href="https://amanksingh.com/blog/two-pointers-sliding-window-guide" rel="noopener noreferrer"&gt;Two Pointers &amp;amp; Sliding Window, Explained Simply&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Aman Singh&lt;/strong&gt; — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.&lt;/p&gt;

&lt;p&gt;I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://amanksingh.com" rel="noopener noreferrer"&gt;https://amanksingh.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/amansingh1501" rel="noopener noreferrer"&gt;https://github.com/amansingh1501&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product: &lt;a href="https://vowerole.com" rel="noopener noreferrer"&gt;https://vowerole.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email: &lt;a href="mailto:aman97aman@gmail.com"&gt;aman97aman@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>arrays</category>
      <category>hashing</category>
      <category>dsa</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>The Senior Engineer's Guide to Coding Interviews</title>
      <dc:creator>Aman Kumar Singh</dc:creator>
      <pubDate>Tue, 21 Jul 2026 07:03:40 +0000</pubDate>
      <link>https://dev.to/moose978/the-senior-engineers-guide-to-coding-interviews-2o35</link>
      <guid>https://dev.to/moose978/the-senior-engineers-guide-to-coding-interviews-2o35</guid>
      <description>&lt;p&gt;I bombed a coding interview about 4 years into my career. Not because I couldn't code; I'd shipped plenty of production software by that point, and I knew the language cold. I just froze on a problem I'd have solved in 5 minutes at my own desk, and the interviewer sat there watching me flail while I watched myself do the exact same thing, and neither of us really enjoyed the half hour it took.&lt;/p&gt;

&lt;p&gt;That whole experience taught me something years of grinding leetcode never did. Senior interviews are a genuinely different game, and if you prepare for one the way a junior would, you tend to get rejected the way a junior does, however strong an engineer you actually happen to be.&lt;/p&gt;

&lt;p&gt;So this is more or less the guide I wish someone had handed me back then. It focuses on the approach itself rather than a pile of problems to memorize.&lt;/p&gt;

&lt;h2&gt;
  
  
  What interviewers are really checking at the senior level
&lt;/h2&gt;

&lt;p&gt;When you ask a junior "can you solve this?", that's basically the whole test right there. When you ask a senior the exact same question, something else is going on underneath it entirely; what they're really asking is whether you can think, and whether they could hand you something vague and half-specified and still trust you to come back with a reasonable answer.&lt;/p&gt;

&lt;p&gt;The problem on the whiteboard barely matters on its own; it's mostly a prop for the real evaluation that's happening around it.&lt;/p&gt;

&lt;p&gt;What they're really doing the whole time is watching how you work. They notice whether you clarify things before you start typing or whether you assume and charge in. They notice whether you go quiet and freeze the moment you hit a wall, or whether you keep thinking out loud. And they're quietly checking whether you can actually weigh tradeoffs or only really know the one approach, and whether the code you leave behind is something a teammate could still read 6 months later without hating you for it.&lt;/p&gt;

&lt;p&gt;I've sat on both sides of that table. As the interviewer, I've honestly passed people who never even finished the problem, just because their reasoning was clean and they were upfront about the gaps; and I've failed people who reached the optimal answer and then couldn't tell me why it actually worked, or what they'd change if the constraints shifted a little. That second group concerned me more, because a correct answer that the candidate can't really explain tells you very little about how they'd actually work day to day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The framework I actually use
&lt;/h2&gt;

&lt;p&gt;I run more or less the same loop on every single problem. It's deliberately boring, and that's the entire point; a boring, repeatable process is the thing that keeps you actually moving forward when you're under pressure and your mind wants to go blank on you.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Restate and clarify. Say the problem back to them in your own words first, and then ask the questions that actually change the answer: how big the input really is, whether it can be empty, whether there are duplicates in it, whether it's already sorted, and whether you're supposed to be optimizing for time or for space. None of that is stalling. A good clarifying question is one of the clearest early signals of seniority there is.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Talk through the naive approach first. Say the brute-force version out loud, even when it's obviously way too slow. Something like: "the straightforward way here is O(n²), just nested loops comparing every pair; let me get that working first and then optimize." This does two things at once. It gets a working baseline up on the board, and it shows the interviewer that you understand exactly where you're starting from.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Find the bottleneck, then optimize it. Don't jump straight to the clever trick. Point at the expensive part first. "the nested loop is the problem here; if I had O(1) lookups instead of scanning every time, this drops to O(n), which basically points me at a hash map." Optimization should look like a decision you actually made. It shouldn't read as a trick you happened to recall from memory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Write it cleanly. Use real names. &lt;code&gt;left&lt;/code&gt; and &lt;code&gt;right&lt;/code&gt;, not &lt;code&gt;i&lt;/code&gt; and &lt;code&gt;j&lt;/code&gt;, when they actually carry meaning. Add a small helper function when it genuinely improves readability. The interviewer is quietly picturing you as a teammate the whole time you write, so give them an actual reason to want that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Test it yourself. Walk through a normal case first, then an edge case, out loud, before anyone even asks you to. Catching your own bug is worth a lot more than never having written one in the first place.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the loop applied to the most common opener, two-sum:&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;twoSum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nums&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;target&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="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="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// seen maps a value we've passed -&amp;gt; the index it was at.&lt;/span&gt;
  &lt;span class="c1"&gt;// One pass, O(1) lookups, so O(n) total instead of the O(n²) double loop.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;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;nums&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="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;need&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;nums&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seen&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;need&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;seen&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;need&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&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;seen&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;nums&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// no pair sums to target&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing there is actually fancy. What makes it a senior answer is really everything around it; the "I'll start with the double loop and then trade space for time" narration, the comment that explains why rather than what, and the null case that's handled instead of just quietly ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistakes that sink senior candidates
&lt;/h2&gt;

&lt;p&gt;The first one is going silent, and it's honestly the most damaging thing on this whole list. If you're thinking, then think out loud; an interviewer just can't give you partial credit for reasoning they can't actually hear, and they can't redirect you when they have no idea where you currently are.&lt;/p&gt;

&lt;p&gt;The second is optimizing too early. Jumping straight to the clever solution and getting it slightly wrong tends to read worse than just building up methodically. I've watched candidates try to reproduce the optimal answer from memory, get exactly one boundary condition wrong, and then completely struggle to debug it, mostly because they'd memorized the answer without ever really understanding it.&lt;/p&gt;

&lt;p&gt;The third is treating clarifying questions as optional. At this level the ambiguity genuinely is part of the test, and often the real work is just noticing that the problem is underspecified and asking the one question that pins it down.&lt;/p&gt;

&lt;p&gt;And the last one is ignoring the interviewer's hints. When they ask something like "is there a way to avoid sorting here?", they're effectively handing you the answer, so just take it; defending your own approach while the interviewer is clearly steering you elsewhere is a real red flag about how you'd handle code review later.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually prepare (without wasting three months)
&lt;/h2&gt;

&lt;p&gt;I honestly don't believe in grinding 500 problems. I've never once really seen it correlate with stronger offers, and it just burns people out. A few other things tend to work a lot better than that.&lt;/p&gt;

&lt;p&gt;Learn the patterns, not the individual problems. There are really only around 15 that matter: two pointers, sliding window, hashing, BFS/DFS, binary search, heaps, dynamic programming, and a handful of others. Once you learn to recognize which pattern a question is really asking for, you can derive the solution on the spot instead of trying to recall it from memory, and building that recognition is basically what the rest of this series is going to be about.&lt;/p&gt;

&lt;p&gt;Do fewer problems, but actually do them properly. 40 problems you can explain line by line will take you a lot further than 200 you only half-remember. After each one, close the editor and try to re-explain the whole approach out loud; if you can't do that, then you didn't really learn it.&lt;/p&gt;

&lt;p&gt;Practice talking while you're coding. This one is high-impact and almost always skipped. Record yourself solving a problem out loud; it feels genuinely awkward, but it exposes those long silences you fall into whenever you actually stop to think, which are exactly the thing that hurts you in a real interview.&lt;/p&gt;

&lt;p&gt;Simulate the pressure too. Solving a problem calmly on your own is honestly a totally different skill from solving it while somebody else watches you do it. Do mock interviews, and just get used to being a little uncomfortable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tell people the night before
&lt;/h2&gt;

&lt;p&gt;Sleep. A rested brain that actually knows 5 patterns will easily outperform an exhausted one that crammed 20 the night before.&lt;/p&gt;

&lt;p&gt;And try to reframe the whole situation while you're at it. You're not being interrogated here. You're pair-programming with someone who just wants to see how you actually work. The best interviews I've ever had genuinely felt like two engineers just solving a problem together, and that's the tone to aim for rather than some kind of performance.&lt;/p&gt;

&lt;p&gt;You honestly don't need to be perfect. You need to be clear, upfront about the things you don't know, and steady when the problem gets hard. That's really what senior looks like from the other side of the table.&lt;/p&gt;

&lt;p&gt;Next up in this series, we get quite a bit more concrete with the arrays and hashing patterns, which show up in more interviews than just about anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Senior interviews really test how you think and communicate, and actually reaching the optimal answer is almost secondary to that.&lt;/li&gt;
&lt;li&gt;Run the same loop every single time: clarify, state the naive approach, find and fix the bottleneck, write it cleanly, and then test it yourself.&lt;/li&gt;
&lt;li&gt;Talking out loud is the highest-impact skill here, and also the most neglected one.&lt;/li&gt;
&lt;li&gt;Learn the ~15 core patterns so you can derive solutions instead of just memorizing individual problems.&lt;/li&gt;
&lt;li&gt;Take the interviewer's hints, because how you respond to a bit of steering shows how you'd actually take code review.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How many LeetCode problems should I solve for a senior interview?&lt;/strong&gt;&lt;br&gt;
Honestly fewer than you'd probably guess. Something like 40 to 60 that you can genuinely explain, decision by decision, will beat hundreds that you only half-remember; depth and pattern recognition matter here far more than the raw count of problems ever does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do senior engineers still get asked LeetCode-style questions?&lt;/strong&gt;&lt;br&gt;
Often, yes, especially at the bigger companies. But the bar itself moves; it shifts away from "did you actually solve it" and toward "how did you reason, communicate, and handle the ambiguity", so you should expect a lot more follow-ups on tradeoffs and on scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the most common reason strong engineers fail coding interviews?&lt;/strong&gt;&lt;br&gt;
They go quiet. Under pressure they just stop narrating what they're doing, and the interviewer is then left unable to credit reasoning they can't actually hear, or to steer someone who has basically gone dark on them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I ask clarifying questions even if the problem seems clear?&lt;/strong&gt;&lt;br&gt;
Yes, honestly you should. Spotting the ambiguity and asking the right question is a real part of what's being graded here; it reads as experience, and it also quietly stops you from very beautifully solving the wrong problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I prepare for the "talk while coding" part?&lt;/strong&gt;&lt;br&gt;
Record yourself. Solve a few problems out loud, alone, and then do some mock interviews with a real person; it genuinely feels awkward, but it drags out that silent-thinking habit that quietly sinks so many otherwise-strong candidates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next in this series: &lt;a href="https://amanksingh.com/blog/arrays-hashing-interview-patterns" rel="noopener noreferrer"&gt;Arrays &amp;amp; Hashing: The Interview Patterns You Must Know&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Aman Singh&lt;/strong&gt; — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.&lt;/p&gt;

&lt;p&gt;I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://amanksingh.com" rel="noopener noreferrer"&gt;https://amanksingh.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/amansingh1501" rel="noopener noreferrer"&gt;https://github.com/amansingh1501&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product: &lt;a href="https://vowerole.com" rel="noopener noreferrer"&gt;https://vowerole.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email: &lt;a href="mailto:aman97aman@gmail.com"&gt;aman97aman@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>career</category>
      <category>dsa</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Setting Up NestJS the Right Way</title>
      <dc:creator>Aman Kumar Singh</dc:creator>
      <pubDate>Tue, 21 Jul 2026 06:58:32 +0000</pubDate>
      <link>https://dev.to/moose978/setting-up-nestjs-the-right-way-30d</link>
      <guid>https://dev.to/moose978/setting-up-nestjs-the-right-way-30d</guid>
      <description>&lt;p&gt;Last time we set up the Next.js frontend in &lt;code&gt;apps/web&lt;/code&gt;. Now the other half: the NestJS API in &lt;code&gt;apps/api&lt;/code&gt;, the service that actually owns the data those shared types describe.&lt;/p&gt;

&lt;p&gt;Like the frontend, there's a thirty-second version of this (&lt;code&gt;nest new api&lt;/code&gt;, done) and a version that saves you real pain later. NestJS is interesting because it hands you a lot of structure on day one, so the setup work here is mostly about restraint. You turn on the few things every production API genuinely needs, and you resist scaffolding the enterprise patterns you won't touch for months.&lt;/p&gt;

&lt;p&gt;This is part four of the &lt;a href="https://amanksingh.com/series/full-stack-saas-masterclass" rel="noopener noreferrer"&gt;Full Stack SaaS Masterclass&lt;/a&gt;, continuing from the &lt;a href="https://amanksingh.com/blog/nextjs-project-setup-guide" rel="noopener noreferrer"&gt;Next.js app&lt;/a&gt; we set up last time. The NestJS app lives in &lt;code&gt;apps/api&lt;/code&gt; and shares types with the frontend through the same &lt;code&gt;packages/types&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modules: the one concept to get right
&lt;/h2&gt;

&lt;p&gt;Everything in NestJS hangs off modules, and getting your mental model right here is most of the battle. A module groups one feature: its controller, its service, and whatever providers that feature needs. The framework wires them together with dependency injection.&lt;/p&gt;

&lt;p&gt;The mistake I see most often is at both extremes. Some people stuff the entire app into &lt;code&gt;AppModule&lt;/code&gt; and end up with a tangle. Others create a module per file and drown in ceremony. The rule that works: one module per real domain. A &lt;code&gt;UsersModule&lt;/code&gt;, an &lt;code&gt;AuthModule&lt;/code&gt;, a &lt;code&gt;BillingModule&lt;/code&gt;, each owning its own controller and service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/modules/users/users.module.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Module&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;UsersController&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;./users.controller&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;UsersService&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;./users.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;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="na"&gt;exports&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="c1"&gt;// let other modules use it&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;Start with one or two feature modules and add more as real domains appear. The structure grows out of the product, not out of a diagram you drew before writing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup that matters
&lt;/h2&gt;

&lt;p&gt;Inside &lt;code&gt;apps/api&lt;/code&gt;, here's what I configure before writing a single endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript strict.&lt;/strong&gt; Same rule as the frontend, non-negotiable. Turn on &lt;code&gt;strict&lt;/code&gt; in &lt;code&gt;tsconfig.json&lt;/code&gt; from the first file. A backend that grew up loose is even worse to fix than a frontend, because the bugs hide in data handling where they hurt most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Global validation.&lt;/strong&gt; This is the single most important production default in the whole setup. Every request body that reaches your handlers should already be validated and typed. NestJS makes this one block of code in &lt;code&gt;main.ts&lt;/code&gt;, paired with DTOs that declare the rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/main.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;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;ValidationPipe&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useGlobalPipes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ValidationPipe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;whitelist&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="c1"&gt;// strip properties not in the DTO&lt;/span&gt;
      &lt;span class="na"&gt;forbidNonWhitelisted&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="c1"&gt;// reject unknown properties outright&lt;/span&gt;
      &lt;span class="na"&gt;transform&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="c1"&gt;// turn plain payloads into DTO instances&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;bootstrap&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="c1"&gt;// src/modules/users/dto/create-user.dto.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;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;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;IsString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;MinLength&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;whitelist&lt;/code&gt; and &lt;code&gt;forbidNonWhitelisted&lt;/code&gt; on, a request carrying an extra &lt;code&gt;isAdmin: true&lt;/code&gt; field it shouldn't have gets rejected before your code ever sees it. That's a security default, not just a tidiness one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Config, loaded once.&lt;/strong&gt; Reaching for &lt;code&gt;process.env&lt;/code&gt; all over the codebase is how you end up shipping with an undefined secret and no idea which of forty files reads it. Use &lt;code&gt;@nestjs/config&lt;/code&gt; to load and validate environment variables once, then inject them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/app.module.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Module&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ConfigModule&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/config&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;UsersModule&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;./modules/users/users.module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;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;ConfigModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forRoot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;isGlobal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;UsersModule&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;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;We'll do proper environment-variable handling, including validation and typed access, in its own article later in this module. For now, loading it in one place is the habit that matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serve the shared types.&lt;/strong&gt; This is the monorepo paying off on the backend. Your controller returns the same &lt;code&gt;User&lt;/code&gt; type the frontend imports, so the contract between the two sides is a single definition rather than two that drift.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/modules/users/users.controller.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Param&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nestjs/common&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@my-saas/types&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;UsersService&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;./users.service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;users&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;getOne&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="nb"&gt;Promise&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="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;users&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;If the backend changes what a &lt;code&gt;User&lt;/code&gt; is, both this file and the frontend that consumes it stop compiling until they're updated. The mismatch becomes a build error instead of a production bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path aliases.&lt;/strong&gt; Same as the frontend. Set up &lt;code&gt;@/&lt;/code&gt; so imports stay readable and survive file moves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tsconfig.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"baseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"paths"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"@/*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"./src/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A sane module structure
&lt;/h2&gt;

&lt;p&gt;Here's the layout I use inside &lt;code&gt;apps/api&lt;/code&gt;. It scales from three endpoints to three hundred without a reorganization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/api/src/
├── modules/          # one folder per feature (users, auth, billing)
│   └── users/
│       ├── dto/
│       ├── users.controller.ts
│       ├── users.service.ts
│       └── users.module.ts
├── common/           # cross-cutting: guards, filters, interceptors
├── config/           # config schema and helpers
├── app.module.ts
└── main.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;common&lt;/code&gt; folder is where shared infrastructure lives: an exception filter that shapes error responses consistently, an auth guard, a logging interceptor. Keeping those out of feature modules stops the same boilerplate from being copy-pasted into every corner of the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to build yet
&lt;/h2&gt;

&lt;p&gt;NestJS documents a lot of advanced machinery, and the temptation on day one is to wire up all of it. Resist that, the same way we resisted extra libraries on the frontend.&lt;/p&gt;

&lt;p&gt;You don't need microservices. Start as a modular monolith and split a service out later if a boundary genuinely demands it, which for most SaaS products never happens. You don't need CQRS or event sourcing to save a user's name to a table. You don't need a queue until you have work that runs outside the request cycle, and when you do, BullMQ slots in cleanly later in the series. You don't need custom decorators everywhere; the built-in ones cover almost everything.&lt;/p&gt;

&lt;p&gt;Every one of those patterns solves a real problem you probably don't have yet. Add them the day the problem shows up, with the context to use them well, rather than guessing now.&lt;/p&gt;

&lt;p&gt;Next in the series, we give this API something to talk to: PostgreSQL, and how to model and connect it in a way you won't regret three migrations from now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Organize the app around one module per real domain; avoid both the god-module and the module-per-file extremes.&lt;/li&gt;
&lt;li&gt;Turn on TypeScript &lt;code&gt;strict&lt;/code&gt; from the first file, exactly as on the frontend.&lt;/li&gt;
&lt;li&gt;A global &lt;code&gt;ValidationPipe&lt;/code&gt; with &lt;code&gt;whitelist&lt;/code&gt; and DTOs is the most important production default, and it doubles as a security control.&lt;/li&gt;
&lt;li&gt;Load environment variables once with &lt;code&gt;@nestjs/config&lt;/code&gt; instead of scattering &lt;code&gt;process.env&lt;/code&gt; through the code.&lt;/li&gt;
&lt;li&gt;Return the shared monorepo types from your controllers so the API contract can't silently drift from the frontend.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How should I structure a NestJS project?&lt;/strong&gt;&lt;br&gt;
Group code by feature into modules, one per real domain such as users, auth, or billing, each with its own controller and service. Keep cross-cutting infrastructure like guards and exception filters in a shared &lt;code&gt;common&lt;/code&gt; folder. Let new modules appear as new domains do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the most important thing to configure in a new NestJS app?&lt;/strong&gt;&lt;br&gt;
A global &lt;code&gt;ValidationPipe&lt;/code&gt; with &lt;code&gt;whitelist: true&lt;/code&gt;, paired with DTOs using class-validator. It guarantees every request body is validated and stripped of unexpected fields before it reaches your handlers, which is both a correctness and a security win.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should I use NestJS microservices from the start?&lt;/strong&gt;&lt;br&gt;
No. Begin with a modular monolith. Microservices add deployment and operational complexity you haven't earned at zero users. You can extract a service later if a clear boundary demands it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I share types between NestJS and my frontend?&lt;/strong&gt;&lt;br&gt;
Put the types in a shared monorepo package and import them on both sides. When a controller returns that shared type, any change to it forces both the API and the frontend to update, so the contract stays in sync.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why use @nestjs/config instead of process.env directly?&lt;/strong&gt;&lt;br&gt;
It loads and validates environment variables in one place and lets you inject typed config where you need it. Reading &lt;code&gt;process.env&lt;/code&gt; throughout the codebase scatters that dependency and makes missing or misnamed variables hard to catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous: &lt;a href="https://amanksingh.com/blog/nextjs-project-setup-guide" rel="noopener noreferrer"&gt;Setting Up Your Next.js App the Right Way&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Next: Setting Up PostgreSQL the Right Way&lt;/li&gt;
&lt;li&gt;Start of series: &lt;a href="https://amanksingh.com/blog/choosing-the-right-tech-stack-for-saas" rel="noopener noreferrer"&gt;Choosing the Right Tech Stack for Your SaaS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Aman Singh&lt;/strong&gt; — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.&lt;/p&gt;

&lt;p&gt;I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://amanksingh.com" rel="noopener noreferrer"&gt;https://amanksingh.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/amansingh1501" rel="noopener noreferrer"&gt;https://github.com/amansingh1501&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product: &lt;a href="https://vowerole.com" rel="noopener noreferrer"&gt;https://vowerole.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email: &lt;a href="mailto:aman97aman@gmail.com"&gt;aman97aman@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>nestjs</category>
      <category>node</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
    <item>
      <title>Setting Up Your Next.js App the Right Way</title>
      <dc:creator>Aman Kumar Singh</dc:creator>
      <pubDate>Tue, 21 Jul 2026 06:58:30 +0000</pubDate>
      <link>https://dev.to/moose978/setting-up-your-nextjs-app-the-right-way-35bj</link>
      <guid>https://dev.to/moose978/setting-up-your-nextjs-app-the-right-way-35bj</guid>
      <description>&lt;p&gt;There's a version of "setting up Next.js" that takes thirty seconds: run &lt;code&gt;create-next-app&lt;/code&gt;, done. And for a weekend project, that's genuinely all you need. But we're building a SaaS meant to live for years, inside a monorepo, sharing types with a backend. The choices you make in the first hour here quietly shape how the next hundred hours feel.&lt;/p&gt;

&lt;p&gt;So this isn't a "run this command" article. It's the handful of setup decisions that actually matter, and the ones people agonize over that don't.&lt;/p&gt;

&lt;p&gt;This is part three of the &lt;a href="https://amanksingh.com/series/full-stack-saas-masterclass" rel="noopener noreferrer"&gt;Full Stack SaaS Masterclass&lt;/a&gt;, continuing from the &lt;a href="https://amanksingh.com/blog/monorepo-with-turborepo" rel="noopener noreferrer"&gt;Turborepo monorepo&lt;/a&gt; we set up last time. The Next.js app lives in &lt;code&gt;apps/web&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  App Router or Pages Router?
&lt;/h2&gt;

&lt;p&gt;This is the first fork, and I'll give you a straight answer: for a new project in 2023, use the App Router. The Pages Router isn't bad. It's stable, and a lot of great apps run on it. But the App Router is where React and Next are clearly heading, and starting a multi-year project on the path that's being deprioritized is borrowing trouble.&lt;/p&gt;

&lt;p&gt;The App Router's real advantage for a SaaS is Server Components. Your dashboard can fetch data on the server, render it to HTML, and ship less JavaScript to the browser. For an app with a lot of data-heavy pages, that's faster loads and better SEO on your marketing pages, more or less for free.&lt;/p&gt;

&lt;p&gt;There's a learning curve. Server Components versus Client Components trips everyone up at first. I'll cover that properly in the React series later. For now: it's worth the curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup that matters
&lt;/h2&gt;

&lt;p&gt;Inside &lt;code&gt;apps/web&lt;/code&gt;, here's what I actually configure before writing any features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeScript, strict.&lt;/strong&gt; Non-negotiable. Turn on &lt;code&gt;strict&lt;/code&gt; mode in &lt;code&gt;tsconfig.json&lt;/code&gt; from the very first file. Retrofitting strictness onto a codebase that grew up loose is miserable. I've done it: it's weeks of &lt;code&gt;any&lt;/code&gt; whack-a-mole. Pay the small tax now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path aliases.&lt;/strong&gt; Set up &lt;code&gt;@/&lt;/code&gt; to point at your source root so imports stay readable as the app grows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tsconfig.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"baseUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"paths"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"@/*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"./src/*"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference this makes six months in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// without aliases, from a deeply nested file&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;Button&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;../../../../components/ui/Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// with aliases, from anywhere&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;Button&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;@/components/ui/Button&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;That second version doesn't break when you move the file. The first one does, and you won't notice until runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consume the shared types.&lt;/strong&gt; This is the monorepo paying off. Import the &lt;code&gt;User&lt;/code&gt; type from the shared package instead of redefining 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;import&lt;/span&gt; &lt;span class="kd"&gt;type&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="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@my-saas/types&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;getProfile&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&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;API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/me`&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if the backend changes the &lt;code&gt;User&lt;/code&gt; shape, this file won't compile until you deal with it. That's the safety net we built the monorepo for, doing its job on the frontend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tailwind for styling.&lt;/strong&gt; An opinion, but a well-worn one. For a SaaS you'll build a lot of custom UI, and Tailwind lets you style without inventing a naming system or context-switching to CSS files. If you or your team genuinely prefer CSS Modules or styled-components, that's a fine choice too. This is one of those reversible decisions not worth a war over.&lt;/p&gt;

&lt;h2&gt;
  
  
  A sane folder structure
&lt;/h2&gt;

&lt;p&gt;People burn absurd amounts of time on folder structure. Here's one that works and that you can stop thinking about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apps/web/src/
├── app/              # App Router routes
├── components/
│   ├── ui/           # dumb, reusable primitives (Button, Input)
│   └── features/     # feature-specific components
├── lib/              # helpers, API client, utilities
├── hooks/            # custom React hooks
└── styles/           # global css, tailwind entry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one distinction I'd insist on is &lt;code&gt;ui&lt;/code&gt; versus &lt;code&gt;features&lt;/code&gt;. &lt;code&gt;ui&lt;/code&gt; holds generic primitives that know nothing about your app. A &lt;code&gt;Button&lt;/code&gt; doesn't care whether it's on a billing page. &lt;code&gt;features&lt;/code&gt; holds components that do: a &lt;code&gt;BillingSummary&lt;/code&gt; that knows about invoices. Keeping those separate stops your reusable primitives from slowly getting tangled up in business logic, which is how component libraries rot.&lt;/p&gt;

&lt;p&gt;Everything else, don't overthink. You can always move a file. You cannot always undo a &lt;code&gt;strict: false&lt;/code&gt; codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to install yet
&lt;/h2&gt;

&lt;p&gt;The instinct on day one is to add every library you &lt;em&gt;might&lt;/em&gt; need. Resist it. You don't yet know if you need a state management library. Server Components and React's built-in state cover more than they used to. You don't need a data-fetching library until you have data to fetch. You don't need a form library until you have a form.&lt;/p&gt;

&lt;p&gt;Each dependency is a thing to learn, update, and occasionally get burned by. Add them when a real need shows up, not in anticipation. The &lt;code&gt;create-next-app&lt;/code&gt; default plus TypeScript, Tailwind, and your shared types is a complete enough starting point to build real features on.&lt;/p&gt;

&lt;p&gt;Next in the series, we set up the other side: the NestJS backend that serves those shared types, inside the same monorepo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For a new multi-year project, start on the App Router. It's where Next.js is heading, and Server Components cut shipped JavaScript.&lt;/li&gt;
&lt;li&gt;Turn on TypeScript &lt;code&gt;strict&lt;/code&gt; from the first file; retrofitting it later is painful.&lt;/li&gt;
&lt;li&gt;Set up &lt;code&gt;@/&lt;/code&gt; path aliases so imports stay stable when you move files.&lt;/li&gt;
&lt;li&gt;Import shared types from the monorepo package so frontend and backend can't drift.&lt;/li&gt;
&lt;li&gt;Keep reusable &lt;code&gt;ui&lt;/code&gt; primitives separate from &lt;code&gt;features&lt;/code&gt; components, and don't install libraries until a real need appears.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Should I use the App Router or Pages Router for a new Next.js app?&lt;/strong&gt;&lt;br&gt;
Use the App Router for new projects. It's the direction Next.js is investing in, and Server Components reduce client-side JavaScript and improve SEO. The Pages Router still works but is the legacy path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why enable TypeScript strict mode from the start?&lt;/strong&gt;&lt;br&gt;
Strict mode catches whole categories of bugs at compile time. Adding it to an existing loose codebase means fixing a flood of errors all at once, so it's far cheaper to start strict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What folder structure should a Next.js SaaS use?&lt;/strong&gt;&lt;br&gt;
A simple one: &lt;code&gt;app&lt;/code&gt; for routes, &lt;code&gt;components/ui&lt;/code&gt; for generic primitives, &lt;code&gt;components/features&lt;/code&gt; for app-specific components, plus &lt;code&gt;lib&lt;/code&gt;, &lt;code&gt;hooks&lt;/code&gt;, and &lt;code&gt;styles&lt;/code&gt;. Keep reusable primitives free of business logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a state management library like Redux?&lt;/strong&gt;&lt;br&gt;
Not to start. React's built-in state and Server Components handle a lot now. Add a state library only when you hit genuine shared-state complexity that local state can't handle cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I share types between my Next.js app and backend?&lt;/strong&gt;&lt;br&gt;
Put the types in a shared monorepo package and import them on both sides. Any change to a type then forces both apps to update, preventing silent mismatches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Previous: &lt;a href="https://amanksingh.com/blog/monorepo-with-turborepo" rel="noopener noreferrer"&gt;Setting Up a Monorepo with Turborepo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Next: &lt;a href="https://amanksingh.com/blog/nestjs-project-setup-guide" rel="noopener noreferrer"&gt;Setting Up NestJS the Right Way&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Start of series: &lt;a href="https://amanksingh.com/blog/choosing-the-right-tech-stack-for-saas" rel="noopener noreferrer"&gt;Choosing the Right Tech Stack for Your SaaS&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About the Author
&lt;/h2&gt;

&lt;p&gt;Hi, I'm &lt;strong&gt;Aman Singh&lt;/strong&gt; — Senior Full Stack Engineer specializing in scalable SaaS products, distributed systems, cloud architecture, and AI-powered applications.&lt;/p&gt;

&lt;p&gt;I write about System Design, Full Stack Engineering, Distributed Systems, Redis, PostgreSQL, AWS, Node.js, and NestJS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio: &lt;a href="https://amanksingh.com" rel="noopener noreferrer"&gt;https://amanksingh.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/amansingh1501" rel="noopener noreferrer"&gt;https://github.com/amansingh1501&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Product: &lt;a href="https://vowerole.com" rel="noopener noreferrer"&gt;https://vowerole.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email: &lt;a href="mailto:aman97aman@gmail.com"&gt;aman97aman@gmail.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>nextjs</category>
      <category>react</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
