Deploy NestJS on AWS

A progressive Node.js framework for building efficient server-side applications.

nestjs.com

Deploy 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.

Terminal window
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.

Terminal window
bun add @thunder-so/thunder --development

NestJS 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.

Terminal window
bun add @vendia/serverless-express
bun 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.

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

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.

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, and deploys it as a container Lambda, which supports up to 10 GB.

Stack (Container mode)

Add dockerFile to functionProps to enable container mode.

stack/prod.ts
const config: LambdaProps = {
// ...
functionProps: {
dockerFile: 'Dockerfile',
memorySize: 1792,
timeout: 10,
keepWarm: true,
},
};

Dockerfile

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 and reference them by ARN — Thunder fetches and injects them automatically.

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.

Terminal window
bun run build
npx cdk deploy --app "bunx 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. 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.

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.

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
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 and reference them by ARN — Thunder fetches and injects them automatically.

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, and deploys it to Fargate. No manual Docker commands needed.

Terminal window
npx cdk deploy --app "bunx tsx stack/prod.ts" --profile default

After deployment, CDK outputs the Load Balancer DNS for your application.