<?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: Ajeet Chaulagain</title>
    <description>The latest articles on DEV Community by Ajeet Chaulagain (@ajeetchaulagain).</description>
    <link>https://dev.to/ajeetchaulagain</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1357234%2F630101ac-6d27-44f6-bcd2-c03720862614.jpeg</url>
      <title>DEV Community: Ajeet Chaulagain</title>
      <link>https://dev.to/ajeetchaulagain</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ajeetchaulagain"/>
    <language>en</language>
    <item>
      <title>Inside a 3-app Turborepo monorepo: parallelism, caching, and CI that stays fast</title>
      <dc:creator>Ajeet Chaulagain</dc:creator>
      <pubDate>Wed, 13 May 2026 01:53:40 +0000</pubDate>
      <link>https://dev.to/ajeetchaulagain/inside-a-3-app-turborepo-monorepo-parallelism-caching-and-ci-that-stays-fast-2040</link>
      <guid>https://dev.to/ajeetchaulagain/inside-a-3-app-turborepo-monorepo-parallelism-caching-and-ci-that-stays-fast-2040</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://ajeetchaulagain.com/blog/turborepo-monorepo-ci/" rel="noopener noreferrer"&gt;ajeetchaulagain.com&lt;/a&gt; on May 13, 2026.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I've been building &lt;a href="https://shipwindow.dev/" rel="noopener noreferrer"&gt;ShipWindow&lt;/a&gt; for a few months now — &lt;strong&gt;deliberately slowly, with a production mindset from day one&lt;/strong&gt;. No users yet, but real architecture, real CI, infrastructure-as-code.&lt;/p&gt;

&lt;p&gt;"Ship fast, refactor later" might be the usual call for a side project like this. But I wanted to try balancing it with a production mindset as I went — &lt;strong&gt;still shipping, but thinking a bit further ahead while I did&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The result has been a mix. Some of the production-minded choices have paid off — the CI work I'm about to walk through is one of them. This post is mostly about the part that paid off.&lt;/p&gt;

&lt;p&gt;CI is where that mindset showed up early. When the project was in its early phase, I had a minimal workflow validating each PR — &lt;strong&gt;lint, type-check, and tests&lt;/strong&gt; running one after another, sequentially. It was fine for the time. But as the project grew, so did the workflow. As of writing this, the same CI runs across &lt;strong&gt;3 apps and 4 packages in around 2 minutes 30 seconds on most pushes&lt;/strong&gt; — and it's set up to scale with the project rather than slow down as more code lands.&lt;/p&gt;

&lt;p&gt;Three apps, a few shared packages, every push rebuilding everything. You'd expect CI to be slow on a setup like this — that was certainly my starting point. It turned out it doesn't have to be, and &lt;strong&gt;the confidence that gives me when merging changes across stacks is honestly the bigger win&lt;/strong&gt;. I'll walk through how it works, and the decisions that got it there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the repo
&lt;/h2&gt;

&lt;p&gt;The high-level structure of the repo looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;shipwindow/
├── apps/
│   ├── web/          &lt;span class="c"&gt;# Next.js 16 — authenticated dashboard&lt;/span&gt;
│   ├── site/         &lt;span class="c"&gt;# Next.js 16 — landing page&lt;/span&gt;
│   └── api/          &lt;span class="c"&gt;# NestJS — webhook ingestion + auth&lt;/span&gt;
├── packages/
│   ├── ui/           &lt;span class="c"&gt;# Shared component library (Tailwind v4)&lt;/span&gt;
│   ├── shared-types/ &lt;span class="c"&gt;# Types shared web ↔ api&lt;/span&gt;
│   ├── eslint-config/
│   └── typescript-config/
├── infra/            &lt;span class="c"&gt;# AWS CDK stacks&lt;/span&gt;
├── turbo.json        &lt;span class="c"&gt;# Task graph + cache config&lt;/span&gt;
└── package.json      &lt;span class="c"&gt;# Yarn workspaces declaration&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three apps live under &lt;code&gt;apps/&lt;/code&gt; — each one is something that gets deployed independently. Four shared packages live under &lt;code&gt;packages/&lt;/code&gt; — these are libraries the apps import from, but nothing ships them on their own. Infrastructure lives in &lt;code&gt;infra/&lt;/code&gt; (stacks written with &lt;a href="https://aws.amazon.com/cdk/" rel="noopener noreferrer"&gt;AWS CDK&lt;/a&gt;), kept separate from the application code because it has its own lifecycle and tooling.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://yarnpkg.com/features/workspaces" rel="noopener noreferrer"&gt;Yarn workspaces&lt;/a&gt; stitch the whole thing together as one repo — when &lt;code&gt;apps/web&lt;/code&gt; imports &lt;code&gt;@shipwindow/ui&lt;/code&gt;, it resolves to the local source directly, no publish step in between. &lt;a href="https://turborepo.dev/" rel="noopener noreferrer"&gt;Turborepo&lt;/a&gt; sits on top of workspaces and orchestrates the task running — knowing what to build in what order, what to cache, and what to skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a monorepo
&lt;/h2&gt;

&lt;p&gt;When I started thinking about ShipWindow's setup, my first instinct was actually to split into multiple repos. It felt safer — less tooling to figure out, less to think about on day one. I'd worked in a monorepo before and knew the upfront cost: the first few weeks of "what goes where" decisions, the conventions to enforce on a project.&lt;/p&gt;

&lt;p&gt;But eventually I was willing to invest that time upfront, knowing it would pay off as the project grew. A few things pushed me in that direction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Past experience.&lt;/strong&gt; I'd worked in a monorepo on a previous project and it had served me well. I also remembered the alternative — publish a package, bump the version, install, redeploy, every time anything shared changed. Not something I wanted to live through again on a side project where I wanted to move fast without the overhead of versioning and publishing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Atomic refactors.&lt;/strong&gt; Shipping solo, I wanted to move quickly without juggling contracts across repos. When I add a new field to a type in &lt;code&gt;packages/shared-types&lt;/code&gt;, both &lt;code&gt;apps/web&lt;/code&gt; and &lt;code&gt;apps/api&lt;/code&gt; get the change in the same PR. No version bump, no broken contracts in production. One PR, done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One review, one diff.&lt;/strong&gt; Every change shows up against the full picture. If a frontend change needs an API endpoint, both land in the same PR — the contract is visible in one diff, not split across two repos with two CI runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared design tokens stay in sync.&lt;/strong&gt; &lt;code&gt;packages/ui&lt;/code&gt; exports brand colors, components, and CSS tokens. The day I rebrand and edit &lt;code&gt;brand.css&lt;/code&gt;, every app updates on the next build. No copy-paste, no drift.&lt;/p&gt;

