
Aws Lambda Typescript Integration
- 1.7k installs
- 318 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
aws-lambda-typescript-integration is an agent skill for TypeScript AWS Lambda with NestJS or raw handlers, cold-start optimization, and API Gateway integration.
About
The aws-lambda-typescript-integration skill documents two TypeScript Lambda approaches: NestJS with dependency injection and larger bundles versus raw TypeScript with minimal overhead under fifty kilobytes and sub-100ms cold starts. It compares cold start, bundle size, and complexity tradeoffs and outlines NestJS and raw project structures with lambda.ts serverless-express adapters or direct handlers. Implementation covers API Gateway and ALB integration, CI/CD setup, and optimization patterns referenced in bundled guides. NestJS path targets complex APIs needing DI while raw TypeScript suits microservices and simple handlers. Allowed tools include Read, Write, Edit, Glob, Grep, and Bash for scaffolding and deployment files. Use when developers create or deploy TypeScript Lambda functions and choose between NestJS framework and minimal TypeScript handlers.
- Compares NestJS Lambda versus raw TypeScript for cold start and bundle size tradeoffs.
- Documents NestJS serverless-express adapter and raw handler project structures.
- Supports API Gateway and ALB integration patterns for both approaches.
- NestJS cold start under 500ms; raw TypeScript under 100ms per skill benchmarks.
- Includes CI/CD setup guidance for TypeScript Lambda deployments.
Aws Lambda Typescript Integration by the numbers
- 1,659 all-time installs (skills.sh)
- +142 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #298 of 4,346 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
aws-lambda-typescript-integration capabilities & compatibility
- Capabilities
- nestjs serverless express lambda adapter · raw typescript minimal handler pattern · cold start and bundle size comparison · api gateway and alb integration · ci/cd setup for typescript lambda
- Works with
- aws
- Use cases
- api development · devops
- Runs
- Remote server
- Pricing
- Bring your own API key
What aws-lambda-typescript-integration says it does
Patterns for creating high-performance AWS Lambda functions in TypeScript with optimized cold starts.
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill aws-lambda-typescript-integrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.7k |
|---|---|
| repo stars | ★ 318 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
How do I create a TypeScript Lambda with minimal cold start and choose between NestJS and raw handlers?
Create TypeScript AWS Lambda functions with NestJS or raw handlers, cold-start optimization, and API Gateway or ALB integration.
Who is it for?
Developers building serverless TypeScript APIs on AWS who need cold-start-aware architecture choices.
Skip if: Skip for non-AWS serverless platforms or Python and Go Lambda functions.
When should I use this skill?
User asks to create TypeScript Lambda, deploy NestJS on AWS Lambda, or optimize Lambda cold starts.
What you get
Project structure, handler code, and deployment patterns for NestJS or raw TypeScript Lambda on API Gateway or ALB.
- Lambda handler source
- API integration configuration
- Cold-start optimization checklist
Files
AWS Lambda TypeScript Integration
Patterns for creating high-performance AWS Lambda functions in TypeScript with optimized cold starts.
Overview
Two approaches for TypeScript Lambda:
1. NestJS Framework - Dependency injection, modular architecture, larger bundle (100KB+) 2. Raw TypeScript - Minimal overhead, smaller bundle (<50KB), maximum control
Both support API Gateway and ALB integration.
When to Use
- Creating new Lambda functions in TypeScript
- Optimizing cold start performance
- Choosing between NestJS and minimal TypeScript
- Configuring API Gateway or ALB integration
- Setting up CI/CD for TypeScript Lambda
Instructions
1. Choose Your Approach
| Approach | Cold Start | Bundle Size | Best For | Complexity |
|---|---|---|---|---|
| NestJS | < 500ms | Larger (100KB+) | Complex APIs, enterprise apps, DI needed | Medium |
| Raw TypeScript | < 100ms | Smaller (< 50KB) | Simple handlers, microservices, minimal deps | Low |
2. Project Structure
NestJS Structure
my-nestjs-lambda/
├── src/
│ ├── app.module.ts
│ ├── main.ts
│ ├── lambda.ts # Lambda entry point
│ └── modules/
│ └── api/
├── package.json
├── tsconfig.json
└── serverless.ymlRaw TypeScript Structure
my-ts-lambda/
├── src/
│ ├── handlers/
│ │ └── api.handler.ts
│ ├── services/
│ └── utils/
├── dist/ # Compiled output
├── package.json
├── tsconfig.json
└── template.yaml3. Implementation Examples
See the References section for detailed implementation guides. Quick examples:
NestJS Handler:
// lambda.ts
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import serverlessExpress from '@codegenie/serverless-express';
import { Context, Handler } from 'aws-lambda';
import express from 'express';
import { AppModule } from './src/app.module';
let cachedServer: Handler;
async function bootstrap(): Promise<Handler> {
const expressApp = express();
const adapter = new ExpressAdapter(expressApp);
const nestApp = await NestFactory.create(AppModule, adapter);
await nestApp.init();
return serverlessExpress({ app: expressApp });
}
export const handler: Handler = async (event: any, context: Context) => {
if (!cachedServer) {
cachedServer = await bootstrap();
}
return cachedServer(event, context);
};Raw TypeScript Handler:
// src/handlers/api.handler.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
export const handler = async (
event: APIGatewayProxyEvent,
context: Context
): Promise<APIGatewayProxyResult> => {
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Hello from TypeScript Lambda!' })
};
};Core Concepts
Cold Start Optimization
TypeScript cold start depends on bundle size and initialization code. Key strategies:
1. Lazy Loading - Defer heavy imports until needed 2. Tree Shaking - Remove unused code from bundle 3. Minification - Use esbuild or terser for smaller bundles 4. Instance Caching - Cache initialized services between invocations
See Raw TypeScript Lambda for detailed patterns.
Connection Management
Create clients at module level and reuse:
// GOOD: Initialize once, reuse across invocations
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
const dynamoClient = new DynamoDBClient({ region: process.env.AWS_REGION });
export const handler = async (event: APIGatewayProxyEvent) => {
// Use dynamoClient - already initialized
};Environment Configuration
// src/config/env.config.ts
export const env = {
region: process.env.AWS_REGION || 'us-east-1',
tableName: process.env.TABLE_NAME || '',
debug: process.env.DEBUG === 'true',
};
// Validate required variables
if (!env.tableName) {
throw new Error('TABLE_NAME environment variable is required');
}Best Practices
Memory and Timeout Configuration
- Memory: Start with 512MB for NestJS, 256MB for raw TypeScript
- Timeout: Set based on cold start + expected processing time
- NestJS: 10-30 seconds for cold start buffer
- Raw TypeScript: 3-10 seconds typically sufficient
Dependencies
Keep package.json minimal:
{
"dependencies": {
"aws-lambda": "^3.1.0",
"@aws-sdk/client-dynamodb": "^3.450.0"
},
"devDependencies": {
"typescript": "^5.3.0",
"esbuild": "^0.19.0"
}
}Error Handling
Return proper HTTP codes with structured errors:
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
try {
const result = await processEvent(event);
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result)
};
} catch (error) {
console.error('Error processing request:', error);
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ error: 'Internal server error' })
};
}
};Logging
Use structured logging for CloudWatch Insights:
const log = (level: string, message: string, meta?: object) => {
console.log(JSON.stringify({
level,
message,
timestamp: new Date().toISOString(),
...meta
}));
};
log('info', 'Request processed', { requestId: context.awsRequestId });Deployment Options
Quick Start
Serverless Framework:
service: my-typescript-api
provider:
name: aws
runtime: nodejs20.x
functions:
api:
handler: dist/handler.handler
events:
- http:
path: /{proxy+}
method: ANYAWS SAM:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: dist/
Handler: handler.handler
Runtime: nodejs20.x
Events:
ApiEvent:
Type: Api
Properties:
Path: /{proxy+}
Method: ANYDeployment Validation
Pre-deploy checks: 1. Run npm test - verify all tests pass 2. Run npm run build - confirm TypeScript compiles without errors 3. Verify bundle size < 50MB (unzipped) 4. Run serverless invoke local or sam local invoke - test locally
Post-deploy verification: 1. Run serverless invoke or aws lambda invoke - verify handler executes 2. Test API endpoint via curl or Postman 3. Check CloudWatch logs for errors 4. Verify cold start time meets SLA
For complete deployment configurations including CI/CD, see Serverless Deployment.
Constraints and Warnings
Lambda Limits
- Deployment package: 250MB unzipped maximum (50MB zipped)
- Memory: 128MB to 10GB
- Timeout: 15 minutes maximum
- Concurrent executions: 1000 default (adjustable)
- Environment variables: 4KB total size
TypeScript-Specific Considerations
- Bundle size: TypeScript compiles to JavaScript; use bundlers to minimize size
- Cold start: Node.js 20.x offers best performance
- Dependencies: Use Lambda Layers for shared dependencies
- Native modules: Must be compiled for Amazon Linux 2
Common Pitfalls
1. Importing heavy libraries at module level - Defer to lazy loading if not always needed 2. Not bundling dependencies - Include all production dependencies in the package 3. Missing type definitions - Install @types/aws-lambda for proper event typing 4. No timeout handling - Use context.getRemainingTimeInMillis() for long operations
Security Considerations
- Never hardcode credentials; use IAM roles and environment variables
- Input Validation for Event Data: All incoming event data (API Gateway request bodies, S3 event objects, SQS message bodies) is untrusted external content; always validate and sanitize before processing to prevent injection attacks
- Content Sanitization: When processing S3 objects or SQS message payloads, treat the content as untrusted third-party data; apply appropriate validation, schema checks, and sanitization before acting on it
- Validate all input data
- Use least privilege IAM policies
- Enable CloudTrail for audit logging
- Sanitize logs to avoid leaking sensitive data
References
For detailed guidance on specific topics:
- [NestJS Lambda](references/nestjs-lambda.md) - Complete NestJS setup, dependency injection, Express/Fastify adapters
- [Raw TypeScript Lambda](references/raw-typescript-lambda.md) - Minimal handler patterns, bundling, tree shaking
- [Serverless Config](references/serverless-config.md) - Serverless Framework and SAM configuration
- [Serverless Deployment](references/serverless-deployment.md) - CI/CD pipelines, environment management
- [Testing](references/testing.md) - Jest, integration testing, SAM Local
Examples
Example 1: Create a NestJS REST API
Input: Create a TypeScript Lambda REST API using NestJS for a todo application
Process: 1. Initialize NestJS project with nest new 2. Install Lambda dependencies: @codegenie/serverless-express, aws-lambda 3. Create lambda.ts entry point with Express adapter 4. Configure serverless.yml with API Gateway events 5. Deploy with Serverless Framework
Validation:
- Run
serverless invoke local -f api- verify handler works - Check bundle size < 250MB
- Test deployed endpoint returns 200 OK
Output: NestJS project with REST API, DynamoDB integration, deployment config
Example 2: Create a Raw TypeScript Lambda
Input: Create a minimal TypeScript Lambda function with optimal cold start
Process: 1. Set up TypeScript project with esbuild 2. Create handler with proper AWS types 3. Configure minimal dependencies 4. Set up SAM or Serverless deployment 5. Optimize bundle size with tree shaking
Validation:
- Run
sam local invoke- test locally before deploying - Verify bundle < 50KB with
du -sh dist/ - Confirm cold start < 100ms via CloudWatch
Output: Minimal TypeScript Lambda, bundle < 50KB, cold start < 100ms
Example 3: Deploy with GitHub Actions
Input: Configure CI/CD for TypeScript Lambda with SAM
Process: 1. Create GitHub Actions workflow 2. Set up Node.js environment 3. Run tests with Jest 4. Bundle with esbuild 5. Deploy with SAM
Validation:
- Verify CI pipeline runs
npm testsuccessfully - Confirm
sam validatepasses in pipeline - Check CloudFormation stack created successfully
Output: GitHub Actions workflow, multi-stage pipeline, test automation
Version
Version: 1.0.0
Express Adapter for Lambda
Overview
Detailed configuration options and advanced patterns for using the Express adapter with AWS Lambda.
Installation
npm install @codegenie/serverless-express express
npm install -D @types/expressConfiguration Options
serverless-express Options
serverlessExpress({
app: expressApp,
// Binary MIME types for file uploads/downloads
binaryMimeTypes: [
'application/pdf',
'image/png',
'image/jpeg',
'image/gif',
'application/zip',
],
// Request transformation
request: (request, event, context) => {
// Add Lambda context to request
request.lambdaEvent = event;
request.lambdaContext = context;
},
// Response transformation
response: (response, event, context) => {
// Add custom headers
response.set('X-Request-Id', context.awsRequestId);
},
});API Gateway v1 vs v2
API Gateway v1 (REST API)
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
// Default format, fully featured
const server = serverlessExpress({ app: expressApp });
export const handler = async (
event: APIGatewayProxyEvent,
context: Context,
): Promise<APIGatewayProxyResult> => {
return server(event, context);
};API Gateway v2 (HTTP API)
import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
// Simpler, cheaper, but different event format
const server = serverlessExpress({
app: expressApp,
eventSource: {
getRequest: (event: APIGatewayProxyEventV2) => ({
method: event.requestContext.http.method,
url: event.rawPath + (event.rawQueryString ? `?${event.rawQueryString}` : ''),
headers: event.headers,
body: event.body,
}),
getResponse: (response) => ({
statusCode: response.statusCode,
headers: response.headers,
body: response.body,
// v2 format
cookies: response.cookies,
}),
},
});Middleware Configuration
Compression
import compression from 'compression';
// Enable compression for responses
expressApp.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
},
level: 6, // Balance between speed and compression
}));Body Parsing
import bodyParser from 'body-parser';
// JSON body parser with size limits
expressApp.use(bodyParser.json({
limit: '10mb',
strict: true,
}));
// URL encoded parser
expressApp.use(bodyParser.urlencoded({
extended: true,
limit: '10mb',
}));
// Raw body for webhooks
expressApp.use('/webhooks', bodyParser.raw({
type: 'application/json',
verify: (req, res, buf) => {
// Store raw body for signature verification
(req as any).rawBody = buf;
},
}));Session Handling
DynamoDB Session Store
import session from 'express-session';
import DynamoDBStore from 'connect-dynamodb';
const DynamoDBStoreSession = DynamoDBStore(session);
expressApp.use(session({
store: new DynamoDBStoreSession({
table: 'sessions',
hashKey: 'sessionId',
readCapacityUnits: 5,
writeCapacityUnits: 5,
}),
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24, // 24 hours
sameSite: 'strict',
},
}));Security Headers
Helmet Configuration
import helmet from 'helmet';
expressApp.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true,
},
// Disable features not needed for API
xssFilter: true,
noSniff: true,
referrerPolicy: { policy: 'same-origin' },
}));Rate Limiting
Express Rate Limit
import rateLimit from 'express-rate-limit';
// Simple in-memory rate limit (use Redis for multi-instance)
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: {
statusCode: 429,
error: 'Too Many Requests',
message: 'Rate limit exceeded. Please try again later.',
},
standardHeaders: true,
legacyHeaders: false,
});
expressApp.use('/api/', limiter);Error Handling
Global Express Error Handler
// Must be last middleware
expressApp.use((err: any, req: Request, res: Response, next: NextFunction) => {
console.error({
error: err.message,
stack: err.stack,
path: req.path,
method: req.method,
});
// Don't leak error details in production
const isDev = process.env.NODE_ENV !== 'production';
res.status(err.status || 500).json({
statusCode: err.status || 500,
error: err.name || 'InternalServerError',
message: isDev ? err.message : 'Internal server error',
...(isDev && { stack: err.stack }),
});
});Performance Optimization
Connection Keep-Alive
// Enable keep-alive for connection reuse
expressApp.use((req, res, next) => {
res.setHeader('Connection', 'keep-alive');
next();
});Response Caching
import apicache from 'apicache';
const cache = apicache.middleware;
// Cache GET requests for 5 minutes
expressApp.use('/api/public/', cache('5 minutes', (req) => req.method === 'GET'));Logging
Morgan with CloudWatch
import morgan from 'morgan';
// Custom format for CloudWatch
expressApp.use(morgan((tokens, req, res) => {
return JSON.stringify({
method: tokens.method(req, res),
url: tokens.url(req, res),
status: tokens.status(req, res),
responseTime: tokens['response-time'](req, res),
contentLength: tokens.res(req, res, 'content-length'),
userAgent: tokens['user-agent'](req, res),
timestamp: new Date().toISOString(),
});
}));Fastify Adapter for Lambda
Overview
Fastify offers superior performance compared to Express for Lambda workloads, with lower cold start times and better throughput.
Installation
npm install @nestjs/platform-fastify fastify aws-lambda-fastify
npm install -D @types/aws-lambdaBasic Configuration
Standard Setup
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import awsLambdaFastify from 'aws-lambda-fastify';
import { AppModule } from './app.module';
let cachedProxy: any;
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({
logger: false,
trustProxy: true,
genReqId: () => `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
}),
);
await app.init();
return awsLambdaFastify(app.getHttpAdapter().getInstance(), {
binaryMimeTypes: ['application/pdf', 'image/*'],
serializeLambdaArguments: false,
});
}
export const handler = async (event, context) => {
if (!cachedProxy) {
cachedProxy = await bootstrap();
}
return cachedProxy(event, context);
};Fastify Options
Performance Tuning
const adapter = new FastifyAdapter({
// Disable logging for cold start performance
logger: false,
// Trust proxy headers from API Gateway
trustProxy: true,
// Connection timeout (Lambda max is 30s)
connectionTimeout: 29000,
// Keep alive timeout
keepAliveTimeout: 5000,
// Max payload size (API Gateway limit is 10MB)
bodyLimit: 10485760,
// Case-sensitive routing
caseSensitive: true,
// Ignore trailing slashes
ignoreTrailingSlash: true,
// Max param length for URL parameters
maxParamLength: 100,
});Plugins
Compression
import compression from '@fastify/compress';
// Register compression plugin
app.register(compression, {
global: true,
encodings: ['gzip', 'deflate'],
threshold: 1024, // Only compress responses > 1KB
});CORS
import cors from '@fastify/cors';
app.register(cors, {
origin: (origin, cb) => {
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
if (!origin || allowedOrigins.includes(origin)) {
cb(null, true);
return;
}
cb(new Error('Not allowed'), false);
},
credentials: true,
methods: ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-Id'],
});Helmet (Security Headers)
import helmet from '@fastify/helmet';
app.register(helmet, {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
},
});Rate Limit
import rateLimit from '@fastify/rate-limit';
app.register(rateLimit, {
max: 100,
timeWindow: '15 minutes',
keyGenerator: (req) => req.headers['x-forwarded-for'] || req.ip,
errorResponseBuilder: (req, context) => ({
statusCode: 429,
error: 'Too Many Requests',
message: `Rate limit exceeded. Try again in ${context.after}`,
retryAfter: context.after,
}),
});Request/Response Hooks
Lifecycle Hooks
// On request hook
app.getHttpAdapter().getInstance().addHook('onRequest', async (request, reply) => {
// Add request ID
reply.header('x-request-id', request.id);
// Log request
console.log({
event: 'request_start',
method: request.method,
url: request.url,
requestId: request.id,
timestamp: new Date().toISOString(),
});
});
// On send hook
app.getHttpAdapter().getInstance().addHook('onSend', async (request, reply, payload) => {
// Log response
console.log({
event: 'request_end',
method: request.method,
url: request.url,
statusCode: reply.statusCode,
duration: Date.now() - (request as any).startTime,
});
});Validation
JSON Schema Validation
// Fastify uses JSON Schema for validation
const createUserSchema = {
schema: {
body: {
type: 'object',
required: ['name', 'email'],
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, maximum: 150 },
},
},
response: {
201: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
},
},
},
},
};
// Use in controller
@Post()
@UsePipes(new ValidationPipe())
async create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}Serialization
Custom Serializer
// Fastify-serialize options
app.register(require('@fastify/response-validation'), {
onError: (error) => {
console.error('Response validation error:', error);
},
});
// Use class-transformer for serialization
@Controller('users')
@UseInterceptors(ClassSerializerInterceptor)
export class UsersController {
// Responses automatically serialized
}Error Handling
Global Error Handler
// Set error handler on Fastify instance
app.getHttpAdapter().getInstance().setErrorHandler((error, request, reply) => {
console.error({
error: error.message,
stack: error.stack,
code: error.code,
validation: error.validation,
});
// Handle validation errors
if (error.validation) {
reply.status(400).send({
statusCode: 400,
error: 'Bad Request',
message: error.message,
validation: error.validation,
});
return;
}
// Handle other errors
const statusCode = error.statusCode || 500;
reply.status(statusCode).send({
statusCode,
error: error.name || 'Internal Server Error',
message: process.env.NODE_ENV === 'production'
? 'Internal server error'
: error.message,
});
});File Upload
Multipart Support
import multipart from '@fastify/multipart';
app.register(multipart, {
limits: {
fieldNameSize: 100,
fieldSize: 1000000, // 1MB
fields: 10,
fileSize: 10000000, // 10MB
files: 5,
},
});
// Controller handling
@Post('upload')
async upload(@Req() req: FastifyRequest) {
const data = await req.file();
// Process file
const buffer = await data.toBuffer();
return {
filename: data.filename,
mimetype: data.mimetype,
size: buffer.length,
};
}Performance Comparison
Benchmarks
| Metric | Express | Fastify | Improvement |
|---|---|---|---|
| Cold Start | ~250ms | ~180ms | 28% faster |
| Throughput | 15k req/s | 25k req/s | 67% higher |
| Memory Usage | 85MB | 65MB | 24% less |
| JSON Parsing | 12k ops/s | 25k ops/s | 108% faster |
When to Choose Fastify
- High throughput APIs - Many concurrent requests
- JSON-heavy APIs - Superior JSON parsing performance
- Memory-constrained environments - Lower memory footprint
- Cold start sensitive - Faster initialization
When to Choose Express
- Existing Express middleware - Large ecosystem compatibility
- Migration projects - Easier migration path
- Complex routing - More mature routing patterns
- Team familiarity - If team knows Express well
NestJS Lambda Reference
Complete guide for deploying NestJS applications on AWS Lambda with optimal performance.
Table of Contents
1. Project Setup 2. Lambda Handler 3. Platform Adapters 4. Cold Start Optimization 5. Lifecycle Management 6. Deployment
---
Project Setup
Installation
# Create new NestJS project
nest new my-lambda-api
cd my-lambda-api
# Install Lambda dependencies
npm install @codegenie/serverless-express aws-lambda
npm install -D @types/aws-lambda serverless-offline
# Optional: Fastify adapter for better performance
npm install @nestjs/platform-fastify aws-lambda-fastifyProject Structure
my-nestjs-lambda/
├── src/
│ ├── app.module.ts
│ ├── main.ts # Standard NestJS entry
│ ├── lambda.ts # Lambda entry point
│ ├── config/
│ │ └── lambda.config.ts
│ └── modules/
│ └── api/
│ ├── api.controller.ts
│ └── api.service.ts
├── test/
├── package.json
├── tsconfig.json
├── serverless.yml
└── webpack.config.js # For bundling---
Lambda Handler
Basic Express Adapter
// lambda.ts
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import serverlessExpress from '@codegenie/serverless-express';
import { Context, Handler, APIGatewayProxyEvent } from 'aws-lambda';
import express from 'express';
import { AppModule } from './src/app.module';
let cachedServer: Handler;
async function bootstrap(): Promise<Handler> {
const expressApp = express();
const adapter = new ExpressAdapter(expressApp);
const nestApp = await NestFactory.create(AppModule, adapter);
// Enable CORS
nestApp.enableCors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
credentials: true,
});
// Set global prefix
nestApp.setGlobalPrefix('api');
await nestApp.init();
return serverlessExpress({ app: expressApp });
}
export const handler: Handler = async (
event: APIGatewayProxyEvent,
context: Context,
) => {
if (!cachedServer) {
cachedServer = await bootstrap();
}
return cachedServer(event, context);
};---
Platform Adapters
Express Adapter (Recommended)
Best compatibility with existing middleware and ecosystem.
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import serverlessExpress from '@codegenie/serverless-express';
import { AppModule } from './src/app.module';
let server: Handler;
async function bootstrap(): Promise<Handler> {
const expressApp = express();
const app = await NestFactory.create(
AppModule,
new ExpressAdapter(expressApp),
);
// Apply global settings
app.setGlobalPrefix('api');
app.enableCors();
await app.init();
return serverlessExpress({ app: expressApp });
}Fastify Adapter (Performance)
Better performance but smaller ecosystem.
import { NestFactory } from '@nestjs/core';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import awsLambdaFastify from 'aws-lambda-fastify';
let cachedProxy: Handler;
async function bootstrap(): Promise<Handler> {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({ logger: false, trustProxy: true }),
);
app.setGlobalPrefix('api');
await app.init();
return awsLambdaFastify(app.getHttpAdapter().getInstance(), {
binaryMimeTypes: ['application/pdf', 'image/*'],
});
}
export const handler: Handler = async (event, context) => {
if (!cachedProxy) {
cachedProxy = await bootstrap();
}
return cachedProxy(event, context);
};---
Cold Start Optimization
Lazy Loading
Defer heavy module initialization:
// config/swagger.config.ts
export async function setupSwagger(app: INestApplication) {
if (process.env.ENABLE_SWAGGER !== 'true') return;
// Lazy load Swagger only when needed
const { SwaggerModule, DocumentBuilder } = await import('@nestjs/swagger');
const config = new DocumentBuilder()
.setTitle('API')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);
}Environment-Based Feature Loading
// lambda.ts
async function bootstrap(): Promise<Handler> {
const app = await NestFactory.create(AppModule);
// Conditional feature loading
if (process.env.NODE_ENV !== 'production') {
await setupSwagger(app);
}
// Only enable logging in non-production
if (process.env.ENABLE_LOGGING === 'true') {
app.useLogger(new CloudWatchLogger());
}
await app.init();
return serverlessExpress({ app: expressApp });
}Connection Pooling
// database.module.ts
import { Module } from '@nestjs/common';
@Module({
providers: [
{
provide: 'DATABASE_CONFIG',
useValue: {
// Lambda-optimized pool settings
max: 1, // Maximum 1 connection for Lambda
min: 0, // Allow zero connections when idle
acquireTimeoutMillis: 5000,
idleTimeoutMillis: 10000,
},
},
],
})
export class DatabaseModule {}---
Lifecycle Management
Module Lifecycle Hooks
// lambda-lifecycle.service.ts
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
@Injectable()
export class LambdaLifecycleService
implements OnModuleInit, OnModuleDestroy
{
private readonly logger = new Logger(LambdaLifecycleService.name);
onModuleInit() {
this.logger.log('[Lambda] Module initializing...');
// Setup resources
}
onModuleDestroy() {
this.logger.log('[Lambda] Module destroying - cleanup resources');
// Cleanup before Lambda container freeze
}
}Graceful Shutdown
// main.ts (for local dev)
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable shutdown hooks
app.enableShutdownHooks();
await app.listen(3000);
}
// lambda.ts (for Lambda)
async function bootstrap(): Promise<Handler> {
const app = await NestFactory.create(AppModule);
// No shutdown hooks needed - Lambda handles container lifecycle
await app.init();
return serverlessExpress({ app: expressApp });
}---
Deployment
Serverless Framework
# serverless.yml
service: nestjs-lambda-api
provider:
name: aws
runtime: nodejs20.x
memorySize: 512
timeout: 29
environment:
NODE_ENV: production
AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1'
functions:
api:
handler: dist/lambda.handler
events:
- http:
path: /{proxy+}
method: ANY
cors: true
plugins:
- serverless-offline
custom:
serverless-offline:
httpPort: 3000AWS SAM
# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Timeout: 29
MemorySize: 512
Runtime: nodejs20.x
Environment:
Variables:
NODE_ENV: production
Resources:
NestJSApiFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: dist/
Handler: lambda.handler
Events:
ApiEvent:
Type: Api
Properties:
Path: /{proxy+}
Method: ANY
Outputs:
ApiUrl:
Description: API Gateway endpoint URL
Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/'Build Configuration
// webpack.config.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
entry: './src/lambda.ts',
target: 'node',
mode: 'production',
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
},
output: {
filename: 'lambda.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: 'commonjs2',
},
};---
Best Practices
1. Always cache the NestJS instance - Critical for warm starts 2. Use lazy loading - Defer non-critical initialization 3. Optimize connection pooling - Max 1-2 connections for Lambda 4. Bundle with webpack/esbuild - Minimize deployment package 5. Monitor cold starts - Log initialization times 6. Use provisioned concurrency - For latency-sensitive APIs 7. Implement health checks - For ALB target group health 8. Validate environment - Fail fast on missing config
Raw TypeScript Lambda Reference
Complete guide for creating minimal AWS Lambda functions in pure TypeScript without frameworks like NestJS.
Table of Contents
1. Project Structure 2. Minimal Handler 3. Dependency Injection Patterns 4. Cold Start Optimization 5. TypeScript Configuration 6. Build and Packaging 7. Testing 8. Deployment
---
Project Structure
Minimal Setup
raw-ts-lambda/
├── src/
│ ├── handlers/
│ │ ├── api.handler.ts
│ │ └── s3.handler.ts
│ ├── services/
│ │ └── user.service.ts
│ ├── models/
│ │ └── user.model.ts
│ ├── utils/
│ │ └── response.util.ts
│ └── config/
│ └── database.config.ts
├── dist/ # Compiled output
├── tests/
│ └── handlers/
│ └── api.handler.test.ts
├── package.json
├── tsconfig.json
└── template.yaml (or serverless.yml)Package.json
{
"name": "raw-ts-lambda",
"version": "1.0.0",
"description": "Minimal TypeScript Lambda without frameworks",
"main": "dist/handlers/api.handler.js",
"scripts": {
"build": "tsc",
"build:prod": "tsc && npm prune --production",
"test": "jest",
"lint": "eslint src/**/*.ts",
"deploy": "sam build && sam deploy",
"local": "sam local start-api"
},
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.450.0",
"@aws-sdk/lib-dynamodb": "^3.450.0",
"@aws-sdk/client-s3": "^3.450.0"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.131",
"@types/jest": "^29.5.10",
"@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.13.0",
"@typescript-eslint/parser": "^6.13.0",
"aws-lambda": "^1.0.7",
"esbuild": "^0.19.8",
"eslint": "^8.54.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"typescript": "^5.3.2"
},
"engines": {
"node": ">=20.0.0"
}
}---
Minimal Handler
Basic API Gateway Handler
// src/handlers/api.handler.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
// Static initialization - runs once per container
// Configure allowed origins via ALLOWED_ORIGINS env var (comma-separated list)
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '').split(',').filter(Boolean);
const responseHeaders = {
'Content-Type': 'application/json',
...(allowedOrigins.length > 0 && {
'Access-Control-Allow-Origin': allowedOrigins.includes('*') ? '*' : allowedOrigins[0],
'Access-Control-Allow-Credentials': 'true',
}),
};
export const handler = async (
event: APIGatewayProxyEvent,
context: Context
): Promise<APIGatewayProxyResult> => {
console.log('Request:', {
requestId: context.awsRequestId,
path: event.path,
method: event.httpMethod,
});
try {
const { httpMethod, path, pathParameters, queryStringParameters, body } = event;
// Simple routing
switch (`${httpMethod} ${path}`) {
case 'GET /health':
return successResponse(200, { status: 'ok', timestamp: new Date().toISOString() });
case 'GET /users':
return await getUsers(queryStringParameters);
case 'GET /users/{id}':
return await getUser(pathParameters?.id);
case 'POST /users':
return await createUser(body);
default:
return errorResponse(404, 'Not Found');
}
} catch (error) {
console.error('Error:', error);
return errorResponse(500, 'Internal Server Error');
}
};
// Service functions
async function getUsers(queryParams: Record<string, string> | null): Promise<APIGatewayProxyResult> {
// Implementation
return successResponse(200, { users: [] });
}
async function getUser(id: string | undefined): Promise<APIGatewayProxyResult> {
if (!id) {
return errorResponse(400, 'User ID is required');
}
// Implementation
return successResponse(200, { id, name: 'John Doe' });
}
async function createUser(body: string | null): Promise<APIGatewayProxyResult> {
if (!body) {
return errorResponse(400, 'Request body is required');
}
try {
const user = JSON.parse(body);
// Validate and save
return successResponse(201, { id: '123', ...user });
} catch (error) {
return errorResponse(400, 'Invalid JSON in request body');
}
}
// Utility functions
function successResponse(statusCode: number, data: unknown): APIGatewayProxyResult {
return {
statusCode,
headers: responseHeaders,
body: JSON.stringify(data),
};
}
function errorResponse(statusCode: number, message: string): APIGatewayProxyResult {
return {
statusCode,
headers: responseHeaders,
body: JSON.stringify({ error: message }),
};
}Handler with Service Layer
// src/handlers/user.handler.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { UserService } from '../services/user.service';
import { DynamoDbUserRepository } from '../repositories/dynamodb-user.repository';
// Lazy-initialized singleton
let userService: UserService | null = null;
function getUserService(): UserService {
if (!userService) {
const repository = new DynamoDbUserRepository();
userService = new UserService(repository);
}
return userService;
}
export const handler = async (
event: APIGatewayProxyEvent,
context: Context
): Promise<APIGatewayProxyResult> => {
const service = getUserService();
try {
switch (event.httpMethod) {
case 'GET':
if (event.pathParameters?.id) {
const user = await service.findById(event.pathParameters.id);
return user
? { statusCode: 200, body: JSON.stringify(user) }
: { statusCode: 404, body: JSON.stringify({ error: 'User not found' }) };
}
const users = await service.findAll();
return { statusCode: 200, body: JSON.stringify(users) };
case 'POST':
const created = await service.create(JSON.parse(event.body || '{}'));
return { statusCode: 201, body: JSON.stringify(created) };
case 'PUT':
const updated = await service.update(
event.pathParameters?.id!,
JSON.parse(event.body || '{}')
);
return { statusCode: 200, body: JSON.stringify(updated) };
case 'DELETE':
await service.delete(event.pathParameters?.id!);
return { statusCode: 204, body: '' };
default:
return { statusCode: 405, body: JSON.stringify({ error: 'Method not allowed' }) };
}
} catch (error) {
console.error('Handler error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' }),
};
}
};S3 Event Handler
// src/handlers/s3.handler.ts
import { S3Handler, S3Event, Context } from 'aws-lambda';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
// Static client instance
const s3Client = new S3Client({});
export const handler: S3Handler = async (event: S3Event, context: Context): Promise<void> => {
console.log('S3 Event:', JSON.stringify(event));
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));
try {
// Process the S3 object
const response = await s3Client.send(
new GetObjectCommand({
Bucket: bucket,
Key: key,
})
);
// Process based on file type
if (key.endsWith('.json')) {
await processJsonFile(response.Body, key);
} else if (key.endsWith('.csv')) {
await processCsvFile(response.Body, key);
}
console.log(`Successfully processed s3://${bucket}/${key}`);
} catch (error) {
console.error(`Error processing s3://${bucket}/${key}:`, error);
throw error; // Let Lambda retry
}
}
};
async function processJsonFile(body: ReadableStream | undefined, key: string): Promise<void> {
if (!body) return;
// Implementation
}
async function processCsvFile(body: ReadableStream | undefined, key: string): Promise<void> {
if (!body) return;
// Implementation
}SQS Event Handler
// src/handlers/sqs.handler.ts
import { SQSHandler, SQSEvent, SQSRecord, Context } from 'aws-lambda';
interface MessagePayload {
type: string;
data: unknown;
}
export const handler: SQSHandler = async (event: SQSEvent, context: Context): Promise<void> => {
console.log('SQS Event:', {
recordCount: event.Records.length,
requestId: context.awsRequestId,
});
const batchItemFailures: { itemIdentifier: string }[] = [];
for (const record of event.Records) {
try {
await processMessage(record);
} catch (error) {
console.error(`Failed to process message ${record.messageId}:`, error);
batchItemFailures.push({ itemIdentifier: record.messageId });
}
}
// Return partial batch response for failed items
if (batchItemFailures.length > 0) {
throw new Error(`Batch item failures: ${JSON.stringify(batchItemFailures)}`);
}
};
async function processMessage(record: SQSRecord): Promise<void> {
const payload: MessagePayload = JSON.parse(record.body);
console.log('Processing message:', {
messageId: record.messageId,
type: payload.type,
});
switch (payload.type) {
case 'SEND_EMAIL':
await sendEmail(payload.data);
break;
case 'PROCESS_ORDER':
await processOrder(payload.data);
break;
default:
console.warn('Unknown message type:', payload.type);
}
}
async function sendEmail(data: unknown): Promise<void> {
// Implementation
}
async function processOrder(data: unknown): Promise<void> {
// Implementation
}---
Dependency Injection Patterns
Simple DI Container
// src/config/container.ts
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
// Service interfaces
export interface IUserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<User>;
delete(id: string): Promise<void>;
}
export interface IEmailService {
send(to: string, subject: string, body: string): Promise<void>;
}
// Simple container
class Container {
private services = new Map<string, unknown>();
register<T>(key: string, factory: () => T): void {
this.services.set(key, factory);
}
resolve<T>(key: string): T {
const service = this.services.get(key);
if (!service) {
throw new Error(`Service ${key} not registered`);
}
if (typeof service === 'function') {
const instance = (service as () => T)();
this.services.set(key, instance); // Cache instance
return instance;
}
return service as T;
}
}
// Global container instance
export const container = new Container();
// Registration
export function initializeContainer(): void {
// Database client
container.register('dynamoClient', () => {
const client = new DynamoDBClient({});
return DynamoDBDocumentClient.from(client);
});
// Repositories
container.register('userRepository', () => {
return new DynamoDbUserRepository(container.resolve('dynamoClient'));
});
// Services
container.register('userService', () => {
return new UserService(container.resolve('userRepository'));
});
container.register('emailService', () => {
return new SesEmailService();
});
}
// Type definitions
export interface User {
id: string;
email: string;
name: string;
}
// Repository implementation
class DynamoDbUserRepository implements IUserRepository {
constructor(private client: DynamoDBDocumentClient) {}
async findById(id: string): Promise<User | null> {
// Implementation
return null;
}
async save(user: User): Promise<User> {
// Implementation
return user;
}
async delete(id: string): Promise<void> {
// Implementation
}
}
// Service implementation
class UserService {
constructor(private repository: IUserRepository) {}
async findById(id: string): Promise<User | null> {
return this.repository.findById(id);
}
}
// Email service
class SesEmailService implements IEmailService {
async send(to: string, subject: string, body: string): Promise<void> {
// SES implementation
}
}Handler with DI
// src/handlers/di-handler.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { container, initializeContainer } from '../config/container';
// Initialize container on first import
let initialized = false;
function ensureInitialized(): void {
if (!initialized) {
initializeContainer();
initialized = true;
}
}
export const handler = async (
event: APIGatewayProxyEvent,
context: Context
): Promise<APIGatewayProxyResult> => {
ensureInitialized();
const userService = container.resolve<UserService>('userService');
// Handler logic
return {
statusCode: 200,
body: JSON.stringify({ message: 'OK' }),
};
};---
Cold Start Optimization
Lazy Loading Pattern
// src/utils/lazy-loader.ts
export class LazyLoader<T> {
private instance: T | null = null;
private initializing = false;
private initPromise: Promise<T> | null = null;
constructor(private factory: () => Promise<T> | T) {}
async get(): Promise<T> {
if (this.instance) {
return this.instance;
}
if (!this.initPromise) {
this.initPromise = Promise.resolve(this.factory()).then((instance) => {
this.instance = instance;
return instance;
});
}
return this.initPromise;
}
getSync(): T | null {
return this.instance;
}
}
// Usage
const dbConnection = new LazyLoader(async () => {
console.log('Initializing database connection...');
const connection = await createConnection({
host: process.env.DB_HOST,
database: process.env.DB_NAME,
// Connection pool optimized for Lambda
max: 2,
min: 0,
idleTimeoutMillis: 10000,
});
return connection;
});
// In handler
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
const db = await dbConnection.get();
// Use db
};Module-Level Caching
// src/config/database.ts
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
// Module-level cache - persists across warm invocations
let cachedClient: DynamoDBDocumentClient | null = null;
export function getDynamoClient(): DynamoDBDocumentClient {
if (!cachedClient) {
const client = new DynamoDBClient({
// Optimize for Lambda
maxAttempts: 3,
requestHandler: {
requestTimeout: 5000,
},
});
cachedClient = DynamoDBDocumentClient.from(client, {
marshallOptions: {
convertEmptyValues: false,
removeUndefinedValues: true,
convertClassInstanceToMap: true,
},
});
console.log('DynamoDB client initialized');
}
return cachedClient;
}
// For testing - allow reset
export function resetDynamoClient(): void {
cachedClient = null;
}---
TypeScript Configuration
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}tsconfig.prod.json (Production)
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"declaration": false,
"declarationMap": false,
"removeComments": true
},
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "tests"]
}---
Build and Packaging
esbuild Configuration
// build.js
const esbuild = require('esbuild');
async function build() {
try {
await esbuild.build({
entryPoints: [
'src/handlers/api.handler.ts',
'src/handlers/s3.handler.ts',
'src/handlers/sqs.handler.ts',
],
bundle: true,
platform: 'node',
target: 'node20',
outdir: 'dist',
format: 'esm',
splitting: true,
minify: true,
sourcemap: true,
external: [
'aws-sdk', // Provided by Lambda runtime
],
banner: {
js: 'import { createRequire } from "module"; import { fileURLToPath } from "url"; import { dirname } from "path"; const require = createRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);',
},
metafile: true,
});
console.log('Build completed successfully');
} catch (error) {
console.error('Build failed:', error);
process.exit(1);
}
}
build();SAM Template
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Runtime: nodejs20.x
MemorySize: 256
Timeout: 10
Architectures:
- x86_64
Environment:
Variables:
NODE_OPTIONS: '--enable-source-maps'
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: es2022
Sourcemap: true
EntryPoints:
- src/handlers/api.handler.ts
Format: esm
Platform: node
Properties:
FunctionName: !Sub '${AWS::StackName}-api'
Handler: api.handler
CodeUri: ./
Description: Raw TypeScript API handler
Events:
ApiRoot:
Type: Api
Properties:
Path: /
Method: ANY
ApiProxy:
Type: Api
Properties:
Path: /{proxy+}
Method: ANY
S3ProcessorFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: es2022
EntryPoints:
- src/handlers/s3.handler.ts
Format: esm
Properties:
FunctionName: !Sub '${AWS::StackName}-s3-processor'
Handler: s3.handler
CodeUri: ./
Events:
S3Event:
Type: S3
Properties:
Bucket: !Ref InputBucket
Events: s3:ObjectCreated:*
SQSProcessorFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: es2022
EntryPoints:
- src/handlers/sqs.handler.ts
Format: esm
Properties:
FunctionName: !Sub '${AWS::StackName}-sqs-processor'
Handler: sqs.handler
CodeUri: ./
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt ProcessingQueue.Arn
BatchSize: 10
FunctionResponseTypes:
- ReportBatchItemFailures
InputBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub '${AWS::StackName}-input'
ProcessingQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: !Sub '${AWS::StackName}-processing'
VisibilityTimeout: 60---
Testing
Jest Configuration
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/handlers/*.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};Handler Unit Test
// tests/handlers/api.handler.test.ts
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
import { handler } from '../../src/handlers/api.handler';
const mockContext: Partial<Context> = {
awsRequestId: 'test-request-id',
functionName: 'test-function',
memoryLimitInMB: '256',
invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789:function:test',
getRemainingTimeInMillis: () => 30000,
};
describe('API Handler', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return 200 for health check', async () => {
const event: Partial<APIGatewayProxyEvent> = {
httpMethod: 'GET',
path: '/health',
headers: {},
queryStringParameters: null,
body: null,
};
const result = await handler(event as APIGatewayProxyEvent, mockContext as Context);
expect(result.statusCode).toBe(200);
const body = JSON.parse(result.body);
expect(body.status).toBe('ok');
});
it('should return 404 for unknown routes', async () => {
const event: Partial<APIGatewayProxyEvent> = {
httpMethod: 'GET',
path: '/unknown',
headers: {},
};
const result = await handler(event as APIGatewayProxyEvent, mockContext as Context);
expect(result.statusCode).toBe(404);
});
it('should return 400 for missing user ID', async () => {
const event: Partial<APIGatewayProxyEvent> = {
httpMethod: 'GET',
path: '/users/{id}',
pathParameters: {},
headers: {},
};
const result = await handler(event as APIGatewayProxyEvent, mockContext as Context);
expect(result.statusCode).toBe(400);
});
it('should return 400 for invalid JSON body', async () => {
const event: Partial<APIGatewayProxyEvent> = {
httpMethod: 'POST',
path: '/users',
body: 'invalid json',
headers: {},
};
const result = await handler(event as APIGatewayProxyEvent, mockContext as Context);
expect(result.statusCode).toBe(400);
});
});Service Test with Mocks
// tests/services/user.service.test.ts
import { UserService } from '../../src/services/user.service';
import { IUserRepository } from '../../src/config/container';
const mockRepository: jest.Mocked<IUserRepository> = {
findById: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
};
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
service = new UserService(mockRepository);
jest.clearAllMocks();
});
describe('findById', () => {
it('should return user when found', async () => {
const user = { id: '123', name: 'John', email: 'john@example.com' };
mockRepository.findById.mockResolvedValue(user);
const result = await service.findById('123');
expect(result).toEqual(user);
expect(mockRepository.findById).toHaveBeenCalledWith('123');
});
it('should return null when user not found', async () => {
mockRepository.findById.mockResolvedValue(null);
const result = await service.findById('999');
expect(result).toBeNull();
});
});
describe('create', () => {
it('should create and return user', async () => {
const input = { name: 'John', email: 'john@example.com' };
const created = { id: '123', ...input };
mockRepository.save.mockResolvedValue(created);
const result = await service.create(input);
expect(result).toEqual(created);
expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining(input));
});
});
});---
Deployment
Serverless Framework
# serverless.yml
service: raw-ts-lambda
provider:
name: aws
runtime: nodejs20.x
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'us-east-1'}
memorySize: 256
timeout: 10
environment:
NODE_OPTIONS: '--enable-source-maps'
STAGE: ${self:provider.stage}
iam:
role:
statements:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource:
- !Sub 'arn:aws:logs:${aws:region}:${aws:accountId}:log-group:/aws/lambda/${self:service}-*'
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:Query
- dynamodb:Scan
Resource:
- !GetAtt UsersTable.Arn
plugins:
- serverless-esbuild
custom:
esbuild:
bundle: true
minify: ${self:custom.isProduction}
sourcemap: true
target: node20
platform: node
format: esm
splitting: true
entryPoints:
- src/handlers/api.handler.ts
- src/handlers/s3.handler.ts
- src/handlers/sqs.handler.ts
external:
- aws-sdk
isProduction: !Equals ['${self:provider.stage}', 'prod']
functions:
api:
handler: dist/api.handler
events:
- http:
path: /{proxy+}
method: ANY
cors: true
s3Processor:
handler: dist/s3.handler
events:
- s3:
bucket: ${self:service}-input-${self:provider.stage}
event: s3:ObjectCreated:*
sqsProcessor:
handler: dist/sqs.handler
events:
- sqs:
arn:
Fn::GetAtt:
- ProcessingQueue
- Arn
batchSize: 10
functionResponseType: ReportBatchItemFailures
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-users-${self:provider.stage}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProcessingQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: ${self:service}-processing-${self:provider.stage}
VisibilityTimeout: 60---
Best Practices Summary
1. Use static/module-level caching for AWS clients and connections 2. Implement lazy loading for expensive resources 3. Keep dependencies minimal - only include what you need 4. Use esbuild for fast bundling and smaller package size 5. Implement proper error handling with structured responses 6. Use TypeScript strict mode for better type safety 7. Write unit tests with mocked dependencies 8. Use environment variables for configuration 9. Enable source maps for easier debugging 10. Monitor cold starts and optimize initialization code
Cold Start Benchmarks
| Configuration | Cold Start | Warm Start | Memory |
|---|---|---|---|
| Minimal (no deps) | ~50ms | ~2ms | 128MB |
| With DynamoDB client | ~150ms | ~5ms | 256MB |
| With full AWS SDK | ~300ms | ~10ms | 512MB |
When to Use Raw TypeScript vs NestJS
Use Raw TypeScript when:
- Maximum performance is critical
- Minimal cold start is required
- Simple handlers with minimal logic
- Small team with no framework experience
- Cost optimization is priority
Use NestJS when:
- Complex application architecture
- Team familiar with NestJS
- Need dependency injection container
- Multiple modules and services
- Enterprise-grade requirements
Serverless Configuration Reference
Complete reference for deploying NestJS Lambda applications using Serverless Framework and AWS SAM.
Serverless Framework
Basic Configuration
service: nestjs-lambda-api
provider:
name: aws
runtime: nodejs20.x
region: ${opt:region, 'us-east-1'}
stage: ${opt:stage, 'dev'}
memorySize: 512
timeout: 29
environment:
NODE_ENV: production
DATABASE_URL: ${ssm:/${self:service}/${self:provider.stage}/database-url}
JWT_SECRET: ${ssm:/${self:service}/${self:provider.stage}/jwt-secret}
iam:
role:
statements:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource:
- !Sub 'arn:aws:logs:${aws:region}:${aws:accountId}:log-group:/aws/lambda/${self:service}-*'
package:
individually: false
patterns:
- '!node_modules/**'
- '!test/**'
- '!.git/**'
- 'dist/**'
- '!dist/tsconfig.build.tsbuildinfo'
custom:
esbuild:
bundle: true
minify: true
target: node20
platform: node
external:
- '@nestjs/microservices'
- '@nestjs/websockets'
- 'class-transformer/storage'
functions:
api:
handler: dist/lambda.handler
events:
- http:
path: /{proxy+}
method: ANY
cors: true
- http:
path: /
method: ANY
cors: true
provisionedConcurrency: ${self:custom.provisionedConcurrency.${self:provider.stage}, 0}
custom:
provisionedConcurrency:
prod: 5
dev: 0
plugins:
- serverless-esbuild
- serverless-offlineCommands
# Deploy
serverless deploy
serverless deploy --stage prod --region eu-west-1
# Local development
serverless offline
# Logs
serverless logs -f api -t
# Remove
serverless removeAWS SAM
Basic Template
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: NestJS Lambda API
Globals:
Function:
Timeout: 29
MemorySize: 512
Runtime: nodejs20.x
Architectures:
- x86_64
Parameters:
Environment:
Type: String
Default: dev
AllowedValues:
- dev
- staging
- prod
Resources:
NestJSApiFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${AWS::StackName}-api'
Handler: dist/lambda.handler
CodeUri: ./
Environment:
Variables:
NODE_ENV: !Ref Environment
DATABASE_URL: !Sub '{{resolve:ssm-secure:/${AWS::StackName}/database-url}}'
Events:
ApiGatewayRoot:
Type: Api
Properties:
Path: /
Method: ANY
RestApiId: !Ref ApiGatewayApi
ApiGatewayProxy:
Type: Api
Properties:
Path: /{proxy+}
Method: ANY
RestApiId: !Ref ApiGatewayApi
Policies:
- AWSLambdaBasicExecutionRole
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Environment
Cors:
AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
AllowHeaders: "'Content-Type,Authorization'"
AllowOrigin: !Sub "'${AllowedOrigins}'"
Outputs:
ApiUrl:
Description: API Gateway endpoint URL
Value: !Sub 'https://${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com/${Environment}/'Commands
# Build
sam build
# Local development
sam local start-api --warm-containers EAGER
# Deploy
sam deploy --guided
sam deploy --config-env prod
# Logs
sam logs -n NestJSApiFunction --tail
# Delete
sam deleteComparison
| Feature | SAM | Serverless Framework |
|---|---|---|
| Native AWS | Yes | No (uses CloudFormation) |
| Local testing | sam local | serverless-offline |
| CI/CD integration | AWS-native | Multi-cloud support |
| Syntax | YAML/JSON | YAML/TypeScript/JavaScript |
| Plugins | SAR, nested stacks | Rich plugin ecosystem |
When to Choose
Choose SAM when:
- Native AWS environment
- Team familiar with CloudFormation
- Deep AWS service integration needed
- AWS CodePipeline/CodeBuild usage
Choose Serverless Framework when:
- Multi-cloud requirements
- Large plugin ecosystem needed
- Prefer TypeScript/JavaScript config
- Easier local development preferred
Deployment Strategies for NestJS Lambda
Overview
This reference covers deployment strategies, CI/CD pipelines, and optimization techniques for NestJS Lambda applications using AWS SAM and Serverless Framework.
AWS SAM (Serverless Application Model)
Installation
# macOS
brew tap aws/tap
brew install aws-sam-cli
# Linux
wget https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip
unzip aws-sam-cli-linux-x86_64.zip -d sam-installation
sudo ./sam-installation/install
# Verify installation
sam --versionProject Structure
my-nestjs-lambda/
├── src/ # NestJS source code
├── dist/ # Compiled output
├── template.yaml # SAM template
├── samconfig.toml # SAM configuration
├── lambda.ts # Lambda entry point
├── package.json
└── tsconfig.jsonSAM Template Reference
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: NestJS Lambda Application
Globals:
Function:
Timeout: 29
MemorySize: 512
Runtime: nodejs20.x
Architectures:
- x86_64
Environment:
Variables:
NODE_ENV: production
LOG_LEVEL: info
Tags:
Application: NestJS API
Environment: !Ref Environment
Parameters:
Environment:
Type: String
Default: dev
AllowedValues:
- dev
- staging
- prod
Description: Deployment environment
DatabaseUrl:
Type: String
NoEcho: true
Description: Database connection string
AllowedOrigins:
Type: String
Default: ""
Description: Comma-separated list of allowed CORS origins (leave empty to disable CORS)
Conditions:
IsProduction: !Equals [!Ref Environment, prod]
Resources:
# API Gateway
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Environment
Cors:
AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'"
AllowOrigin: !Sub "'${AllowedOrigins}'"
MaxAge: "'600'"
Auth:
DefaultAuthorizer: CognitoAuthorizer
Authorizers:
CognitoAuthorizer:
UserPoolArn: !GetAtt UserPool.Arn
Identity:
Header: Authorization
ValidationExpression: ^Bearer [-0-9a-zA-Z\._]*$
# Lambda Function
NestJSApiFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub '${AWS::StackName}-api'
Handler: dist/lambda.handler
CodeUri: ./
Description: NestJS API Lambda function
Role: !GetAtt LambdaExecutionRole.Arn
Environment:
Variables:
DATABASE_URL: !Ref DatabaseUrl
JWT_SECRET: !Sub '{{resolve:secretsmanager:${JWTSecret}:SecretString:jwt}}'
COGNITO_USER_POOL_ID: !Ref UserPool
COGNITO_CLIENT_ID: !Ref UserPoolClient
Events:
ApiRoot:
Type: Api
Properties:
RestApiId: !Ref ApiGatewayApi
Path: /
Method: ANY
ApiProxy:
Type: Api
Properties:
RestApiId: !Ref ApiGatewayApi
Path: /{proxy+}
Method: ANY
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: !If [IsProduction, 5, 0]
AutoPublishAlias: live
DeploymentPreference:
Type: Canary10Percent5Minutes
Alarms:
- !Ref ErrorAlarm
- !Ref LatencyAlarm
Hooks:
PreTraffic: !Ref PreTrafficHookFunction
PostTraffic: !Ref PostTrafficHookFunction
# Lambda Execution Role
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
Policies:
- PolicyName: CloudWatchLogsPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- logs:DescribeLogStreams
Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*'
- PolicyName: SecretsManagerPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: !Ref JWTSecret
- PolicyName: DynamoDBPolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:Query
- dynamodb:Scan
Resource: !GetAtt DynamoDBTable.Arn
- PolicyName: S3Policy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
Resource: !Sub '${S3Bucket.Arn}/*'
# VPC Configuration for RDS access
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group for Lambda functions
VpcId: !Ref VPC
SecurityGroupIngress: []
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
CidrIp: !Ref VPCCidr
# DynamoDB Table
DynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Sub '${AWS::StackName}-data'
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
- AttributeName: GSI1PK
AttributeType: S
- AttributeName: GSI1SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
GlobalSecondaryIndexes:
- IndexName: GSI1
KeySchema:
- AttributeName: GSI1PK
KeyType: HASH
- AttributeName: GSI1SK
KeyType: RANGE
Projection:
ProjectionType: ALL
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: !If [IsProduction, true, false]
# S3 Bucket
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub '${AWS::StackName}-assets-${AWS::AccountId}'
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
LifecycleConfiguration:
Rules:
- Id: ExpireOldVersions
Status: Enabled
NoncurrentVersionExpirationInDays: 30
# Secrets Manager
JWTSecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub '${AWS::StackName}/jwt-secret'
Description: JWT signing secret
GenerateSecretString:
SecretStringTemplate: '{"jwt":""}'
GenerateStringKey: jwt
PasswordLength: 32
ExcludeCharacters: '"@/\'
# CloudWatch Alarms
ErrorAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub '${AWS::StackName}-errors'
AlarmDescription: Lambda error rate alarm
MetricName: Errors
Namespace: AWS/Lambda
Statistic: Sum
Period: 60
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
Dimensions:
- Name: FunctionName
Value: !Ref NestJSApiFunction
LatencyAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub '${AWS::StackName}-latency'
AlarmDescription: Lambda duration alarm
MetricName: Duration
Namespace: AWS/Lambda
Statistic: p99
Period: 60
EvaluationPeriods: 2
Threshold: 5000
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: FunctionName
Value: !Ref NestJSApiFunction
# CloudWatch Log Group
LambdaLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/aws/lambda/${NestJSApiFunction}'
RetentionInDays: !If [IsProduction, 30, 7]
Outputs:
ApiUrl:
Description: API Gateway endpoint URL
Value: !Sub 'https://${ApiGatewayApi}.execute-api.${AWS::Region}.amazonaws.com/${Environment}/'
Export:
Name: !Sub '${AWS::StackName}-api-url'
FunctionArn:
Description: Lambda function ARN
Value: !GetAtt NestJSApiFunction.Arn
Export:
Name: !Sub '${AWS::StackName}-function-arn'SAM Configuration (samconfig.toml)
version = 0.1
[default]
[default.global.parameters]
stack_name = "nestjs-lambda-api"
[default.build.parameters]
cached = true
parallel = true
[default.validate.parameters]
lint = true
[default.deploy.parameters]
capabilities = "CAPABILITY_IAM CAPABILITY_AUTO_EXPAND"
confirm_changeset = true
resolve_s3 = true
s3_prefix = "nestjs-lambda-api"
region = "us-east-1"
image_repositories = []
parameter_overrides = [
"Environment=dev"
]
[default.deploy.parameters.globals]
[default.deploy.parameters.globals.parameters]
parallel = true
[dev.deploy.parameters]
stack_name = "nestjs-lambda-api-dev"
parameter_overrides = [
"Environment=dev",
"DatabaseUrl=postgres://..."
]
[staging.deploy.parameters]
stack_name = "nestjs-lambda-api-staging"
parameter_overrides = [
"Environment=staging",
"DatabaseUrl=postgres://..."
]
[prod.deploy.parameters]
stack_name = "nestjs-lambda-api-prod"
parameter_overrides = [
"Environment=prod",
"DatabaseUrl=postgres://..."
]
[default.sync.parameters]
watch = true
[default.local_start_api.parameters]
warm_containers = "EAGER"
[default.local_start_lambda.parameters]
warm_containers = "EAGER"SAM Commands
# Initialize SAM project
sam init --runtime nodejs20.x --dependency-manager npm --app-template hello-world
# Validate template
sam validate --lint
# Build application
sam build
# Local development
sam local invoke NestJSApiFunction -e events/api-event.json
sam local start-api --warm-containers EAGER
# Deploy
sam deploy --guided
sam deploy --config-env prod
# Sync (for development)
sam sync --watch
# Logs
sam logs -n NestJSApiFunction --tail
# Delete stack
sam deleteSAM Policy Templates
# Inline policy for specific AWS services
Policies:
- S3ReadPolicy:
BucketName: !Ref S3Bucket
- S3WritePolicy:
BucketName: !Ref S3Bucket
- DynamoDBReadPolicy:
TableName: !Ref DynamoDBTable
- DynamoDBCrudPolicy:
TableName: !Ref DynamoDBTable
- SESBulkTemplatedCrudPolicy:
IdentityName: !Ref SESIdentity
- SQSSendMessagePolicy:
QueueName: !GetAtt SQSQueue.QueueName
- SNSPublishMessagePolicy:
TopicName: !GetAtt SNSTopic.TopicName
- CloudWatchPutMetricPolicy: {}
- VPCAccessPolicy: {}Serverless Framework
Installation
# Install Serverless Framework
npm install -g serverless
# Or use npx
npx serverless
# Verify installation
serverless --versionProject Structure
my-nestjs-lambda/
├── src/ # NestJS source code
├── dist/ # Compiled output
├── serverless.yml # Serverless configuration
├── serverless.ts # TypeScript config (optional)
├── lambda.ts # Lambda entry point
├── package.json
└── webpack.config.jsServerless Configuration Reference
service: nestjs-lambda-api
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs20.x
stage: ${opt:stage, 'dev'}
region: ${opt:region, 'us-east-1'}
memorySize: 512
timeout: 29
logRetentionInDays: 14
versionFunctions: true
# Environment variables
environment:
NODE_ENV: production
STAGE: ${self:provider.stage}
SERVICE_NAME: ${self:service}
DATABASE_URL: ${ssm:/${self:service}/${self:provider.stage}/database-url}
JWT_SECRET: ${ssm:/${self:service}/${self:provider.stage}/jwt-secret~true}
SENTRY_DSN: ${ssm:/${self:service}/${self:provider.stage}/sentry-dsn~true}
# IAM role statements
iam:
role:
name: ${self:service}-${self:provider.stage}-lambda-role
statements:
# CloudWatch Logs
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource:
- 'arn:aws:logs:${aws:region}:${aws:accountId}:log-group:/aws/lambda/*:*:*'
# X-Ray Tracing
- Effect: Allow
Action:
- xray:PutTraceSegments
- xray:PutTelemetryRecords
Resource:
- !Sub 'arn:aws:xray:${aws:region}:${aws:accountId}:*'
# SSM Parameter Store
- Effect: Allow
Action:
- ssm:GetParameter
- ssm:GetParameters
Resource:
- 'arn:aws:ssm:${aws:region}:${aws:accountId}:parameter/${self:service}/*'
# DynamoDB
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:Query
- dynamodb:Scan
- dynamodb:BatchGetItem
- dynamodb:BatchWriteItem
Resource:
- !GetAtt DynamoDBTable.Arn
- !Sub '${DynamoDBTable.Arn}/index/*'
# S3
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
- s3:ListBucket
Resource:
- !GetAtt S3Bucket.Arn
- !Sub '${S3Bucket.Arn}/*'
# SQS
- Effect: Allow
Action:
- sqs:SendMessage
- sqs:SendMessageBatch
- sqs:ReceiveMessage
- sqs:DeleteMessage
- sqs:GetQueueAttributes
Resource:
- !GetAtt SQSQueue.Arn
# SNS
- Effect: Allow
Action:
- sns:Publish
Resource:
- !Ref SNSTopic
# SES
- Effect: Allow
Action:
- ses:SendEmail
- ses:SendRawEmail
- ses:SendTemplatedEmail
Resource:
- !Sub 'arn:aws:ses:${aws:region}:${aws:accountId}:identity/${self:custom.email.from}'
Condition:
StringEquals:
ses:FromAddress: ${self:custom.email.from}
# Secrets Manager
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- 'arn:aws:secretsmanager:${aws:region}:${aws:accountId}:secret:${self:service}/*'
# VPC configuration
vpc:
securityGroupIds:
- !Ref LambdaSecurityGroup
subnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
# API Gateway
apiGateway:
binaryMediaTypes:
- '*/*'
minimumCompressionSize: 1024
# Tracing
tracing:
lambda: true
apiGateway: true
# Tags
tags:
Service: ${self:service}
Stage: ${self:provider.stage}
ManagedBy: serverless
# Stack tags
stackTags:
Service: ${self:service}
Stage: ${self:provider.stage}
plugins:
- serverless-esbuild
- serverless-offline
- serverless-plugin-aws-alerts
- serverless-plugin-warmup
custom:
# esbuild configuration
esbuild:
bundle: true
minify: ${self:custom.isProduction}
sourcemap: true
target: node20
platform: node
format: cjs
mainFields:
- main
- module
external:
- '@nestjs/microservices'
- '@nestjs/websockets'
- 'class-transformer/storage'
- 'aws-sdk'
keepNames: true
splitting: false
concurrency: 10
packager: npm
installExtraArgs: ['--legacy-peer-deps']
# Offline configuration
serverless-offline:
httpPort: 3000
host: 0.0.0.0
lambdaPort: 3002
noPrependStageInUrl: true
# Warming configuration
warmup:
warmer:
enabled: ${self:custom.isProduction}
events:
- schedule: rate(5 minutes)
concurrency: 2
# Alerts configuration
alerts:
stages:
- prod
dashboards: true
topics:
alarm:
- !Ref AlarmTopic
alarms:
- functionErrors
- functionThrottles
- functionInvocations
- functionDuration
definitions:
functionErrors:
threshold: 1
statistic: Sum
period: 60
evaluationPeriods: 1
comparisonOperator: GreaterThanOrEqualToThreshold
functionDuration:
threshold: 5000
statistic: p99
period: 60
evaluationPeriods: 2
comparisonOperator: GreaterThanThreshold
# Custom variables
isProduction: !Equals ['${self:provider.stage}', 'prod']
email:
from: noreply@example.com
package:
individually: false
patterns:
- '!**/*'
- 'dist/**'
- '!dist/**/*.map'
- '!dist/**/*.spec.js'
- '!dist/**/*.test.js'
- '!dist/**/__tests__/**'
- 'package.json'
functions:
api:
name: ${self:service}-${self:provider.stage}-api
handler: dist/lambda.handler
description: NestJS API Lambda function
memorySize: 512
timeout: 29
reservedConcurrency: 100
provisionedConcurrency: ${self:custom.isProduction, 0}
environment:
FUNCTION_NAME: api
events:
- http:
path: /{proxy+}
method: ANY
cors:
origin: ${self:custom.cors.origins}
headers:
- Content-Type
- Authorization
- X-Amz-Date
- X-Api-Key
- X-Amz-Security-Token
- X-Amz-User-Agent
allowCredentials: true
- http:
path: /
method: ANY
cors: true
iamRoleStatements:
- Effect: Allow
Action:
- execute-api:Invoke
Resource:
- !Sub 'arn:aws:execute-api:${aws:region}:${aws:accountId}:${ApiGatewayRestApiId}/*'
worker:
name: ${self:service}-${self:provider.stage}-worker
handler: dist/worker.handler
description: Background worker Lambda
memorySize: 256
timeout: 900
events:
- sqs:
arn:
Fn::GetAtt:
- SQSQueue
- Arn
batchSize: 10
maximumBatchingWindowInSeconds: 5
functionResponseType: ReportBatchItemFailures
destinationConfig:
onFailure:
destination:
Fn::GetAtt:
- DLQ
- Arn
scheduler:
name: ${self:service}-${self:provider.stage}-scheduler
handler: dist/scheduler.handler
description: Scheduled task Lambda
memorySize: 256
timeout: 300
events:
- schedule:
rate: rate(1 hour)
input:
task: cleanup
- schedule:
rate: cron(0 2 * * ? *)
input:
task: daily-report
resources:
Resources:
# DynamoDB Table
DynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service}-${self:provider.stage}
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: PK
AttributeType: S
- AttributeName: SK
AttributeType: S
KeySchema:
- AttributeName: PK
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
PointInTimeRecoverySpecification:
PointInTimeRecoveryEnabled: ${self:custom.isProduction}
SSESpecification:
SSEEnabled: true
Tags:
- Key: Service
Value: ${self:service}
- Key: Stage
Value: ${self:provider.stage}
# S3 Bucket
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:service}-${self:provider.stage}-assets-${aws:accountId}
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
LifecycleConfiguration:
Rules:
- Id: TransitionToIA
Status: Enabled
TransitionInDays: 30
StorageClass: STANDARD_IA
- Id: DeleteOldVersions
Status: Enabled
NoncurrentVersionExpirationInDays: 30
# SQS Queue
SQSQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: ${self:service}-${self:provider.stage}-queue
VisibilityTimeout: 960
MessageRetentionPeriod: 1209600
RedrivePolicy:
deadLetterTargetArn:
Fn::GetAtt:
- DLQ
- Arn
maxReceiveCount: 3
# Dead Letter Queue
DLQ:
Type: AWS::SQS::Queue
Properties:
QueueName: ${self:service}-${self:provider.stage}-dlq
MessageRetentionPeriod: 1209600
# SNS Topic
SNSTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: ${self:service}-${self:provider.stage}-topic
# Alarm Topic
AlarmTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: ${self:service}-${self:provider.stage}-alarms
# Security Group
LambdaSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: ${self:service}-${self:provider.stage}-lambda-sg
GroupDescription: Security group for Lambda functions
VpcId:
Ref: VPC
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 5432
ToPort: 5432
CidrIp:
Ref: VPCCidr
Outputs:
ApiGatewayRestApiId:
Value:
Ref: ApiGatewayRestApi
Export:
Name: ${self:service}-${self:provider.stage}-restApiId
ApiGatewayRestApiRootResourceId:
Value:
Fn::GetAtt:
- ApiGatewayRestApi
- RootResourceId
Export:
Name: ${self:service}-${self:provider.stage}-rootResourceIdServerless TypeScript Configuration
// serverless.ts
import type { AWS } from '@serverless/typescript';
const serverlessConfiguration: AWS = {
service: 'nestjs-lambda-api',
frameworkVersion: '3',
plugins: [
'serverless-esbuild',
'serverless-offline',
'serverless-plugin-aws-alerts',
],
provider: {
name: 'aws',
runtime: 'nodejs20.x',
stage: '${opt:stage, "dev"}',
region: '${opt:region, "us-east-1"}',
memorySize: 512,
timeout: 29,
environment: {
NODE_ENV: 'production',
DATABASE_URL: '${ssm:/${self:service}/${self:provider.stage}/database-url}',
},
iam: {
role: {
statements: [
{
Effect: 'Allow',
Action: ['logs:*'],
Resource: {
'Fn::Sub': 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${self:service}-*',
},
},
],
},
},
},
functions: {
api: {
handler: 'dist/lambda.handler',
events: [
{
http: {
path: '/{proxy+}',
method: 'ANY',
cors: true,
},
},
],
},
},
custom: {
esbuild: {
bundle: true,
minify: true,
target: 'node20',
},
},
};
module.exports = serverlessConfiguration;Serverless Commands
# Deploy
serverless deploy
serverless deploy --stage prod --region eu-west-1
serverless deploy function -f api
# Offline development
serverless offline
serverless offline start --reloadHandler
# Logs
serverless logs -f api -t
serverless logs -f api --startTime 5m
# Info
serverless info
serverless info --stage prod
# Remove
serverless remove
serverless remove --stage dev
# Print compiled config
serverless print
# Invoke function
serverless invoke -f api -p event.json
serverless invoke local -f api -p event.json
# Metrics
serverless metrics
serverless metrics -f api --startTime 1h
# Plugins
serverless plugin install -n serverless-esbuild
serverless plugin uninstall -n serverless-esbuild
serverless plugin list
serverless plugin search esbuildCI/CD Pipeline
GitHub Actions with SAM
# .github/workflows/sam-deploy.yml
name: SAM Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
AWS_REGION: us-east-1
NODE_VERSION: '20'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run lint
run: npm run lint
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup SAM
uses: aws-actions/setup-sam@v2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: SAM Build
run: sam build
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: sam-build
path: |
.aws-sam/
dist/
deploy-dev:
needs: build
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v4
- name: Setup SAM
uses: aws-actions/setup-sam@v2
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: sam-build
- name: SAM Deploy (Dev)
run: |
sam deploy --config-env dev \
--no-confirm-changeset \
--no-fail-on-empty-changeset
deploy-prod:
needs: deploy-dev
runs-on: ubuntu-latest
environment: production
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup SAM
uses: aws-actions/setup-sam@v2
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: sam-build
- name: SAM Deploy (Prod)
run: |
sam deploy --config-env prod \
--no-confirm-changeset \
--no-fail-on-empty-changesetGitHub Actions with Serverless Framework
# .github/workflows/serverless-deploy.yml
name: Serverless Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
AWS_REGION: us-east-1
NODE_VERSION: '20'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run lint
run: npm run lint
deploy-dev:
needs: test
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install Serverless
run: npm install -g serverless
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy to dev
run: serverless deploy --stage dev
deploy-prod:
needs: deploy-dev
runs-on: ubuntu-latest
environment: production
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install Serverless
run: npm install -g serverless
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Deploy to production
run: serverless deploy --stage prodBuild Optimization
Webpack Configuration
// webpack.config.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const TerserPlugin = require('terser-webpack-plugin');
const { IgnorePlugin } = require('webpack');
module.exports = {
entry: './lambda.ts',
target: 'node',
mode: 'production',
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.ts$/,
use: {
loader: 'ts-loader',
options: {
transpileOnly: true,
experimentalWatchApi: true,
},
},
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.js'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
output: {
filename: 'lambda.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: 'commonjs2',
clean: true,
},
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
keep_classnames: true,
keep_fnames: true,
compress: {
drop_console: true,
drop_debugger: true,
},
},
}),
],
},
plugins: [
// Ignore optional dependencies that aren't needed in Lambda
new IgnorePlugin({
resourceRegExp: /^@nestjs\/microservices$/,
}),
new IgnorePlugin({
resourceRegExp: /^@nestjs\/websockets$/,
}),
],
stats: {
modules: true,
warnings: false,
},
};esbuild Configuration
// esbuild.config.js
const esbuild = require('esbuild');
const { nodeExternalsPlugin } = require('esbuild-node-externals');
async function build() {
try {
await esbuild.build({
entryPoints: ['lambda.ts', 'worker.ts', 'scheduler.ts'],
bundle: true,
platform: 'node',
target: 'node20',
outdir: 'dist',
minify: process.env.NODE_ENV === 'production',
sourcemap: true,
splitting: false,
format: 'cjs',
metafile: true,
external: [
'@nestjs/microservices',
'@nestjs/websockets',
'class-transformer/storage',
'aws-sdk', // Provided by Lambda runtime
],
define: {
'process.env.NODE_ENV': '"production"',
},
plugins: [
nodeExternalsPlugin({
allowList: ['@nestjs/core', '@nestjs/common', '@nestjs/platform-express'],
}),
],
});
console.log('Build completed successfully');
} catch (error) {
console.error('Build failed:', error);
process.exit(1);
}
}
build();esbuild with Serverless
# serverless.yml with esbuild
custom:
esbuild:
bundle: true
minify: ${self:custom.isProduction}
sourcemap: true
target: node20
platform: node
format: cjs
mainFields:
- main
- module
external:
- '@nestjs/microservices'
- '@nestjs/websockets'
- 'class-transformer/storage'
- 'aws-sdk'
keepNames: true
splitting: false
concurrency: 10
packager: npm
installExtraArgs:
- '--legacy-peer-deps'
# Exclude specific paths from bundling
exclude:
- './test/**'
- './**/*.spec.ts'
- './**/*.test.ts'Package Optimization
Dependencies to Exclude
{
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@codegenie/serverless-express": "^4.0.0",
"aws-lambda": "^1.0.0",
"express": "^4.18.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.0"
},
"devDependencies": {
"@nestjs/testing": "^10.0.0",
"@types/aws-lambda": "^8.10.0",
"@types/express": "^4.17.0",
"@types/node": "^20.0.0",
"esbuild": "^0.20.0",
"serverless": "^3.0.0",
"serverless-esbuild": "^1.0.0",
"serverless-offline": "^13.0.0",
"ts-loader": "^9.0.0",
"typescript": "^5.0.0",
"webpack": "^5.0.0",
"webpack-node-externals": "^3.0.0"
}
}SAM Package Configuration
# template.yaml - package optimization
Globals:
Function:
CodeUri: ./
Runtime: nodejs20.x
Architectures:
- x86_64
# Exclude dev dependencies
Environment:
Variables:
NODE_ENV: production
Resources:
NestJSApiFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: es2020
Sourcemap: true
EntryPoints:
- lambda.ts
External:
- '@nestjs/microservices'
- '@nestjs/websockets'
Properties:
Handler: dist/lambda.handlerEnvironment Management
SAM Environment Variables
# Using parameter overrides
Parameters:
Environment:
Type: String
Default: dev
# Secure parameters
DatabaseUrl:
Type: String
NoEcho: true
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
Environment:
Variables:
STAGE: !Ref Environment
DATABASE_URL: !Ref DatabaseUrl
SECRET_VALUE: !Sub '{{resolve:secretsmanager:${MySecret}:SecretString:value}}'
SSM_VALUE: !Sub '{{resolve:ssm-secure:/${Environment}/my-param}}'SSM Parameter Store
# serverless.yml - SSM integration
provider:
environment:
# Standard parameters
API_URL: ${ssm:/${self:service}/${self:provider.stage}/api-url}
# Secure parameters (encrypted)
DATABASE_URL: ${ssm:/${self:service}/${self:provider.stage}/database-url~true}
# Reference by ARN
EXTERNAL_API_KEY: ${ssm:arn:aws:ssm:${aws:region}:${aws:accountId}:parameter/external/api-key}
iam:
role:
statements:
- Effect: Allow
Action:
- ssm:GetParameter
- ssm:GetParameters
Resource:
- arn:aws:ssm:${aws:region}:${aws:accountId}:parameter/${self:service}/*Secrets Manager
# SAM with Secrets Manager
Resources:
MySecret:
Type: AWS::SecretsManager::Secret
Properties:
Name: !Sub '${AWS::StackName}/my-secret'
GenerateSecretString:
SecretStringTemplate: '{"username":"admin"}'
GenerateStringKey: password
PasswordLength: 32
ExcludeCharacters: '"@/\'
MyFunction:
Type: AWS::Serverless::Function
Properties:
Environment:
Variables:
DB_SECRET: !Sub '{{resolve:secretsmanager:${MySecret}:SecretString}}'# Serverless with Secrets Manager
provider:
environment:
# Full secret JSON
DB_SECRET: ${secrets:${self:service}/${self:provider.stage}/database}
# Specific key from secret
DB_PASSWORD: ${secrets:${self:service}/${self:provider.stage}/database:password}
iam:
role:
statements:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource:
- arn:aws:secretsmanager:${aws:region}:${aws:accountId}:secret:${self:service}/*Monitoring
SAM CloudWatch Dashboard
Resources:
MonitoringDashboard:
Type: AWS::CloudWatch::Dashboard
Properties:
DashboardName: !Sub '${AWS::StackName}-dashboard'
DashboardBody: !Sub |
{
"widgets": [
{
"type": "metric",
"properties": {
"title": "Invocations",
"metrics": [
["AWS/Lambda", "Invocations", "FunctionName", "${NestJSApiFunction}", { "stat": "Sum" }]
],
"period": 300,
"region": "${AWS::Region}",
"yAxis": { "left": { "min": 0 } }
}
},
{
"type": "metric",
"properties": {
"title": "Errors",
"metrics": [
["AWS/Lambda", "Errors", "FunctionName", "${NestJSApiFunction}", { "stat": "Sum" }],
[".", "Throttles", ".", ".", { "stat": "Sum" }]
],
"period": 300,
"region": "${AWS::Region}"
}
},
{
"type": "metric",
"properties": {
"title": "Duration",
"metrics": [
["AWS/Lambda", "Duration", "FunctionName", "${NestJSApiFunction}", { "stat": "p99" }],
["...", { "stat": "Average" }]
],
"period": 300,
"region": "${AWS::Region}"
}
}
]
}Serverless CloudWatch Alarms
custom:
alerts:
stages:
- prod
- staging
dashboards: true
topics:
alarm:
- !Ref AlarmTopic
ok:
- !Ref OkTopic
alarms:
- functionErrors
- functionThrottles
- functionInvocations
- functionDuration
definitions:
functionErrors:
description: 'Function errors exceeded threshold'
threshold: 1
statistic: Sum
period: 60
evaluationPeriods: 1
comparisonOperator: GreaterThanOrEqualToThreshold
treatMissingData: notBreaching
functionThrottles:
description: 'Function throttled'
threshold: 1
statistic: Sum
period: 60
evaluationPeriods: 1
comparisonOperator: GreaterThanOrEqualToThreshold
functionDuration:
description: 'Function duration exceeded threshold'
threshold: 5000
statistic: p99
period: 60
evaluationPeriods: 2
comparisonOperator: GreaterThanThreshold
customAlarm:
description: 'Custom business metric'
metric: CustomMetric
namespace: MyNamespace
threshold: 100
statistic: Average
period: 300
evaluationPeriods: 1X-Ray Tracing
# SAM - Enable tracing
Globals:
Function:
Tracing: Active
Resources:
ApiGatewayApi:
Type: AWS::Serverless::Api
Properties:
TracingEnabled: true# Serverless - Enable tracing
provider:
tracing:
lambda: true
apiGateway: true// X-Ray instrumentation
import * as AWSXRay from 'aws-xray-sdk-core';
import * as AWS from 'aws-sdk';
// Capture AWS SDK calls
const tracedAWS = AWSXRay.captureAWS(AWS);
// Capture HTTP requests
import http from 'http';
import https from 'https';
AWSXRay.captureHTTPsGlobal(http);
AWSXRay.captureHTTPsGlobal(https);
// Custom subsegment
const segment = AWSXRay.getSegment();
const subsegment = segment?.addNewSubsegment('custom-operation');
try {
// Your code
subsegment?.close();
} catch (error) {
subsegment?.close(error);
}Performance Tuning
Memory Configuration
# SAM - Memory and CPU tuning
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
MemorySize: 512
# Memory impacts CPU proportionally
# 1769MB = 1 vCPU
# 3008MB = ~1.7 vCPU
ComputeIntensiveFunction:
Type: AWS::Serverless::Function
Properties:
MemorySize: 3008# Serverless - Memory per function
functions:
api:
memorySize: 512
compute:
memorySize: 1024
health:
memorySize: 256Provisioned Concurrency
# SAM - Provisioned Concurrency
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 10
AutoPublishAlias: live# Serverless - Provisioned Concurrency
functions:
api:
provisionedConcurrency: 10Reserved Concurrency
# SAM - Reserved Concurrency
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
ReservedConcurrentExecutions: 100# Serverless - Reserved Concurrency
functions:
api:
reservedConcurrency: 100Rollback Strategy
SAM Auto-Rollback
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
AutoPublishAlias: live
DeploymentPreference:
Type: Canary10Percent5Minutes
Alarms:
- !Ref ErrorAlarm
- !Ref LatencyAlarm
Hooks:
PreTraffic: !Ref PreTrafficHook
PostTraffic: !Ref PostTrafficHook
PreTrafficHook:
Type: AWS::Serverless::Function
Properties:
Handler: hooks.preTraffic
Runtime: nodejs20.x
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- codedeploy:PutLifecycleEventHookExecutionStatus
Resource:
- !Sub 'arn:aws:codedeploy:${aws:region}:${aws:accountId}:deploymentgroup:${AWS::StackName}/*'
PostTrafficHook:
Type: AWS::Serverless::Function
Properties:
Handler: hooks.postTraffic
Runtime: nodejs20.x
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- codedeploy:PutLifecycleEventHookExecutionStatus
Resource:
- !Sub 'arn:aws:codedeploy:${aws:region}:${aws:accountId}:deploymentgroup:${AWS::StackName}/*'Serverless Canary
custom:
canary:
type: LinearCanary
alias: Live
linearTrafficShifting:
- intervalMinutes: 10
percentage: 10
- intervalMinutes: 10
percentage: 50
- intervalMinutes: 10
percentage: 100
alarms:
- functionErrors
- functionDurationSecurity Best Practices
SAM VPC Configuration
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
VpcConfig:
SecurityGroupIds:
- !Ref LambdaSecurityGroup
SubnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
Policies:
- VPCAccessPolicy: {}Serverless VPC
provider:
vpc:
securityGroupIds:
- !Ref LambdaSecurityGroup
subnetIds:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2IAM Least Privilege
# SAM - Granular IAM
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
Resource:
- !GetAtt MyTable.Arn
Condition:
ForAllValues:StringEquals:
dynamodb:LeadingKeys:
- 'USER#${cognito-identity.amazonaws.com:sub}'# Serverless - Granular IAM
provider:
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
Resource:
- !GetAtt MyTable.Arn
Condition:
ForAllValues:StringEquals:
dynamodb:LeadingKeys:
- 'USER#${cognito-identity.amazonaws.com:sub}'Cost Optimization
Graviton2 (ARM64)
# SAM - ARM64
Globals:
Function:
Architectures:
- arm64# Serverless - ARM64
provider:
architecture: arm64SAM Lambda Layers
Resources:
CommonLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: common-dependencies
ContentUri: layers/common/
CompatibleRuntimes:
- nodejs20.x
RetentionPolicy: Retain
MyFunction:
Type: AWS::Serverless::Function
Properties:
Layers:
- !Ref CommonLayerServerless Lambda Layers
layers:
common:
path: layers/common
compatibleRuntimes:
- nodejs20.x
package:
patterns:
- 'node_modules/**'
functions:
api:
layers:
- { Ref: CommonLambdaLayer }SAM vs Serverless Framework
| Feature | SAM | Serverless Framework |
|---|---|---|
| Native AWS | Yes | No (uses CloudFormation) |
| Local testing | sam local | serverless-offline |
| CI/CD integration | AWS-native | Multi-cloud support |
| Syntax | YAML/JSON | YAML/TypeScript/JavaScript |
| Extensions | SAR, nested stacks | Plugins ecosystem |
| Debugging | SAM CLI | Serverless console |
When to Choose SAM
- Native AWS environment
- Team familiar with CloudFormation
- Need deep AWS service integration
- Want to use AWS CodePipeline/CodeBuild
When to Choose Serverless Framework
- Multi-cloud requirements
- Large plugin ecosystem needed
- Prefer TypeScript/JavaScript config
- Want easier local development
Complete Example Project
nestjs-lambda-sam/
├── src/
│ ├── app.module.ts
│ ├── main.ts
│ ├── lambda.ts
│ └── modules/
│ └── users/
├── dist/
├── template.yaml
├── samconfig.toml
├── package.json
└── tsconfig.jsonnestjs-lambda-serverless/
├── src/
│ ├── app.module.ts
│ ├── main.ts
│ ├── lambda.ts
│ └── modules/
│ └── users/
├── dist/
├── serverless.yml
├── package.json
└── tsconfig.jsonTesting Lambda Functions
Complete guide for testing NestJS Lambda handlers including unit tests, integration tests, and mocking strategies.
Unit Tests for Handler
Test the Lambda handler in isolation with mocked AWS Lambda context.
// lambda.spec.ts
import { handler } from './lambda';
import { Context } from 'aws-lambda';
describe('Lambda Handler', () => {
const mockContext: Partial<Context> = {
functionName: 'test-function',
memoryLimitInMB: '512',
invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789:function:test',
awsRequestId: 'test-request-id',
};
beforeEach(() => {
// Reset cached server for cold start tests
jest.resetModules();
});
it('should bootstrap on first invocation (cold start)', async () => {
const event = {
httpMethod: 'GET',
path: '/api/health',
headers: {},
body: null,
};
const result = await handler(event, mockContext as Context, () => {});
expect(result.statusCode).toBe(200);
});
it('should reuse instance on warm invocation', async () => {
const event = {
httpMethod: 'GET',
path: '/api/health',
headers: {},
body: null,
};
// First invocation
await handler(event, mockContext as Context, () => {});
// Second invocation (should use cached server)
const start = Date.now();
const result = await handler(event, mockContext as Context, () => {});
const duration = Date.now() - start;
expect(result.statusCode).toBe(200);
expect(duration).toBeLessThan(100); // Warm start should be fast
});
});Integration Test with serverless-offline
Test the full request/response cycle using the serverless-offline plugin.
// test/lambda.integration.spec.ts
describe('Lambda Integration', () => {
let server: any;
beforeAll(async () => {
// Bootstrap for local testing
const { bootstrap } = await import('../lambda');
server = await bootstrap();
});
afterAll(async () => {
if (server) {
// Cleanup
}
});
it('should handle API Gateway events', async () => {
const event = {
httpMethod: 'POST',
path: '/api/users',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Test User' }),
};
const result = await server(event, {});
expect(result.statusCode).toBe(201);
expect(JSON.parse(result.body)).toHaveProperty('id');
});
});Mocking Strategies
Mock AWS Services
// __mocks__/aws-sdk.ts
export const mockSend = jest.fn();
export const DynamoDBClient = jest.fn(() => ({
send: mockSend,
}));
export const GetItemCommand = jest.fn();
export const PutItemCommand = jest.fn();Mock NestJS Services
// test/mocks/services.mock.ts
export const createMockUserService = () => ({
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
});
// In test
const mockUserService = createMockUserService();
mockUserService.findById.mockResolvedValue({ id: '123', name: 'Test' });Mock Lambda Context
// test/utils/lambda-context.ts
import { Context } from 'aws-lambda';
export function createMockContext(overrides?: Partial<Context>): Context {
return {
awsRequestId: 'test-request-id',
functionName: 'test-function',
memoryLimitInMB: '512',
invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789:function:test',
getRemainingTimeInMillis: () => 30000,
done: () => {},
fail: () => {},
succeed: () => {},
...overrides,
} as Context;
}Best Practices for Testing
1. Reset module cache between tests to test cold/warm start behavior 2. Mock external services (DynamoDB, S3, etc.) to avoid real AWS calls 3. Test both cold and warm starts to verify caching works correctly 4. Use realistic event payloads matching API Gateway format 5. Measure timing for warm invocations to detect performance regressions 6. Test error scenarios including timeouts and service failures
Related skills
How it compares
Use aws-lambda-typescript-integration for TypeScript handler design; pair with an IaC skill for provisioning the Lambda infrastructure itself.
FAQ
What approaches does aws-lambda-typescript-integration support?
NestJS with serverless-express for complex APIs or raw TypeScript handlers for minimal bundle and faster cold starts.
When should I use aws-lambda-typescript-integration?
When creating, deploying, or optimizing TypeScript Lambda functions with API Gateway or ALB integration.
Is aws-lambda-typescript-integration safe to install?
Review the Security Audits panel on this page before installing in production.