Skip to main content

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

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 || '';
}