&lt;p&gt;Working in a monorepo, &lt;strong&gt;the honest cost is discipline&lt;/strong&gt;. Without it, everything starts depending on everything, and you stop knowing what's safe to change. I've worked on a monorepo project before, and it's a pattern I've seen play out — especially if you haven't worked in one before and are still getting your head around it. The discipline lives in being deliberate about what belongs in a shared package versus what stays in an app, and honest about what each package is actually responsible for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Turborepo
&lt;/h2&gt;

&lt;p&gt;Once I'd decided on a monorepo, the next question was how to actually run things across it. Yarn workspaces handles the dependency graph — when &lt;code&gt;apps/web&lt;/code&gt; imports &lt;code&gt;@shipwindow/ui&lt;/code&gt;, it resolves to the local source without any publish step. That part is solved.&lt;/p&gt;

&lt;p&gt;But workspaces alone doesn't handle task orchestration — what to build first, what to cache, what to skip. For that, build tools like &lt;a href="https://lerna.js.org/" rel="noopener noreferrer"&gt;Lerna&lt;/a&gt;, &lt;a href="https://nx.dev/" rel="noopener noreferrer"&gt;Nx&lt;/a&gt;, or &lt;a href="https://turborepo.dev/" rel="noopener noreferrer"&gt;Turborepo&lt;/a&gt; are generally used. They sit on top of workspaces, not in place of them — you use both.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://turborepo.dev/" rel="noopener noreferrer"&gt;Turborepo&lt;/a&gt; describes itself as &lt;em&gt;"the build system for JavaScript and TypeScript codebases"&lt;/em&gt; — and it's maintained by Vercel, which matters here because &lt;strong&gt;their free remote cache&lt;/strong&gt; is one of the reasons I picked it. It's written in Rust, configured through a single &lt;code&gt;turbo.json&lt;/code&gt; file, and built around the task graph and caching model that most monorepo tools have converged on.&lt;/p&gt;

&lt;p&gt;On a previous project, I worked in a monorepo that used Lerna. I didn't pick it — the project had been set up before I joined — but I lived with it long enough to get a feel for it. Lerna was widely used for JS monorepos at the time.&lt;/p&gt;

&lt;p&gt;Turborepo is newer and its ecosystem is still growing, but its focus is squarely on builds and caching — which, for a side project where I don't publish anything externally but care a lot about CI speed, lined up better with what I needed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Worth flagging: Turborepo's &lt;a href="https://turborepo.dev/docs/crafting-your-repository" rel="noopener noreferrer"&gt;Crafting your repository&lt;/a&gt; docs cover structuring a monorepo, managing dependencies, configuring tasks, caching, and more — in real depth. Start there if you're setting up your first one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The apps and packages
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;apps/web&lt;/code&gt;&lt;/strong&gt; — authenticated dashboard, &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js 16&lt;/a&gt; App Router&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;apps/site&lt;/code&gt;&lt;/strong&gt; — landing page, statically rendered except for one server action&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;apps/api&lt;/code&gt;&lt;/strong&gt; — &lt;a href="https://nestjs.com/" rel="noopener noreferrer"&gt;NestJS&lt;/a&gt; backend, ingests GitHub webhooks, hosts auth endpoints&lt;br&gt;
Each app runs on a different port in dev, deploys to a different platform, and has its own scaling profile.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;packages/ui&lt;/code&gt;&lt;/strong&gt; — shared component library built with &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind v4&lt;/a&gt;, consumed directly from source by both Next.js apps&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;packages/shared-types&lt;/code&gt;&lt;/strong&gt; — single source of truth for the wire shape between web and api: &lt;code&gt;WebhookEvent&lt;/code&gt;, &lt;code&gt;PullRequest&lt;/code&gt;, &lt;code&gt;Review&lt;/code&gt;, etc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;packages/eslint-config&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;packages/typescript-config&lt;/code&gt;&lt;/strong&gt; — shared lint and TS configs; apps extend these so the rules stay consistent&lt;br&gt;
None of the packages have a publish step. They're consumed through the workspace graph at build time — which is the whole point of using workspaces in the first place.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Turborepo orchestrates everything
&lt;/h2&gt;

&lt;p&gt;Turborepo's job is to figure out &lt;strong&gt;what work needs doing, in what order, and what can be skipped&lt;/strong&gt;. It does all of that based on a single config file at the root of your repo: &lt;code&gt;turbo.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;turbo.json&lt;/code&gt; is where you describe the task graph — what tasks exist, what they depend on, what their inputs and outputs are. Here's a trimmed version of mine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tasks"&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;"build"&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;"dependsOn"&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;"^build"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&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;"$TURBO_DEFAULT$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".env*"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&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;".next/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"!.next/cache/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"generated/**"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NEXT_PUBLIC_BACKEND_URL"&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;"lint"&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;"dependsOn"&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;"^build"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"CI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RESEND_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VERCEL_ENV"&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;"dev"&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;"cache"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"persistent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things in here are doing most of the work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;dependsOn: ["^build"]&lt;/code&gt;&lt;/strong&gt; — the caret means "build all upstream packages first." So when I run &lt;code&gt;turbo run build&lt;/code&gt; in &lt;code&gt;apps/web&lt;/code&gt;, Turbo first builds &lt;code&gt;packages/ui&lt;/code&gt; and &lt;code&gt;packages/shared-types&lt;/code&gt;, then &lt;code&gt;apps/web&lt;/code&gt; itself. I never order tasks manually. Turbo walks the workspace graph for me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;a href="https://turborepo.dev/docs/reference/configuration#env" rel="noopener noreferrer"&gt;&lt;code&gt;env&lt;/code&gt; array&lt;/a&gt;&lt;/strong&gt; — this is the easy one to get wrong. Any environment variable a task reads but doesn't declare here gets silently ignored when Turbo computes the cache key.&lt;/p&gt;

