Lambda functions need production patterns different from traditional servers. Here's how to build reliable, performant Lambda applications.
Basic Handler Pattern#
1import { APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda';
2
3export const handler: APIGatewayProxyHandler = async (event) => {
4 try {
5 const body = JSON.parse(event.body || '{}');
6
7 const result = await processRequest(body);
8
9 return {
10 statusCode: 200,
11 headers: {
12 'Content-Type': 'application/json',
13 'Access-Control-Allow-Origin': '*',
14 },
15 body: JSON.stringify(result),
16 };
17 } catch (error) {
18 console.error('Handler error:', error);
19
20 return {
21 statusCode: 500,
22 body: JSON.stringify({ error: 'Internal server error' }),
23 };
24 }
25};
26
27// Typed handler factory
28function createHandler<T, R>(
29 handler: (body: T, event: APIGatewayProxyEvent) => Promise<R>,
30 options: { statusCode?: number } = {}
31): APIGatewayProxyHandler {
32 return async (event) => {
33 try {
34 const body = JSON.parse(event.body || '{}') as T;
35 const result = await handler(body, event);
36
37 return {
38 statusCode: options.statusCode || 200,
39 headers: { 'Content-Type': 'application/json' },
40 body: JSON.stringify(result),
41 };
42 } catch (error) {
43 return handleError(error);
44 }
45 };
46}Cold Start Optimization#
1// Initialize outside handler - reused across invocations
2import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
4
5// Connection reuse
6const client = new DynamoDBClient({});
7const docClient = DynamoDBDocumentClient.from(client);
8
9// Lazy initialization
10let dbConnection: DatabaseConnection | null = null;
11
12async function getDbConnection(): Promise<DatabaseConnection> {
13 if (!dbConnection) {
14 dbConnection = await createConnection({
15 host: process.env.DB_HOST,
16 // ... config
17 });
18 }
19 return dbConnection;
20}
21
22export const handler: APIGatewayProxyHandler = async (event) => {
23 const db = await getDbConnection();
24 // Use db...
25};
26
27// Provisioned concurrency for critical functions
28// In serverless.yml:
29// functions:
30// api:
31// handler: handler.main
32// provisionedConcurrency: 5Error Handling#
1// Custom error classes
2class AppError extends Error {
3 constructor(
4 message: string,
5 public statusCode: number,
6 public code: string
7 ) {
8 super(message);
9 this.name = 'AppError';
10 }
11}
12
13class ValidationError extends AppError {
14 constructor(message: string, public details?: any) {
15 super(message, 400, 'VALIDATION_ERROR');
16 }
17}
18
19class NotFoundError extends AppError {
20 constructor(resource: string) {
21 super(`${resource} not found`, 404, 'NOT_FOUND');
22 }
23}
24
25// Error handler
26function handleError(error: unknown): APIGatewayProxyResult {
27 console.error('Error:', error);
28
29 if (error instanceof AppError) {
30 return {
31 statusCode: error.statusCode,
32 body: JSON.stringify({
33 error: error.message,
34 code: error.code,
35 }),
36 };
37 }
38
39 // Don't expose internal errors
40 return {
41 statusCode: 500,
42 body: JSON.stringify({
43 error: 'Internal server error',
44 code: 'INTERNAL_ERROR',
45 }),
46 };
47}
48
49// Middleware pattern
50const withErrorHandling = (
51 handler: APIGatewayProxyHandler
52): APIGatewayProxyHandler => {
53 return async (event, context) => {
54 try {
55 return await handler(event, context, () => {});
56 } catch (error) {
57 return handleError(error);
58 }
59 };
60};Request Validation#
1import { z } from 'zod';
2
3const createUserSchema = z.object({
4 email: z.string().email(),
5 name: z.string().min(1).max(100),
6 age: z.number().min(18).optional(),
7});
8
9function validate<T>(schema: z.ZodSchema<T>, data: unknown): T {
10 const result = schema.safeParse(data);
11
12 if (!result.success) {
13 throw new ValidationError('Validation failed', result.error.flatten());
14 }
15
16 return result.data;
17}
18
19export const createUser = createHandler(async (body) => {
20 const data = validate(createUserSchema, body);
21 return userService.create(data);
22}, { statusCode: 201 });Middleware Chain#
1type Middleware = (
2 handler: APIGatewayProxyHandler
3) => APIGatewayProxyHandler;
4
5// Auth middleware
6const withAuth: Middleware = (handler) => async (event, context, callback) => {
7 const token = event.headers.Authorization?.replace('Bearer ', '');
8
9 if (!token) {
10 return {
11 statusCode: 401,
12 body: JSON.stringify({ error: 'Unauthorized' }),
13 };
14 }
15
16 try {
17 const user = await verifyToken(token);
18 (event as any).user = user;
19 return handler(event, context, callback);
20 } catch {
21 return {
22 statusCode: 401,
23 body: JSON.stringify({ error: 'Invalid token' }),
24 };
25 }
26};
27
28// Logging middleware
29const withLogging: Middleware = (handler) => async (event, context, callback) => {
30 const start = Date.now();
31 const requestId = context.awsRequestId;
32
33 console.log({
34 type: 'REQUEST',
35 requestId,
36 path: event.path,
37 method: event.httpMethod,
38 });
39
40 const result = await handler(event, context, callback);
41
42 console.log({
43 type: 'RESPONSE',
44 requestId,
45 statusCode: result.statusCode,
46 duration: Date.now() - start,
47 });
48
49 return result;
50};
51
52// Compose middlewares
53const compose = (...middlewares: Middleware[]): Middleware => {
54 return (handler) =>
55 middlewares.reduceRight((h, middleware) => middleware(h), handler);
56};
57
58// Usage
59const enhance = compose(withErrorHandling, withLogging, withAuth);
60
61export const handler = enhance(async (event) => {
62 const user = (event as any).user;
63 return {
64 statusCode: 200,
65 body: JSON.stringify({ message: `Hello ${user.name}` }),
66 };
67});Async Processing#
1import { SQSHandler, SQSEvent } from 'aws-lambda';
2
3// SQS handler with batch processing
4export const sqsHandler: SQSHandler = async (event: SQSEvent) => {
5 const results = await Promise.allSettled(
6 event.Records.map(async (record) => {
7 const body = JSON.parse(record.body);
8 await processMessage(body);
9 })
10 );
11
12 // Report failures for retry
13 const failures = results
14 .map((result, index) => ({ result, record: event.Records[index] }))
15 .filter(({ result }) => result.status === 'rejected')
16 .map(({ record }) => ({
17 itemIdentifier: record.messageId,
18 }));
19
20 return {
21 batchItemFailures: failures,
22 };
23};
24
25// Invoke async function
26import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda';
27
28const lambda = new LambdaClient({});
29
30async function invokeAsync(functionName: string, payload: any): Promise<void> {
31 await lambda.send(
32 new InvokeCommand({
33 FunctionName: functionName,
34 InvocationType: 'Event', // Async
35 Payload: JSON.stringify(payload),
36 })
37 );
38}Observability#
1// Structured logging
2const logger = {
3 info: (message: string, data?: object) => {
4 console.log(JSON.stringify({ level: 'INFO', message, ...data }));
5 },
6 error: (message: string, error: Error, data?: object) => {
7 console.error(
8 JSON.stringify({
9 level: 'ERROR',
10 message,
11 error: error.message,
12 stack: error.stack,
13 ...data,
14 })
15 );
16 },
17};
18
19// X-Ray tracing
20import AWSXRay from 'aws-xray-sdk-core';
21import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
22
23const client = AWSXRay.captureAWSv3Client(new DynamoDBClient({}));
24
25// Custom metrics
26import { MetricUnits, Metrics } from '@aws-lambda-powertools/metrics';
27
28const metrics = new Metrics({ namespace: 'MyApp' });
29
30export const handler = async (event) => {
31 metrics.addMetric('RequestCount', MetricUnits.Count, 1);
32
33 try {
34 const result = await processRequest(event);
35 metrics.addMetric('SuccessCount', MetricUnits.Count, 1);
36 return result;
37 } catch (error) {
38 metrics.addMetric('ErrorCount', MetricUnits.Count, 1);
39 throw error;
40 } finally {
41 metrics.publishStoredMetrics();
42 }
43};Configuration#
1// Use Parameter Store or Secrets Manager
2import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
3
4const ssm = new SSMClient({});
5const configCache: Record<string, string> = {};
6
7async function getConfig(name: string): Promise<string> {
8 if (configCache[name]) {
9 return configCache[name];
10 }
11
12 const result = await ssm.send(
13 new GetParameterCommand({
14 Name: name,
15 WithDecryption: true,
16 })
17 );
18
19 const value = result.Parameter?.Value || '';
20 configCache[name] = value;
21 return value;
22}
23
24// Environment-specific config
25const config = {
26 tableName: process.env.TABLE_NAME!,
27 region: process.env.AWS_REGION!,
28 stage: process.env.STAGE || 'dev',
29};Best Practices#
Cold Starts:
✓ Initialize outside handler
✓ Use provisioned concurrency for critical paths
✓ Keep deployment packages small
✓ Avoid VPC when not needed
Reliability:
✓ Implement proper error handling
✓ Use dead letter queues
✓ Set appropriate timeouts
✓ Implement retries with backoff
Observability:
✓ Structured logging
✓ Custom metrics
✓ Distributed tracing
✓ Alerting on errors
Security:
✓ Least privilege IAM
✓ Encrypt environment variables
✓ Validate all inputs
✓ Use secrets manager
Conclusion#
Production Lambda requires patterns for cold starts, error handling, and observability. Initialize connections outside handlers, implement proper middleware chains, and use structured logging. Monitor cold start times and consider provisioned concurrency for latency-sensitive functions.