--- title: 'Deploy NestJS on AWS' description: 'Deploy NestJS applications on AWS Lambda and API Gateway, or as containerized services with ECS Fargate and Application Load Balancer.' --- import { Tabs, TabItem, Aside } from '@astrojs/starlight/components'; import PatternList from '../../../components/docs/PatternList.astro'; import FrameworkHero from '../../../components/docs/FrameworkHero.astro'; Deploy your [NestJS](https://nestjs.com/) applications to AWS using Thunder. Choose the pattern that fits your app's needs. ## Available Patterns ## Prerequisites ## Getting Started ### Create Project Install the NestJS CLI and scaffold a new project. This sets up the project structure, installs dependencies, and prepares you for development. ```sh npm install -g @nestjs/cli nest new my-nestjs-app cd my-nestjs-app ``` ### Install Thunder Add Thunder as a development dependency. It provides the CDK constructs you'll use to define your AWS infrastructure. ```sh bun add @thunder-so/thunder --development ``` ```sh npm install @thunder-so/thunder --save-dev ``` ```sh pnpm add -D @thunder-so/thunder ``` --- ## NestJS Lambda Deployment Deploy your NestJS API to [AWS Lambda](https://aws.amazon.com/lambda/) with [API Gateway](https://aws.amazon.com/api-gateway/) as the public HTTP endpoint. NestJS adapts to Lambda using `@vendia/serverless-express`, which wraps the Express HTTP adapter in a Lambda-compatible handler. ### Install Adapter for Lambda `@vendia/serverless-express` bridges NestJS's Express adapter to the [Lambda handler format](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html) expected by API Gateway. ```sh bun add @vendia/serverless-express bun add -D @types/aws-lambda ``` ```sh npm install @vendia/serverless-express npm install -D @types/aws-lambda ``` ```sh pnpm add @vendia/serverless-express pnpm add -D @types/aws-lambda ``` ### Configure NestJS for AWS Lambda Create a separate entry point for Lambda. This bootstraps the NestJS app once on cold start and reuses the cached handler across subsequent invocations — avoiding the cost of re-initializing the app on every request. ```ts title="src/lambda.ts" import { NestFactory } from '@nestjs/core'; import { ExpressAdapter } from '@nestjs/platform-express'; import { configure as serverlessExpress } from '@vendia/serverless-express'; import express from 'express'; import { AppModule } from './app.module'; let cachedHandler: any; async function bootstrap() { const expressApp = express(); const app = await NestFactory.create(AppModule, new ExpressAdapter(expressApp)); await app.init(); return serverlessExpress({ app: expressApp }); } export const handler = async (event: any, context: any) => { if (!cachedHandler) { cachedHandler = await bootstrap(); } return cachedHandler(event, context); }; ``` Reference: [NestJS Serverless docs](https://docs.nestjs.com/faq/serverless) ### Stack (Zip mode) The `Lambda` construct provisions a Lambda function and an API Gateway HTTP API. Point `codeDir` at the NestJS build output (`dist/`) and set the handler to the Lambda entry point. ```ts title="stack/prod.ts" import { Cdk, Lambda, type LambdaProps } from '@thunder-so/thunder'; const config: LambdaProps = { env: { account: 'YOUR_ACCOUNT_ID', region: 'us-east-1' }, application: 'myapp', service: 'api', environment: 'prod', rootDir: '.', functionProps: { runtime: Cdk.aws_lambda.Runtime.NODEJS_22_X, architecture: Cdk.aws_lambda.Architecture.ARM_64, codeDir: 'dist', handler: 'lambda.handler', memorySize: 1792, timeout: 10, keepWarm: true, }, }; new Lambda(new Cdk.App(), `${config.application}-${config.service}-${config.environment}-stack`, config); ``` ### Container Mode Zip deployments have a 250 MB unzipped size limit. NestJS apps with many dependencies can exceed this. Switch to container mode — Thunder builds a Docker image, pushes it to [ECR](https://aws.amazon.com/ecr/), and deploys it as a [container Lambda](https://docs.aws.amazon.com/lambda/latest/dg/images-create.html), which supports up to 10 GB. #### Stack (Container mode) Add `dockerFile` to `functionProps` to enable container mode. ```ts title="stack/prod.ts" const config: LambdaProps = { // ... functionProps: { dockerFile: 'Dockerfile', memorySize: 1792, timeout: 10, keepWarm: true, }, }; ``` #### Dockerfile ```dockerfile title="Dockerfile" FROM public.ecr.aws/lambda/nodejs:22 AS builder WORKDIR ${LAMBDA_TASK_ROOT} COPY . . RUN npm ci RUN npm run build FROM public.ecr.aws/lambda/nodejs:22 WORKDIR ${LAMBDA_TASK_ROOT} COPY --from=builder /var/task/dist/ ./ COPY --from=builder /var/task/node_modules ./node_modules CMD ["lambda.handler"] ``` ### Environment Variables and Secrets Runtime environment variables are injected into the Lambda function at deploy time. For sensitive values, store them in [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) and reference them by ARN — Thunder fetches and injects them automatically. ```ts title="stack/prod.ts" const config: LambdaProps = { // ... functionProps: { variables: [ { NODE_ENV: 'production' }, ], secrets: [ { key: 'DATABASE_URL', resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/myapp/DATABASE_URL-abc123', }, ], }, }; ``` ### Deploy Build the NestJS app first — `nest build` compiles TypeScript to `dist/`. Then deploy with CDK. ```sh bun run build npx cdk deploy --app "bunx tsx stack/prod.ts" --profile default ``` ```sh npm run build npx cdk deploy --app "npx tsx stack/prod.ts" --profile default ``` ```sh pnpm run build pnpm exec cdk deploy --app "pnpm exec tsx stack/prod.ts" --profile default ``` After deployment, CDK outputs the **API Gateway URL** for your function. --- ## NestJS Containerized Deployment with Fargate Run your NestJS API as a Node.js server inside a Docker container on [ECS Fargate](https://aws.amazon.com/fargate/). Traffic is routed through an [Application Load Balancer](https://aws.amazon.com/elasticloadbalancing/application-load-balancer/). No Lambda adapter needed — NestJS runs as a standard HTTP server. This pattern is ideal for long-running services, WebSocket support, and workloads that exceed Lambda's limits. ### Configure for Node Server NestJS listens on port 3000 by default. Make it configurable via environment variable so the container runtime can override it if needed. ```ts title="src/main.ts" import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); await app.listen(process.env.PORT ?? 3000); } bootstrap(); ``` ### Stack The `Fargate` construct creates an ECS cluster, a Fargate task definition, and an Application Load Balancer. ```ts title="stack/prod.ts" import { Cdk, Fargate, type FargateProps } from '@thunder-so/thunder'; const config: FargateProps = { env: { account: 'YOUR_ACCOUNT_ID', region: 'us-east-1' }, application: 'myapp', service: 'api', environment: 'prod', rootDir: '.', serviceProps: { dockerFile: 'Dockerfile', architecture: Cdk.aws_ecs.CpuArchitecture.ARM64, cpu: 512, memorySize: 1024, port: 3000, desiredCount: 1, healthCheckPath: '/', }, }; new Fargate(new Cdk.App(), `${config.application}-${config.service}-${config.environment}-stack`, config); ``` ### Dockerfile Create a `Dockerfile` in your project root. The multi-stage build compiles TypeScript in the builder stage, then copies only the compiled output and production dependencies into the final image. ```dockerfile title="Dockerfile" FROM public.ecr.aws/docker/library/node:22-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM public.ecr.aws/docker/library/node:22-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV PORT=3000 COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./ EXPOSE 3000 CMD ["node", "dist/main"] ``` ### Environment Variables and Secrets Runtime environment variables are injected into the Fargate task at deploy time. For sensitive values, store them in [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) and reference them by ARN — Thunder fetches and injects them automatically. ```ts title="stack/prod.ts" const config: FargateProps = { // ... serviceProps: { // ... variables: [ { NODE_ENV: 'production' }, ], secrets: [ { key: 'DATABASE_URL', resource: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:/myapp/DATABASE_URL-abc123', }, ], }, }; ``` ### Deploy CDK builds the Docker image, pushes it to [ECR](https://aws.amazon.com/ecr/), and deploys it to Fargate. No manual Docker commands needed. ```sh npx cdk deploy --app "bunx tsx stack/prod.ts" --profile default ``` ```sh npx cdk deploy --app "npx tsx stack/prod.ts" --profile default ``` ```sh pnpm exec cdk deploy --app "pnpm exec tsx stack/prod.ts" --profile default ``` After deployment, CDK outputs the **Load Balancer DNS** for your application.