&lt;p&gt;This env array was something that troubled me initially. The cache had quietly lied to me more than once before I understood what was happening — CI coming back green when it shouldn't have, stale results being served without anything flagging it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; list every env var your task actually reads to make a cache reliable and predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running apps locally
&lt;/h2&gt;

&lt;p&gt;To run every app, it's one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn dev                &lt;span class="c"&gt;# all apps in parallel&lt;/span&gt;
yarn dev &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;web   &lt;span class="c"&gt;# just one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hot reload across packages is what makes the monorepo feel worth it day to day. Change a button in &lt;code&gt;packages/ui&lt;/code&gt;, and the apps using it update immediately — no build step, no &lt;code&gt;npm link&lt;/code&gt;, no publish. &lt;strong&gt;It just works.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Under the hood, Yarn workspaces points each app at the package's actual folder on disk rather than a copy. So edits in &lt;code&gt;packages/ui&lt;/code&gt; count the same as edits inside the app — the dev server picks them up like any other file change.&lt;/p&gt;

&lt;p&gt;If you've worked in a multi-repo setup before, this is the part that quietly justifies the rest of the complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in CI: parallelism and caching
&lt;/h2&gt;

&lt;p&gt;Three apps, four packages, lint and type-check and tests on every push. Initially, my CI ran these sequentially — that's just how I'd set it up. While I was just starting out and in the early phase of adding features, it didn't matter much. The project was small, and sequential was simpler to reason about.&lt;/p&gt;

&lt;p&gt;The shift to parallel jobs wasn't &lt;strong&gt;really about speed&lt;/strong&gt;. Sequential CI was still fast enough at three apps and a handful of tests. The real reason was headroom for later: as features and tests grow, sequential adds up. Splitting tasks into parallel jobs also keeps the workflow predictable, and lets Turborepo handle the actual task ordering inside each one. That's cleaner than chaining steps in YAML and hoping the order holds.&lt;/p&gt;

&lt;p&gt;In practice, that means lint, type-check, tests, and cdk synth are each their own &lt;a href="https://docs.github.com/en/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt; job, depending on a shared install step. Once install finishes, all four run in parallel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwzet41crbljbhlcxrt7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvwzet41crbljbhlcxrt7.png" alt="GitHub Actions workflow showing install job followed by lint, type-check, test, and cdk-synth jobs running in parallel" width="735" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The four jobs after install all run side by side, so the pipeline finishes in roughly the time of the longest job, not the sum.&lt;/p&gt;

&lt;p&gt;Inside each of those jobs is where Turborepo does its work. In my setup, lint, type-check, and tests all &lt;code&gt;dependsOn: ["^build"]&lt;/code&gt; — meaning each of them needs the upstream packages built before it can run. I don't have to think about that. Turborepo walks the task graph, builds whatever's needed (or pulls it from the cache if it's already built), then runs lint, type-check, and tests on top. &lt;strong&gt;It figures out the right order so I don't have to script it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On the &lt;strong&gt;caching side&lt;/strong&gt;, that's where the actual savings come from. Turborepo tracks each task's inputs — source files, env vars, dependencies — and skips work it's seen before with identical inputs. The &lt;a href="https://vercel.com/docs/monorepos/remote-caching" rel="noopener noreferrer"&gt;Vercel Remote Cache&lt;/a&gt; makes this work across CI. Setup is two environment variables in your workflow: &lt;code&gt;TURBO_TOKEN&lt;/code&gt; (a Vercel access token) and &lt;code&gt;TURBO_TEAM&lt;/code&gt; (your team slug). When &lt;code&gt;npx turbo run &amp;lt;task&amp;gt;&lt;/code&gt; runs, Turbo reads those env vars from the environment and connects to the remote cache automatically. No other config needed.&lt;/p&gt;

&lt;p&gt;Here's what one of the parallel jobs looks like in my workflow — the lint job, for example:&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/workflows/pr-validate.yml&lt;/span&gt;

&lt;span class="c1"&gt;# ... yarn install job...&lt;/span&gt;

&lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Lint&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;install&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;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;TURBO_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.TURBO_TOKEN }}&lt;/span&gt;
    &lt;span class="na"&gt;TURBO_TEAM&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.TURBO_TEAM }}&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="m"&gt;24&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx turbo run lint&lt;/span&gt;

&lt;span class="c1"&gt;# ... type-check, test and cdk-synth jobs ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The structure is the same for each parallel job — &lt;code&gt;needs: install&lt;/code&gt;, then run a single Turbo task. The &lt;code&gt;TURBO_TOKEN&lt;/code&gt; and &lt;code&gt;TURBO_TEAM&lt;/code&gt; env vars are what let Turbo talk to the remote cache; without them, the job would run everything from scratch.&lt;/p&gt;

&lt;p&gt;For a typical PR in ShipWindow — say I add a new type &lt;code&gt;GitHubEvent&lt;/code&gt; in &lt;code&gt;packages/shared-types&lt;/code&gt; and update &lt;code&gt;apps/web&lt;/code&gt; to render it — Turborepo realizes &lt;code&gt;apps/site&lt;/code&gt; isn't affected. Its build is already cached, its lint result is already cached, its tests haven't changed. So those get pulled from the cache instead of being rerun. &lt;strong&gt;Only the work that actually changed runs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a solo project, the cache is already paying off — I can see most PRs hitting at least some cached tasks. With more contributors, the win gets bigger: builds run for one person's PR can be reused on someone else's, so the same work doesn't happen twice. Turborepo's job is to skip work that doesn't need redoing and to handle task order. GitHub Actions runs the jobs in parallel. Together, the CI stays relatively quick.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd improve
&lt;/h2&gt;

&lt;p&gt;One thing on the list for later: &lt;strong&gt;affected-only builds with &lt;a href="https://turborepo.dev/docs/reference/run#--filter-string" rel="noopener noreferrer"&gt;&lt;code&gt;--filter&lt;/code&gt;&lt;/a&gt;.&lt;/strong&gt; Turborepo supports running tasks only for the workspaces affected by changed files — &lt;code&gt;turbo run build --filter=...^...&lt;/code&gt; skips unaffected workspaces entirely instead of having Turbo do a cache lookup on each one.&lt;/p&gt;

