AWS Lambda
toServerlessHandler does and how events map to Request / Response, read the Serverless module overview.Deploy your Vercube application to AWS Lambda with full support for API Gateway v1 and v2, automatic binary content handling, and seamless cookie management.
Basic Setup
import { createApp } from '@vercube/core';
import { toServerlessHandler } from '@vercube/serverless/aws-lambda';
const app = createApp();
// Your controllers are automatically registered
export const handler = toServerlessHandler(app);
Supported API Gateway Versions
The adapter automatically detects and supports both API Gateway versions:
API Gateway v1 (REST API)
Works with APIGatewayProxyEvent:
// API Gateway v1 Event Format
{
httpMethod: 'GET',
path: '/api/users',
headers: {
'content-type': 'application/json',
'authorization': 'Bearer token...'
},
body: '{"name":"John"}',
queryStringParameters: {
page: '1',
limit: '10'
},
pathParameters: {
id: '123'
}
}
Key Features:
- Traditional REST API structure
- Simple header handling
- Cookie handling via
Set-Cookieheader - Binary content with
isBase64Encodedflag
API Gateway v2 (HTTP API)
Works with APIGatewayProxyEventV2:
// API Gateway v2 Event Format
{
requestContext: {
http: {
method: 'GET',
path: '/api/users'
}
},
headers: {
'content-type': 'application/json',
'authorization': 'Bearer token...'
},
body: '{"name":"John"}',
queryStringParameters: {
page: '1',
limit: '10'
},
pathParameters: {
id: '123'
}
}
Key Features:
- Improved performance and lower cost
- Enhanced cookie handling with cookies array
- Streamlined event structure
- Better WebSocket support
Binary Content Handling
Binary responses are automatically encoded as base64:
@Controller('/files')
export class FileController {
@Inject(FileService)
private fileService!: FileService;
@Get('/download/:id')
async downloadFile(@Param('id') id: string) {
const fileBuffer = await this.fileService.getFile(id);
return new Response(fileBuffer, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="file-${id}.pdf"`
}
});
}
@Get('/image/:id')
async getImage(@Param('id') id: string) {
const imageBuffer = await this.fileService.getImage(id);
return new Response(imageBuffer, {
headers: {
'Content-Type': 'image/png'
}
});
}
}
The adapter automatically:
- Detects binary content types
- Encodes the body as base64
- Sets
isBase64Encoded: truein the response - API Gateway decodes it for the client
Configure Binary Media Types
Tell API Gateway which content types should be treated as binary:
functions:
api:
handler: lambda.handler
events:
- http:
path: /{proxy+}
method: ANY
binaryMediaTypes:
- 'image/*'
- 'application/pdf'
- 'application/zip'
- 'application/octet-stream'
Cookie Handling
Cookies work seamlessly across both API Gateway versions:
@Controller('/auth')
export class AuthController {
@Inject(AuthService)
private authService!: AuthService;
@Post('/login')
async login(@Body({ validationSchema: LoginSchema }) credentials: LoginDto) {
const token = await this.authService.generateToken(credentials);
// Set authentication cookie
return FastResponse.ok({ success: true })
.cookie('auth_token', token, {
httpOnly: true,
secure: true,
maxAge: 3600,
sameSite: 'strict'
});
}
@Post('/logout')
async logout() {
// Clear authentication cookie
return FastResponse.ok({ success: true })
.cookie('auth_token', '', {
httpOnly: true,
secure: true,
maxAge: 0
});
}
@Get('/session')
async getSession(@Cookie('auth_token') token: string) {
if (!token) {
throw new UnauthorizedException('Not authenticated');
}
const session = await this.authService.validateToken(token);
return { session };
}
}
How it works:
- API Gateway v1: Cookies set via
Set-Cookieheader - API Gateway v2: Cookies set via
cookiesarray for better handling
Environment Variables
Access Lambda-specific environment variables:
import { RuntimeConfig } from '@vercube/core';
@Controller('/config')
export class ConfigController {
@Get('/lambda-info')
getLambdaInfo() {
return {
// AWS Lambda environment variables
region: process.env.AWS_REGION,
functionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
functionVersion: process.env.AWS_LAMBDA_FUNCTION_VERSION,
memoryLimit: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE,
logGroup: process.env.AWS_LAMBDA_LOG_GROUP_NAME,
logStream: process.env.AWS_LAMBDA_LOG_STREAM_NAME
};
}
@Get('/runtime-info')
getRuntimeInfo() {
return {
// Runtime execution details
requestId: process.env.AWS_REQUEST_ID,
executionEnv: process.env.AWS_EXECUTION_ENV,
runtime: process.env.AWS_LAMBDA_RUNTIME_API
};
}
}
Serverless Framework Configuration
Basic Configuration
service: vercube-api
provider:
name: aws
runtime: nodejs22.x
region: us-east-1
memorySize: 512
timeout: 30
# Environment variables
environment:
NODE_ENV: production
DATABASE_URL: ${env:DATABASE_URL}
JWT_SECRET: ${env:JWT_SECRET}
functions:
api:
handler: lambda.handler
events:
- http:
path: /{proxy+}
method: ANY
cors: true
Advanced Configuration
service: vercube-api
provider:
name: aws
runtime: nodejs22.x
region: us-east-1
# Performance settings
memorySize: 1024
timeout: 60
# VPC configuration (for database access)
vpc:
securityGroupIds:
- sg-xxxxxxxxx
subnetIds:
- subnet-xxxxxxxxx
- subnet-yyyyyyyyy
# IAM permissions
iam:
role:
statements:
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
Resource: 'arn:aws:s3:::my-bucket/*'
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
Resource: 'arn:aws:dynamodb:${aws:region}:*:table/my-table'
# Environment variables
environment:
NODE_ENV: ${opt:stage, 'dev'}
DATABASE_URL: ${env:DATABASE_URL}
REDIS_URL: ${env:REDIS_URL}
JWT_SECRET: ${env:JWT_SECRET}
S3_BUCKET: ${env:S3_BUCKET}
functions:
api:
handler: lambda.handler
# Provisioned concurrency for predictable performance
provisionedConcurrency: 2
# Reserved concurrent executions
reservedConcurrency: 100
events:
- http:
path: /{proxy+}
method: ANY
cors:
origin: '*'
headers:
- Content-Type
- Authorization
- X-Api-Key
allowCredentials: true
# Binary media types
binaryMediaTypes:
- 'image/*'
- 'application/pdf'
- 'application/zip'
# Layer for dependencies
layers:
- arn:aws:lambda:us-east-1:xxxxx:layer:my-dependencies:1
# Tags
tags:
Environment: ${opt:stage, 'dev'}
Service: vercube-api
# Plugins
plugins:
- serverless-offline
- serverless-plugin-typescript
# Custom configuration
custom:
serverless-offline:
httpPort: 3000
Cold Start Optimization
Minimize cold start times for better performance:
Module-Level Initialization
import { createApp } from '@vercube/core';
import { toServerlessHandler } from '@vercube/serverless/aws-lambda';
import { DatabaseService } from './services/Database';
// Initialize app at module level (happens once per container)
const app = createApp({
setup: async (app) => {
// Bind services
app.container.bind(DatabaseService);
// Initialize database connection (reused across invocations)
const db = app.container.get(DatabaseService);
await db.connect();
}
});
// Export handler (execution is fast)
export const handler = toServerlessHandler(app);
Lazy Loading Heavy Dependencies
export class HeavyServiceFactory {
private static instance: HeavyService | null = null;
static async getInstance() {
if (!this.instance) {
// Only import when needed
const { HeavyService } = await import('./HeavyService');
this.instance = new HeavyService();
}
return this.instance;
}
}
@Controller('/heavy')
export class HeavyController {
@Get('/process')
async process() {
// Load heavy service only when this endpoint is called
const service = await HeavyServiceFactory.getInstance();
return await service.process();
}
}
Provisioned Concurrency
Keep functions warm to eliminate cold starts:
functions:
api:
handler: lambda.handler
provisionedConcurrency: 5 # Keep 5 instances always warm
# Or use auto-scaling
provisionedConcurrency:
minCapacity: 2
maxCapacity: 10
targetUtilizationPercent: 0.75
Database Connections
Handle database connections efficiently in Lambda:
Connection Pooling
export class DatabaseService {
private static pool: Pool | null = null;
async getPool() {
// Reuse connection pool across invocations
if (DatabaseService.pool) {
return DatabaseService.pool;
}
DatabaseService.pool = new Pool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
// Lambda-optimized settings
max: 1, // Single connection per Lambda instance
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000
});
return DatabaseService.pool;
}
async query(sql: string, params?: any[]) {
const pool = await this.getPool();
return await pool.query(sql, params);
}
}
RDS Proxy
Use RDS Proxy to manage database connections:
provider:
environment:
DB_HOST: my-rds-proxy.proxy-xxxxxxxxx.us-east-1.rds.amazonaws.com
DB_PORT: 5432
iam:
role:
statements:
- Effect: Allow
Action:
- rds-db:connect
Resource: 'arn:aws:rds-db:us-east-1:xxxxx:dbuser:prx-xxxxx/*'
Advanced Response Handling
Custom Headers
@Controller('/api')
export class ApiController {
@Get('/cached-data')
getCachedData() {
return FastResponse.ok({ data: 'cached' })
.header('Cache-Control', 'public, max-age=3600')
.header('X-Custom-Header', 'custom-value');
}
@Get('/streaming-data')
getStreamingData() {
const stream = this.createDataStream();
return new Response(stream, {
headers: {
'Content-Type': 'application/json',
'Transfer-Encoding': 'chunked'
}
});
}
}
Error Handling
import { NotFoundException, BadRequestException, InternalServerErrorException } from '@vercube/core';
@Controller('/users')
export class UserController {
@Get('/:id')
async getUser(@Param('id') id: string) {
try {
const user = await this.userService.findById(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
} catch (error) {
if (error instanceof NotFoundException) {
throw error;
}
// Log unexpected errors
console.error('Error fetching user:', error);
throw new InternalServerErrorException('Failed to fetch user');
}
}
}
// Automatically returns proper AWS response:
// {
// statusCode: 404,
// headers: { 'Content-Type': 'application/json' },
// body: '{"statusCode":404,"message":"User with ID ... not found"}'
// }
Deployment
Deploy to AWS
# Deploy to default stage (dev)
serverless deploy
# Deploy to production
serverless deploy --stage prod
# Deploy specific function
serverless deploy function -f api
# Deploy with verbose output
serverless deploy --verbose
Environment-Specific Deployments
service: vercube-api
provider:
name: aws
runtime: nodejs22.x
region: ${opt:region, 'us-east-1'}
stage: ${opt:stage, 'dev'}
environment:
NODE_ENV: ${self:provider.stage}
DATABASE_URL: ${env:DATABASE_URL_${self:provider.stage}}
functions:
api:
handler: lambda.handler
# Deploy to development
serverless deploy --stage dev
# Deploy to production
serverless deploy --stage prod --region us-west-2
Monitoring and Debugging
CloudWatch Metrics
Monitor your Lambda function performance:
functions:
api:
handler: lambda.handler
# Enable detailed CloudWatch metrics
tracing:
lambda: true
apiGateway: true
Key Metrics to Monitor:
- Invocations - Number of times function is invoked
- Duration - Execution time per invocation
- Errors - Number of failed invocations
- Throttles - Number of throttled invocations
- ConcurrentExecutions - Number of concurrent invocations
- IteratorAge - For stream-based invocations
X-Ray Tracing
Enable AWS X-Ray for detailed tracing:
provider:
tracing:
lambda: true
apiGateway: true
functions:
api:
handler: lambda.handler
import AWSXRay from 'aws-xray-sdk-core';
import AWS from 'aws-sdk';
// Wrap AWS SDK
const XAWS = AWSXRay.captureAWS(AWS);
@Controller('/traced')
export class TracedController {
@Get('/data')
async getData() {
// This will appear in X-Ray traces
const segment = AWSXRay.getSegment();
const subsegment = segment.addNewSubsegment('custom-operation');
try {
const data = await this.processData();
subsegment.close();
return data;
} catch (error) {
subsegment.addError(error);
subsegment.close();
throw error;
}
}
}
Troubleshooting
Common Issues
Handler not found
Error: Cannot find module 'lambda'
Solution: Ensure handler path matches your file structure:
functions:
api:
handler: lambda.handler # <filename>.<export name>
Request timeout
Task timed out after 30.00 seconds
Solution: Increase timeout:
functions:
api:
timeout: 60
Binary content corrupted
Response body appears corrupted or truncated
Solution: Configure binary media types:
functions:
api:
events:
- http:
binaryMediaTypes:
- 'image/*'
- 'application/pdf'
Cold start too slow
Duration: 3000ms (Cold Start: 2500ms)
Solutions:
- Reduce deployment package size
- Use Lambda layers for dependencies
- Enable provisioned concurrency
- Lazy load heavy modules
Memory limit exceeded
Process exited before completing request
Solution: Increase memory:
functions:
api:
memorySize: 1024 # or higher