Lambda Patterns
Advanced patterns and best practices for building production-ready Lambda functions with sdlcs-aws-cdk-lib.
Overview
This guide covers:
- Lambda function structure and organization
- Environment variable patterns
- Error handling and logging
- Testing strategies
- Performance optimization
- Security best practices
Function Structure
Recommended Directory Layout
lib-src/lambda/
├── package.json # Workspace configuration
├── tsconfig.json # Shared TypeScript config
└── myFunction/ # Individual function
├── package.json # Function-specific dependencies
├── tsconfig.json # Function-specific TS config
├── jest.config.js # Jest configuration
├── .gitignore # Ignore build artifacts
├── src/
│ └── index.ts # Handler implementation
└── test/
└── index.test.ts # Unit tests
Basic Handler Pattern
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
interface FunctionResponse {
message: string;
data?: unknown;
}
export async function handler(
event: APIGatewayProxyEvent,
context: Context
): Promise<APIGatewayProxyResult> {
const sdlc = process.env.SDLC || 'unknown';
const requestId = context.awsRequestId;
console.log('Request received', {
sdlc,
requestId,
path: event.path,
method: event.httpMethod,
});
try {
// Your business logic here
const result = await processRequest(event);
return successResponse(result);
} catch (error) {
console.error('Error processing request', { error, requestId });
return errorResponse(error);
}
}
function successResponse(data: unknown): APIGatewayProxyResult {
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ success: true, data }),
};
}
function errorResponse(error: unknown): APIGatewayProxyResult {
const message = error instanceof Error ? error.message : 'Unknown error';
return {
statusCode: 500,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ success: false, error: message }),
};
}
async function processRequest(event: APIGatewayProxyEvent): Promise<unknown> {
// Your implementation
return { message: 'Processed successfully' };
}
Environment Variables
Standard Environment Variables
The BlueGreenLambda construct automatically sets:
{
SDLC: 'dev', // Current environment
SDLC_CORE: 'dev', // Core environment
AWS_LAMBDA_FUNCTION_VERSION: '$LATEST' // Lambda version
}
Custom Environment Variables
const lambda = new BlueGreenLambda(this, 'MyFunction', {
sdlc,
functionName: 'MyAPI',
entry: './lib-src/lambda/myApi/src/index.ts',
environment: {
// Database
TABLE_NAME: table.tableName,
// API Keys (use Secrets Manager in production)
API_ENDPOINT: 'https://api.example.com',
// Feature flags
ENABLE_FEATURE_X: sdlc === 'prod' ? 'false' : 'true',
// Logging
LOG_LEVEL: sdlc === 'prod' ? 'INFO' : 'DEBUG',
},
});
Type-Safe Environment Variables
// lib-src/lambda/myFunction/src/config.ts
interface Config {
sdlc: string;
tableName: string;
logLevel: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
enableFeatureX: boolean;
}
export function getConfig(): Config {
const missing: string[] = [];
const sdlc = process.env.SDLC;
const tableName = process.env.TABLE_NAME;
const logLevel = process.env.LOG_LEVEL;
const enableFeatureX = process.env.ENABLE_FEATURE_X;
if (!sdlc) missing.push('SDLC');
if (!tableName) missing.push('TABLE_NAME');
if (missing.length > 0) {
throw new Error(`Missing required environment variables: ${missing.join(', ')}`);
}
return {
sdlc: sdlc!,
tableName: tableName!,
logLevel: (logLevel || 'INFO') as Config['logLevel'],
enableFeatureX: enableFeatureX === 'true',
};
}
Error Handling
Structured Error Handling
class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public code?: string
) {
super(message);
this.name = 'AppError';
}
}
export async function handler(
event: APIGatewayProxyEvent,
context: Context
): Promise<APIGatewayProxyResult> {
try {
const result = await processRequest(event);
return {
statusCode: 200,
body: JSON.stringify({ success: true, data: result }),
};
} catch (error) {
if (error instanceof AppError) {
return {
statusCode: error.statusCode,
body: JSON.stringify({
success: false,
error: error.message,
code: error.code,
}),
};
}
// Unexpected error
console.error('Unexpected error', { error, context });
return {
statusCode: 500,
body: JSON.stringify({
success: false,
error: 'Internal server error',
}),
};
}
}
Validation Errors
import { z } from 'zod';
const RequestSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(0).max(120),
});
function validateRequest(body: unknown) {
try {
return RequestSchema.parse(body);
} catch (error) {
if (error instanceof z.ZodError) {
throw new AppError(
'Validation failed',
400,
'VALIDATION_ERROR'
);
}
throw error;
}
}
Logging
Structured Logging
interface LogEntry {
level: 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
message: string;
timestamp: string;
requestId: string;
[key: string]: unknown;
}
class Logger {
constructor(private requestId: string) {}
private log(level: LogEntry['level'], message: string, data?: Record<string, unknown>) {
const entry: LogEntry = {
level,
message,
timestamp: new Date().toISOString(),
requestId: this.requestId,
...data,
};
console.log(JSON.stringify(entry));
}
debug(message: string, data?: Record<string, unknown>) {
this.log('DEBUG', message, data);
}
info(message: string, data?: Record<string, unknown>) {
this.log('INFO', message, data);
}
warn(message: string, data?: Record<string, unknown>) {
this.log('WARN', message, data);
}
error(message: string, error?: unknown, data?: Record<string, unknown>) {
this.log('ERROR', message, {
...data,
error: error instanceof Error ? {
message: error.message,
stack: error.stack,
} : error,
});
}
}
// Usage
export async function handler(
event: APIGatewayProxyEvent,
context: Context
): Promise<APIGatewayProxyResult> {
const logger = new Logger(context.awsRequestId);
logger.info('Request received', {
path: event.path,
method: event.httpMethod,
});
try {
const result = await processRequest(event, logger);
logger.info('Request processed successfully');
return successResponse(result);
} catch (error) {
logger.error('Request failed', error);
return errorResponse(error);
}
}
Testing
Unit Tests
// lib-src/lambda/myFunction/test/index.test.ts
import { handler } from '../src/index';
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
function createMockEvent(overrides?: Partial<APIGatewayProxyEvent>): APIGatewayProxyEvent {
return {
body: null,
headers: {},
multiValueHeaders: {},
httpMethod: 'GET',
isBase64Encoded: false,
path: '/test',
pathParameters: null,
queryStringParameters: null,
multiValueQueryStringParameters: null,
stageVariables: null,
requestContext: {} as any,
resource: '',
...overrides,
};
}
function createMockContext(): Context {
return {
callbackWaitsForEmptyEventLoop: false,
functionName: 'test-function',
functionVersion: '1',
invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456:function:test',
memoryLimitInMB: '128',
awsRequestId: 'test-request-id',
logGroupName: '/aws/lambda/test',
logStreamName: '2024/01/01/[$LATEST]test',
getRemainingTimeInMillis: () => 30000,
done: () => {},
fail: () => {},
succeed: () => {},
};
}
describe('handler', () => {
beforeEach(() => {
process.env.SDLC = 'test';
process.env.TABLE_NAME = 'test-table';
});
it('should return success response', async () => {
const event = createMockEvent();
const context = createMockContext();
const result = await handler(event, context);
expect(result.statusCode).toBe(200);
const body = JSON.parse(result.body);
expect(body.success).toBe(true);
});
it('should handle errors gracefully', async () => {
const event = createMockEvent({ body: 'invalid-json' });
const context = createMockContext();
const result = await handler(event, context);
expect(result.statusCode).toBe(500);
const body = JSON.parse(result.body);
expect(body.success).toBe(false);
});
});
Integration Tests
// Run against deployed Lambda
import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
describe('Integration Tests', () => {
const lambda = new LambdaClient({ region: 'us-east-1' });
const functionName = 'MyAPI-dev';
it('should invoke Lambda successfully', async () => {
const command = new InvokeCommand({
FunctionName: functionName,
Payload: JSON.stringify({
httpMethod: 'GET',
path: '/test',
}),
});
const response = await lambda.send(command);
const payload = JSON.parse(new TextDecoder().decode(response.Payload));
expect(response.StatusCode).toBe(200);
expect(payload.statusCode).toBe(200);
});
});
Performance Optimization
Cold Start Optimization
// Initialize outside handler (reused across invocations)
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client, {
marshallOptions: { removeUndefinedValues: true },
});
export async function handler(event: APIGatewayProxyEvent) {
// Handler code uses initialized docClient
// Client is reused across warm invocations
}
Connection Pooling
// For HTTP clients
import axios from 'axios';
import { Agent } from 'https';
const httpsAgent = new Agent({
keepAlive: true,
maxSockets: 50,
});
const api = axios.create({
httpsAgent,
timeout: 5000,
});
Caching
const cache = new Map<string, { data: unknown; expires: number }>();
function getCached<T>(key: string): T | null {
const cached = cache.get(key);
if (cached && cached.expires > Date.now()) {
return cached.data as T;
}
cache.delete(key);
return null;
}
function setCache(key: string, data: unknown, ttlSeconds: number) {
cache.set(key, {
data,
expires: Date.now() + ttlSeconds * 1000,
});
}
Security Best Practices
Use IAM Roles
// Grant minimal permissions in your stack
blueGreenLambda.lambda.addToRolePolicy(new PolicyStatement({
actions: ['dynamodb:GetItem', 'dynamodb:Query'],
resources: [table.tableArn],
}));
Validate Input
import { z } from 'zod';
const EventSchema = z.object({
body: z.string(),
pathParameters: z.object({
id: z.string().uuid(),
}).nullable(),
});
export async function handler(event: unknown) {
const validatedEvent = EventSchema.parse(event);
// Type-safe event handling
}
Secrets Management
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({});
async function getSecret(secretName: string): Promise<string> {
const command = new GetSecretValueCommand({ SecretId: secretName });
const response = await client.send(command);
return response.SecretString || '';
}
Related
- Blue-Green Deployments - Deploy Lambda functions safely
- Context Strategy - Environment management
- Basic Lambda Example - Complete example