&lt;p&gt;I'd be happy to explore it as the project grows. For now, caching is doing the heavy lifting and the savings would be marginal — it earns its place once the workspace is big enough that even the lookups start to add up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Most of what I've described here is roughly what I believe most Turborepo projects converge on. The patterns aren't novel — they just take a while to feel obvious.&lt;/p&gt;

&lt;p&gt;The bigger thing I'm taking from a few months of this: &lt;strong&gt;the engineering decisions that matter on a side project aren't really about being objectively right.&lt;/strong&gt; They're about giving yourself room to keep going — and to keep improving as you do. Monorepo over multi-repo, Turborepo over rolling my own scripts, caching over hoping CI stays fast — none of these are universal answers. They were the ones that kept me building features instead of fighting tooling.&lt;/p&gt;

&lt;p&gt;If you're curious what I'm building, take a look at &lt;a href="https://shipwindow.dev/" rel="noopener noreferrer"&gt;ShipWindow&lt;/a&gt;. Still pre-launch, waitlist's open if you want to be one of the first to try it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this useful, &lt;a href="https://ajeetchaulagain.com/" rel="noopener noreferrer"&gt;more posts at ajeetchaulagain.com&lt;/a&gt; on side projects and software.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>monorepo</category>
      <category>turborepo</category>
      <category>devops</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How to Deploy NestJS to AWS Lambda Using CDK and GitHub Actions</title>
      <dc:creator>Ajeet Chaulagain</dc:creator>
      <pubDate>Fri, 08 May 2026 01:31:00 +0000</pubDate>
      <link>https://dev.to/ajeetchaulagain/how-to-deploy-nestjs-to-aws-lambda-using-cdk-and-github-actions-3lf8</link>
      <guid>https://dev.to/ajeetchaulagain/how-to-deploy-nestjs-to-aws-lambda-using-cdk-and-github-actions-3lf8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://ajeetchaulagain.com/blog/nestjs-aws-lambda-cdk-deployment/" rel="noopener noreferrer"&gt;ajeetchaulagain.com&lt;/a&gt; on March 30, 2026.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Deploying a NestJS application to AWS Lambda behind API Gateway sounds straightforward — until you start wiring things together.&lt;/p&gt;

&lt;p&gt;Between adapting NestJS to a serverless runtime, configuring &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html" rel="noopener noreferrer"&gt;API Gateway&lt;/a&gt;, and setting up infrastructure with &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/home.html" rel="noopener noreferrer"&gt;AWS CDK&lt;/a&gt;, there are several moving parts that can quickly become messy. While working on a personal side project, I ran into these challenges firsthand — particularly around adapting NestJS to Lambda's execution model.&lt;/p&gt;

&lt;p&gt;By the end of this guide, you'll have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A NestJS app adapted for Lambda's execution model using &lt;code&gt;@codegenie/serverless-express&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Infrastructure defined as code: a Lambda function and HTTP API Gateway, provisioned with AWS CDK&lt;/li&gt;
&lt;li&gt;A GitHub Actions workflow that builds and deploys your stack automatically on every push to &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftvpvlk6q5gn8ctyuo64v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftvpvlk6q5gn8ctyuo64v.png" alt="Simple architecture diagram of NestJS deployment in AWS Lambda" width="735" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The complete source code is available on &lt;a href="https://github.com/ajeetchaulagain/nestjs-serverless-aws-lambda-cdk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;To follow along, you'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account&lt;/li&gt;
&lt;li&gt;Basic familiarity with Node.js and NestJS&lt;/li&gt;
&lt;li&gt;Basic familiarity with AWS services and tools (AWS CLI, IAM, Lambda, API Gateway, CloudFormation)&lt;/li&gt;
&lt;li&gt;A GitHub account (for setting up a repository and GitHub Actions)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need to be an AWS expert to follow along, but having a rough idea of how Lambda and API Gateway fit together will make things easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up a new NestJS project
&lt;/h2&gt;

&lt;p&gt;Start by creating a new NestJS application using the Nest CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @nestjs/cli
nest new nestjs-serverless-aws-cdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This scaffolds a standard NestJS project with all the necessary boilerplate.&lt;/p&gt;

&lt;p&gt;For a deeper understanding of NestJS concepts and architecture, I highly recommend checking out the &lt;a href="https://docs.nestjs.com/" rel="noopener noreferrer"&gt;NestJS official documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, start the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;nestjs-serverless-aws-cdk
npm run start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the application running at &lt;code&gt;http://localhost:3000/&lt;/code&gt;. The default port is configured in &lt;code&gt;src/main.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AppModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scaffolded project already includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A controller (&lt;code&gt;app.controller.ts&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A service (&lt;code&gt;app.service.ts&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A module (&lt;code&gt;app.module.ts&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since the focus of this guide is deployment, we'll reuse the default endpoint instead of creating new ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapting NestJS for AWS Lambda
&lt;/h2&gt;

&lt;p&gt;By default, a NestJS application runs on a long-lived HTTP server. It bootstraps once, initializes its dependencies, and continues handling incoming requests.&lt;/p&gt;

&lt;p&gt;AWS Lambda, on the other hand, follows an &lt;em&gt;event-driven model&lt;/em&gt; where code is executed per request in short-lived, stateless environments (with occasional cold starts).&lt;/p&gt;

&lt;p&gt;Because of this difference, NestJS cannot run directly in Lambda without some adaptation. To bridge this gap, we use &lt;a href="https://www.npmjs.com/package/@codegenie/serverless-express" rel="noopener noreferrer"&gt;@codegenie/serverless-express&lt;/a&gt;, which allows a NestJS application to run inside AWS Lambda.&lt;/p&gt;

&lt;p&gt;Under the hood, it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Converts the incoming Lambda event into an HTTP request&lt;/li&gt;
&lt;li&gt;Passes it to your NestJS application&lt;/li&gt;
&lt;li&gt;Transforms the response back into a Lambda-compatible format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In simple terms, it makes Lambda behave like an HTTP server so NestJS can run without major changes.&lt;/p&gt;

