Deploy NestJS on AWS
A progressive Node.js framework for building efficient server-side applications.
nestjs.comDeploy your NestJS 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.
npm install -g @nestjs/clinest new my-nestjs-appcd my-nestjs-appInstall Thunder
Add Thunder as a development dependency. It provides the CDK constructs you’ll use to define your AWS infrastructure.
bun add @thunder-so/thunder --developmentnpm install @thunder-so/thunder --save-devpnpm add -D @thunder-so/thunderNestJS Lambda Deployment
Deploy your NestJS API to AWS Lambda with 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 expected by API Gateway.
bun add @vendia/serverless-expressbun add -D @types/aws-lambdanpm install @vendia/serverless-expressnpm install -D @types/aws-lambdapnpm add @vendia/serverless-expresspnpm add -D @types/aws-lambdaConfigure 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.
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
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.
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, and deploys it as a container Lambda, which supports up to 10 GB.
Stack (Container mode)
Add dockerFile to functionProps to enable container mode.
const config: LambdaProps = { // ... functionProps: { dockerFile: 'Dockerfile', memorySize: 1792, timeout: 10, keepWarm: true, },};Dockerfile
FROM public.ecr.aws/lambda/nodejs:22 AS builderWORKDIR ${LAMBDA_TASK_ROOT}COPY . .RUN npm ciRUN npm run build
FROM public.ecr.aws/lambda/nodejs:22WORKDIR ${LAMBDA_TASK_ROOT}COPY --from=builder /var/task/dist/ ./COPY --from=builder /var/task/node_modules ./node_modulesCMD ["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 and reference them by ARN — Thunder fetches and injects them automatically.
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.
bun run buildnpx cdk deploy --app "bunx tsx stack/prod.ts" --profile defaultnpm run buildnpx cdk deploy --app "npx tsx stack/prod.ts" --profile defaultpnpm run buildpnpm exec cdk deploy --app "pnpm exec tsx stack/prod.ts" --profile defaultAfter 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. Traffic is routed through an 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.
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.
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.
FROM public.ecr.aws/docker/library/node:22-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
FROM public.ecr.aws/docker/library/node:22-alpine AS runnerWORKDIR /appENV NODE_ENV=productionENV PORT=3000COPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesCOPY --from=builder /app/package.json ./EXPOSE 3000CMD ["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 and reference them by ARN — Thunder fetches and injects them automatically.
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, and deploys it to Fargate. No manual Docker commands needed.
npx cdk deploy --app "bunx tsx stack/prod.ts" --profile defaultnpx cdk deploy --app "npx tsx stack/prod.ts" --profile defaultpnpm exec cdk deploy --app "pnpm exec tsx stack/prod.ts" --profile defaultAfter deployment, CDK outputs the Load Balancer DNS for your application.