&lt;p&gt;Now that we understand the need for additional configuration, let's install the required packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @codegenie/serverless-express @types/aws-lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: &lt;code&gt;@types/aws-lambda&lt;/code&gt; provides the TypeScript types used in the handler.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, create a new file called &lt;code&gt;lambda.ts&lt;/code&gt; inside the &lt;code&gt;src/&lt;/code&gt; directory (alongside &lt;code&gt;main.ts&lt;/code&gt;). This will act as the entry point for your Lambda function.&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/lambda.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;ExpressAdapter&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/platform-express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;serverlessExpress&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;@codegenie/serverless-express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;APIGatewayProxyEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;APIGatewayProxyResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="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-lambda&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="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AsyncHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;APIGatewayProxyResult&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;let&lt;/span&gt; &lt;span class="nx"&gt;serverlessExpressInstance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AsyncHandler&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&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;APIGatewayProxyResult&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;expressApp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nestApp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;AppModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExpressAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expressApp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;nestApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enableCors&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;nestApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;serverlessExpressInstance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;serverlessExpress&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;expressApp&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="nx"&gt;unknown&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;AsyncHandler&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;serverlessExpressInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&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;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&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;APIGatewayProxyResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serverlessExpressInstance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;serverlessExpressInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&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 key things worth calling out in the code above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;serverlessExpressInstance&lt;/code&gt; is declared outside the handler and only populated on the first invocation. On subsequent invocations within the same Lambda container, the &lt;code&gt;if (serverlessExpressInstance)&lt;/code&gt; check skips the full NestJS bootstrap and reuses the existing instance. This is the standard pattern for reducing cold start overhead in Lambda.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;setup&lt;/code&gt; function is where NestJS bootstraps. It creates an Express app, wraps it with the NestJS &lt;code&gt;ExpressAdapter&lt;/code&gt;, and passes it to &lt;code&gt;serverlessExpress&lt;/code&gt;. The resulting instance is a function that accepts Lambda events and returns Lambda-compatible responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This handler follows the standard async/await pattern and works with modern AWS Lambda Node.js runtimes (including Node.js 20 and 24).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The AWS Lambda Node.js 24 runtime dropped support for the callback-style handler (&lt;code&gt;callback(null, response)&lt;/code&gt;) — using it will cause the function to hang until timeout. See the &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-nodejs.html" rel="noopener noreferrer"&gt;AWS Lambda Node.js runtime docs&lt;/a&gt; for details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Setting up infrastructure using AWS CDK
&lt;/h2&gt;

&lt;p&gt;Now that we have the Lambda handler ready to serve our NestJS application, let's set up the infrastructure using AWS CDK. AWS Cloud Development Kit (CDK) is an Infrastructure as Code (IaC) framework that allows you to define AWS resources using programming languages such as TypeScript, Python, Java, and more. Think of it like this: instead of writing raw CloudFormation templates, you write code that is synthesized into CloudFormation, which is then used to deploy resources to AWS.&lt;/p&gt;

&lt;p&gt;The high-level CDK flow looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;👨‍💻 Write CDK Code
        ↓
🧪 cdk synth
        ↓
📄 CloudFormation Template
        ↓
🚀 cdk deploy
        ↓
☁️ AWS Resources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, CDK makes infrastructure easier to reason about and maintain compared to raw templates.&lt;/p&gt;

&lt;h3&gt;
  
  
  CDK mental model (quick overview)
&lt;/h3&gt;

&lt;p&gt;Before we define our infrastructure, it helps to understand a few core CDK concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App&lt;/strong&gt; → The root container for your CDK application&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack&lt;/strong&gt; → A unit of deployment (maps to a CloudFormation stack)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Construct&lt;/strong&gt; → Building blocks used to define AWS resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this guide, we'll define a single stack that provisions our Lambda function, API Gateway, and related resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initializing CDK in the project
&lt;/h3&gt;

&lt;p&gt;Now that you've understood the basics of AWS CDK, let's initialize it in our project. To keep the infrastructure code separate from the NestJS source code, you'll initialize the CDK project into a separate subdirectory, &lt;code&gt;infra/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Run the following command from the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;infra &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;infra
npx cdk init app &lt;span class="nt"&gt;--language&lt;/span&gt; typescript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates the basic CDK project structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── bin/
├── lib/
├── cdk.json
├── package.json
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bin/&lt;/code&gt; contains the entry point of your CDK app&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lib/&lt;/code&gt; contains the stack definition for your infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cdk.json&lt;/code&gt; contains configuration for the CDK CLI&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Configuring AWS credentials (step by step)
&lt;/h3&gt;

&lt;p&gt;Before running any AWS CDK commands, you need to configure AWS credentials on your local machine.&lt;/p&gt;

&lt;p&gt;This allows AWS to authenticate your requests and perform actions on your behalf — such as deploying CloudFormation stacks and uploading assets.&lt;/p&gt;

&lt;p&gt;For example, when you run:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Your IAM identity needs permissions to create foundational resources like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CloudFormation stacks&lt;/li&gt;
&lt;li&gt;S3 buckets (for storing assets)&lt;/li&gt;
&lt;li&gt;IAM roles and policies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In production environments, it's best to follow the &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/getting-started-reduce-permissions.html" rel="noopener noreferrer"&gt;principle of least privilege&lt;/a&gt;, granting only the permissions required for a specific task.&lt;/p&gt;

&lt;p&gt;However, to keep this guide simple and focused, we'll use an IAM user with the &lt;strong&gt;&lt;code&gt;AdministratorAccess&lt;/code&gt;&lt;/strong&gt; policy attached.&lt;/p&gt;

&lt;p&gt;Follow these steps to configure AWS credentials:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Install AWS CLI
&lt;/h4&gt;

&lt;p&gt;Make sure AWS CLI is installed on your machine. Follow the &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;official installation guide&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Create an IAM user
&lt;/h4&gt;

&lt;p&gt;Create an IAM user with the AdministratorAccess policy and generate an access key for it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in to your AWS console&lt;/li&gt;
&lt;li&gt;Navigate to IAM → Users → Create User&lt;/li&gt;
&lt;li&gt;Create a user (e.g., &lt;code&gt;cdk-bootstrap-admin&lt;/code&gt;)

&lt;ul&gt;
&lt;li&gt;Attach the &lt;strong&gt;AdministratorAccess&lt;/strong&gt; policy&lt;/li&gt;
&lt;li&gt;Generate an &lt;strong&gt;access key&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Make sure to download the secret key for use later — you won't be able to view it again.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Run AWS Configure
&lt;/h4&gt;

&lt;p&gt;Run &lt;code&gt;aws configure&lt;/code&gt; to set up credentials for the AWS CLI so it can authenticate your requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll be prompted to enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;AWS&lt;/span&gt; &lt;span class="err"&gt;Access&lt;/span&gt; &lt;span class="err"&gt;Key&lt;/span&gt; &lt;span class="py"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="err"&gt;AWS&lt;/span&gt; &lt;span class="err"&gt;Secret&lt;/span&gt; &lt;span class="err"&gt;Access&lt;/span&gt; &lt;span class="py"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="err"&gt;Default&lt;/span&gt; &lt;span class="err"&gt;region&lt;/span&gt; &lt;span class="py"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;span class="err"&gt;Default&lt;/span&gt; &lt;span class="err"&gt;output&lt;/span&gt; &lt;span class="py"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the access keys you generated in the previous step.&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Verify credentials configuration
&lt;/h4&gt;

&lt;p&gt;To confirm AWS credentials are configured correctly, run the following AWS CLI command, which will show the details of your AWS account and IAM user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-caller-identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"UserId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UserId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AccountNumber"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Arn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::&amp;lt;AccountNumber&amp;gt;:user/cdk-bootstrap-admin"&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;h3&gt;
  
  
  Bootstrapping the CDK project
&lt;/h3&gt;

&lt;p&gt;Now that your AWS credentials are configured locally, you're ready to bootstrap your AWS environment for CDK.&lt;/p&gt;

&lt;p&gt;Bootstrapping prepares your AWS account so that AWS CDK can deploy resources on your behalf.&lt;/p&gt;

&lt;p&gt;Run the following command in your &lt;code&gt;infra/&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This command creates a foundational one-time CloudFormation stack (called &lt;code&gt;CDKToolkit&lt;/code&gt;) in your AWS account, which sets up required resources such as IAM roles and an S3 bucket to upload CloudFormation templates. These resources are needed for future CDK deployments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnhfiomq9dm5c862dldv1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnhfiomq9dm5c862dldv1.png" alt="Screenshot listing resources created by bootstrap in the CDKToolkit CloudFormation stack" width="735" height="353"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You only need to run &lt;code&gt;cdk bootstrap&lt;/code&gt; once per account and region.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Writing the CDK stack
&lt;/h3&gt;

&lt;p&gt;Now that you've bootstrapped your environment, you're ready to define your infrastructure using AWS CDK. In this section, you'll create a stack that provisions a Lambda function for your application.&lt;/p&gt;

&lt;p&gt;I'm also using a Lambda layer to bundle production dependencies. Separating &lt;code&gt;node_modules&lt;/code&gt; from the application code helps reduce the size of the deployment package and allows the same layer to be reused across multiple Lambda functions.&lt;/p&gt;

&lt;p&gt;Navigate to the &lt;code&gt;infra/lib&lt;/code&gt; directory, where you'll find a file named &lt;code&gt;infra-stack.ts&lt;/code&gt;. This file was generated during CDK project initialization. You can rename the stack if needed, but for this guide, we'll keep it as is.&lt;/p&gt;

&lt;p&gt;Add the following stack definition code to &lt;code&gt;infra/lib/infra-stack.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// infra/lib/infra-stack.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cdk&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-cdk-lib/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;Construct&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;constructs&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;lambda&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-cdk-lib/aws-lambda&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;apigwv2&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-cdk-lib/aws-apigatewayv2&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;integrations&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-cdk-lib/aws-apigatewayv2-integrations&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;path&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;path&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;InfraStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stack&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="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&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="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&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;nodeModulesLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayerVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NodeModulesLayer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../layer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="na"&gt;compatibleRuntimes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_24_X&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;description&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 node_modules for NestJS Lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nestApiLambda&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NestApiLambdaFunction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_24_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/lambda.handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../dist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;infra&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;tsconfig*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="na"&gt;memorySize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;nodeModulesLayer&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;httpApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;apigwv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HttpApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HttpApi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;defaultIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HttpLambdaIntegration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LambdaIntegration&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;nestApiLambda&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;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HttpApiUrl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;httpApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this setup, you're using a few core CDK constructs to define your infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lambda.Function&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lambda.LayerVersion&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apigwv2.HttpApi&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In CDK, constructs are the basic building blocks used to model infrastructure. If you haven't come across constructs before, the official docs are worth a quick read: &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/constructs.html" rel="noopener noreferrer"&gt;AWS CDK Constructs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now let's go through the different configuration parts in the code above.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda layer configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodeModulesLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayerVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NodeModulesLayer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../layer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="na"&gt;compatibleRuntimes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_24_X&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;description&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 node_modules for NestJS Lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code defines a Lambda layer using the &lt;code&gt;LayerVersion&lt;/code&gt; construct, where the code is loaded from a &lt;code&gt;layer/&lt;/code&gt; directory, which doesn't exist yet.&lt;/p&gt;

&lt;p&gt;To create it, add the following script to your root &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"build:lambda-layer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mkdir -p layer/nodejs &amp;amp;&amp;amp; cp package.json package-lock.json layer/nodejs/ &amp;amp;&amp;amp; npm ci --prefix layer/nodejs --omit=dev &amp;amp;&amp;amp; rm layer/nodejs/package.json layer/nodejs/package-lock.json"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since we don't need dev dependencies for our Lambda runtime, this script installs only production dependencies into &lt;code&gt;layer/nodejs/&lt;/code&gt;. In short, it copies the existing &lt;code&gt;package.json&lt;/code&gt; and lock file into &lt;code&gt;layer/nodejs/&lt;/code&gt; temporarily and installs production dependencies (&lt;code&gt;npm ci --omit=dev&lt;/code&gt;) that our application needs.&lt;/p&gt;

&lt;p&gt;Now, run that script from the project root to create a &lt;code&gt;/layer&lt;/code&gt; directory (we'll need this before deploying):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build:lambda-layer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also add &lt;code&gt;/layer/nodejs/node_modules&lt;/code&gt; to your &lt;code&gt;.gitignore&lt;/code&gt;. This directory should be generated during builds, not committed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda function configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nestApiLambda&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NestApiLambdaFunction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_24_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/lambda.handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&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="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../dist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;infra&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;tsconfig*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;memorySize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;nodeModulesLayer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code defines our Lambda function along with some configuration. Here's a breakdown of the key settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;runtime: lambda.Runtime.NODEJS_24_X&lt;/code&gt; — sets Node.js v24 as the runtime used by the Lambda function&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;handler: 'src/lambda.handler'&lt;/code&gt; — the entry point Lambda invokes. We created this handler previously in &lt;code&gt;src/lambda.ts&lt;/code&gt;. Since the application is deployed from the &lt;code&gt;dist&lt;/code&gt; directory which contains a &lt;code&gt;src&lt;/code&gt; folder, the handler is defined as &lt;code&gt;src/lambda.handler&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;code: lambda.Code.fromAsset(…)&lt;/code&gt; — packages the compiled application (&lt;code&gt;dist/&lt;/code&gt;) and uploads it as the deployment bundle&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;exclude: […]&lt;/code&gt; — skips unnecessary files from the bundle. I would strongly recommend being intentional here — smaller bundles lead to faster deployments. For example, I've excluded the &lt;code&gt;infra&lt;/code&gt; directory and &lt;code&gt;tsconfig*&lt;/code&gt; related files present in our &lt;code&gt;dist&lt;/code&gt; directory. The only files needed for the NestJS runtime are in &lt;code&gt;dist/src&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memorySize: 512&lt;/code&gt; — allocates 512 MB of memory to our Lambda runtime. NestJS has a heavier bootstrap compared to minimal handlers, so this tends to give better performance out of the box. Without &lt;code&gt;memorySize&lt;/code&gt;, it defaults to 128 MB.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;layers&lt;/code&gt; — attaches the layer defined above. This layer is merged into the function's filesystem at runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  HTTP API Gateway configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;httpApi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;apigwv2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HttpApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HttpApi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;defaultIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HttpLambdaIntegration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LambdaIntegration&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;nestApiLambda&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;Here you define an HTTP API using &lt;em&gt;API Gateway v2&lt;/em&gt;. This guide uses HTTP API (v2), which is cheaper than REST API (v1), has lower latency, and is sufficient for NestJS since routing lives inside the application. See the &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html" rel="noopener noreferrer"&gt;official comparison&lt;/a&gt; if you need the advanced features REST API offers.&lt;/p&gt;

&lt;p&gt;This uses &lt;code&gt;HttpLambdaIntegration&lt;/code&gt;, which enables &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html" rel="noopener noreferrer"&gt;Lambda proxy integration&lt;/a&gt;. In simple terms, any incoming HTTP requests to API Gateway are forwarded to your Lambda as an event, and the response from Lambda is forwarded back to the client unchanged.&lt;/p&gt;

&lt;p&gt;By setting this as the default integration, CDK creates a &lt;code&gt;$default&lt;/code&gt; route — a catch-all that matches any HTTP method and any path. This is an important consideration for our NestJS application because we want the routing to happen inside the application itself, not at the API Gateway level.&lt;/p&gt;

&lt;h3&gt;
  
  
  CloudFormation outputs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HttpApiUrl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;httpApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prints the generated endpoint URL to the terminal after running &lt;code&gt;cdk deploy&lt;/code&gt;. Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="s"&gt;InfraStack.HttpApiUrl = https://abc123.execute-api.us-east-1.amazonaws.com/&lt;/span&gt;
&lt;span class="na"&gt;Stack ARN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="s"&gt;arn:aws:cloudformation:us-east-1:&amp;lt;account-number&amp;gt;:stack/InfraStack/a0fb4130-2a7f-11f1-ab1d-12c493467933&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploying the infrastructure
&lt;/h2&gt;

&lt;p&gt;At this point, your infrastructure is defined. Now it's time to deploy it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying locally
&lt;/h3&gt;

&lt;p&gt;Before running the &lt;code&gt;cdk deploy&lt;/code&gt; command, let's first validate the CloudFormation stack by running &lt;code&gt;cdk synth&lt;/code&gt; from the &lt;code&gt;infra/&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;infra &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx cdk synth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles your CDK code and generates the CloudFormation template in the &lt;code&gt;infra/cdk.out/&lt;/code&gt; directory. It's a quick way to catch configuration issues before running a full deployment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbhfdc7j6c0jrb9c0r2xj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbhfdc7j6c0jrb9c0r2xj.png" alt="CDK synth output in the terminal" width="359" height="198"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once synthesis succeeds, make sure your &lt;code&gt;tsconfig.build.json&lt;/code&gt; excludes the &lt;code&gt;infra/&lt;/code&gt; directory and explicitly sets &lt;code&gt;rootDir&lt;/code&gt;:&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.build.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;"extends"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./tsconfig.json"&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;"rootDir"&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="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;"exclude"&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;"node_modules"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"infra"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"**/*spec.ts"&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;Two things are happening here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Excluding &lt;code&gt;infra/&lt;/code&gt;&lt;/strong&gt; — without this, &lt;code&gt;nest build&lt;/code&gt; will try to compile your CDK code as part of the application. Since CDK dependencies like &lt;code&gt;aws-cdk-lib&lt;/code&gt; are installed inside &lt;code&gt;infra/node_modules&lt;/code&gt; (not at the project root), the build will fail with module resolution errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Setting &lt;code&gt;rootDir&lt;/code&gt;&lt;/strong&gt; — controls the output folder structure. Without it set explicitly, TypeScript figures it out from whichever files are included. Once &lt;code&gt;infra/&lt;/code&gt; is excluded, TypeScript only sees &lt;code&gt;src/&lt;/code&gt; and uses that as the root — so &lt;code&gt;lambda.ts&lt;/code&gt; compiles to &lt;code&gt;dist/lambda.js&lt;/code&gt; instead of &lt;code&gt;dist/src/lambda.js&lt;/code&gt;, which no longer matches the handler path you defined in the stack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now build your application (&lt;code&gt;dist/&lt;/code&gt;) and Lambda layer (&lt;code&gt;layer/&lt;/code&gt;) from the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
npm run build:lambda-layer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures your compiled application and production dependencies are ready for deployment.&lt;/p&gt;

&lt;p&gt;Now run the deploy command from the &lt;code&gt;infra/&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This starts the CDK deployment process. You'll see a summary of IAM and resource changes before it proceeds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg1g9ucfqgk369565r9ta.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg1g9ucfqgk369565r9ta.png" alt="CDK deploy confirmation prompt showing IAM and resource changes" width="735" height="157"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Confirm the deployment when prompted. After a successful deploy, you'll see the API URL in the terminal output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ibc1nsozq02uaa5il0p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ibc1nsozq02uaa5il0p.png" alt="CDK deploy output showing the HttpApiUrl" width="735" height="145"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hit this URL and you should see the "Hello World!" response from your &lt;code&gt;app.controller.ts&lt;/code&gt; endpoint — your NestJS app is live on AWS Lambda!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw8uv9brkw95psfyx7upb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw8uv9brkw95psfyx7upb.png" alt="Hello World response from the deployed NestJS Lambda endpoint" width="735" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying with GitHub Actions
&lt;/h3&gt;

&lt;p&gt;So far, you've been deploying locally using &lt;code&gt;cdk deploy&lt;/code&gt;. That works well for development, but in a production-grade setup, you'll want deployments to run through CI.&lt;/p&gt;

&lt;p&gt;A common approach is to trigger deployments automatically when changes are pushed to the main branch. In this section, we'll keep things simple and use GitHub Actions to automate deployments on every push to &lt;code&gt;main&lt;/code&gt;. I won't walk through the workflow line by line — the goal here is to get a working setup in place.&lt;/p&gt;

&lt;p&gt;If you're new to GitHub Actions, the &lt;a href="https://docs.github.com/en/actions" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; are a good place to start.&lt;/p&gt;

&lt;p&gt;Add the following file &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;:&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/workflows/deploy.yml&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;Deploy&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&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;deploy&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&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.2.2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Node.js&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.4.0&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="m"&gt;24&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build application&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build Lambda layer&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run build:lambda-layer&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure AWS credentials&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;aws-actions/configure-aws-credentials@v4.1.0&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;aws-access-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-secret-access-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ vars.AWS_REGION }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install CDK dependencies&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="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;infra&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy CDK Stack&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx cdk deploy --require-approval never&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;infra&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At a high level, this workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installs dependencies&lt;/li&gt;
&lt;li&gt;Builds the application (&lt;code&gt;dist/&lt;/code&gt;) and Lambda layer (&lt;code&gt;/layer&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Configures AWS credentials for CI&lt;/li&gt;
&lt;li&gt;Deploys the CDK stack&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AWS credentials for CI&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you look at the workflow code, you'll see it uses GitHub &lt;strong&gt;secrets&lt;/strong&gt; and &lt;strong&gt;variables&lt;/strong&gt; to authenticate with AWS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; — stored as a secret&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; — stored as a secret&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_REGION&lt;/code&gt; — stored as a variable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Make sure you add these in your repository settings. You can follow the official guides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets" rel="noopener noreferrer"&gt;GitHub docs — Using secrets in workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-variables" rel="noopener noreferrer"&gt;GitHub docs — Using variables in workflows&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; GitHub masks secret values in logs, so outputs like your API URL (which includes the region) will also be masked and won't be directly clickable. Using &lt;code&gt;vars&lt;/code&gt; for &lt;code&gt;AWS_REGION&lt;/code&gt; keeps the output readable and ensures the endpoint URL is visible and usable, while sensitive values remain stored in secrets.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once everything is in place, push your changes (including the workflow file) to the &lt;code&gt;main&lt;/code&gt; branch. This will trigger the deployment automatically.&lt;/p&gt;

&lt;p&gt;You can monitor the workflow from the &lt;strong&gt;Actions&lt;/strong&gt; tab in your repository.&lt;/p&gt;

&lt;p&gt;After the workflow completes, open the workflow run and check the &lt;strong&gt;Deploy CDK Stack&lt;/strong&gt; step — the &lt;code&gt;HttpApiUrl&lt;/code&gt; will be printed there without the region being masked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmbyzy8v1t469ew95f2f8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmbyzy8v1t469ew95f2f8.png" alt="GitHub Actions deploy workflow run showing the HttpApiUrl output" width="735" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;That's the full setup. If you've made it this far, you have a production-ready baseline for running NestJS on AWS Lambda!&lt;/p&gt;

&lt;p&gt;You adapted NestJS to fit Lambda's execution model, defined your infrastructure as code using CDK, exposed it through API Gateway, and automated deployments with GitHub Actions. None of these pieces are complex on their own — but together they give you a setup that's consistent, repeatable, and easy to extend.&lt;/p&gt;

&lt;p&gt;A few things this setup gets right out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure is version-controlled alongside your application code&lt;/li&gt;
&lt;li&gt;Deployments are automated and consistent, and can be extended to support multiple environments&lt;/li&gt;
&lt;li&gt;NestJS runs in Lambda without changes to your routing or application logic — the gateway proxies everything through and NestJS handles it internally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From here, you can layer in things like custom domains, environment-specific stacks, API Gateway authentication, or move to container-based Lambda deployments if your workloads grow.&lt;/p&gt;

&lt;p&gt;Thanks for following along — enjoy the serverless!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this saved you time, you can find more posts like this on &lt;a href="https://ajeetchaulagain.com/blog/" rel="noopener noreferrer"&gt;my blog&lt;/a&gt; or &lt;a href="https://www.linkedin.com/in/ajeet-chaulagain/" rel="noopener noreferrer"&gt;follow me on LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>aws</category>
      <category>serverless</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
