Serverless functions let you run code without managing servers. Pay only for execution time, scale automatically, and focus on business logic.
AWS Lambda Basics#
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 processData(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('Error:', error);
19
20 return {
21 statusCode: 500,
22 body: JSON.stringify({ error: 'Internal server error' }),
23 };
24 }
25};
26
27// Typed handler helper
28function createHandler<T, R>(
29 fn: (input: T, event: APIGatewayProxyEvent) => Promise<R>
30): APIGatewayProxyHandler {
31 return async (event) => {
32 try {
33 const input = JSON.parse(event.body || '{}') as T;
34 const result = await fn(input, event);
35
36 return {
37 statusCode: 200,
38 headers: { 'Content-Type': 'application/json' },
39 body: JSON.stringify(result),
40 };
41 } catch (error) {
42 if (error instanceof ValidationError) {
43 return {
44 statusCode: 400,
45 body: JSON.stringify({ error: error.message }),
46 };
47 }
48
49 console.error('Unhandled error:', error);
50 return {
51 statusCode: 500,
52 body: JSON.stringify({ error: 'Internal server error' }),
53 };
54 }
55 };
56}
57
58// Usage
59interface CreateUserInput {
60 email: string;
61 name: string;
62}
63
64export const createUser = createHandler<CreateUserInput, User>(async (input) => {
65 const validated = createUserSchema.parse(input);
66 return userService.create(validated);
67});Vercel Serverless Functions#
1// api/users/[id].ts
2import type { VercelRequest, VercelResponse } from '@vercel/node';
3
4export default async function handler(
5 req: VercelRequest,
6 res: VercelResponse
7) {
8 const { id } = req.query;
9
10 switch (req.method) {
11 case 'GET':
12 const user = await getUser(id as string);
13 return res.json(user);
14
15 case 'PUT':
16 const updated = await updateUser(id as string, req.body);
17 return res.json(updated);
18
19 case 'DELETE':
20 await deleteUser(id as string);
21 return res.status(204).end();
22
23 default:
24 res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
25 return res.status(405).end(`Method ${req.method} Not Allowed`);
26 }
27}Vercel Edge Functions#
1// app/api/geo/route.ts
2import { NextRequest } from 'next/server';
3
4export const runtime = 'edge';
5
6export async function GET(request: NextRequest) {
7 const country = request.geo?.country || 'Unknown';
8 const city = request.geo?.city || 'Unknown';
9
10 return Response.json({
11 country,
12 city,
13 message: `Hello from ${city}, ${country}!`,
14 });
15}
16
17// Edge function with streaming
18export async function POST(request: NextRequest) {
19 const body = await request.json();
20
21 const stream = new ReadableStream({
22 async start(controller) {
23 for await (const chunk of generateResponse(body.prompt)) {
24 controller.enqueue(new TextEncoder().encode(chunk));
25 }
26 controller.close();
27 },
28 });
29
30 return new Response(stream, {
31 headers: {
32 'Content-Type': 'text/event-stream',
33 'Cache-Control': 'no-cache',
34 },
35 });
36}Cold Start Optimization#
1// Initialize outside handler for connection reuse
2import { PrismaClient } from '@prisma/client';
3
4// Reused across invocations
5const prisma = new PrismaClient();
6
7export const handler = async (event: APIGatewayProxyEvent) => {
8 // Connection already established
9 const users = await prisma.user.findMany();
10 return { statusCode: 200, body: JSON.stringify(users) };
11};
12
13// Lazy initialization
14let dbConnection: Database | null = null;
15
16async function getDb(): Promise<Database> {
17 if (!dbConnection) {
18 dbConnection = await createConnection();
19 }
20 return dbConnection;
21}
22
23// Provisioned concurrency for critical functions
24// serverless.yml
25/*
26functions:
27 api:
28 handler: handler.main
29 provisionedConcurrency: 5
30*/Environment and Secrets#
1// Use environment variables
2const config = {
3 dbUrl: process.env.DATABASE_URL!,
4 apiKey: process.env.API_KEY!,
5 environment: process.env.NODE_ENV || 'development',
6};
7
8// Validate at startup
9function validateEnv() {
10 const required = ['DATABASE_URL', 'API_KEY', 'JWT_SECRET'];
11
12 for (const key of required) {
13 if (!process.env[key]) {
14 throw new Error(`Missing required environment variable: ${key}`);
15 }
16 }
17}
18
19// AWS Secrets Manager for sensitive data
20import { SecretsManager } from '@aws-sdk/client-secrets-manager';
21
22const secretsManager = new SecretsManager({});
23let cachedSecrets: Record<string, string> | null = null;
24
25async function getSecrets(): Promise<Record<string, string>> {
26 if (cachedSecrets) return cachedSecrets;
27
28 const response = await secretsManager.getSecretValue({
29 SecretId: process.env.SECRET_ID,
30 });
31
32 cachedSecrets = JSON.parse(response.SecretString!);
33 return cachedSecrets;
34}Request Validation#
1import { z } from 'zod';
2
3const createOrderSchema = z.object({
4 customerId: z.string().uuid(),
5 items: z.array(
6 z.object({
7 productId: z.string(),
8 quantity: z.number().min(1),
9 })
10 ).min(1),
11 shippingAddress: z.object({
12 street: z.string(),
13 city: z.string(),
14 country: z.string(),
15 postalCode: z.string(),
16 }),
17});
18
19export const createOrder = createHandler(async (event) => {
20 const body = JSON.parse(event.body || '{}');
21
22 // Validate input
23 const result = createOrderSchema.safeParse(body);
24
25 if (!result.success) {
26 return {
27 statusCode: 400,
28 body: JSON.stringify({
29 error: 'Validation failed',
30 details: result.error.flatten(),
31 }),
32 };
33 }
34
35 const order = await orderService.create(result.data);
36
37 return {
38 statusCode: 201,
39 body: JSON.stringify(order),
40 };
41});Error Handling#
1class AppError extends Error {
2 constructor(
3 message: string,
4 public statusCode: number = 500,
5 public code?: string
6 ) {
7 super(message);
8 this.name = 'AppError';
9 }
10}
11
12class NotFoundError extends AppError {
13 constructor(resource: string) {
14 super(`${resource} not found`, 404, 'NOT_FOUND');
15 }
16}
17
18class ValidationError extends AppError {
19 constructor(message: string) {
20 super(message, 400, 'VALIDATION_ERROR');
21 }
22}
23
24// Error handling wrapper
25function withErrorHandling(
26 handler: (event: APIGatewayProxyEvent) => Promise<any>
27): APIGatewayProxyHandler {
28 return async (event) => {
29 try {
30 const result = await handler(event);
31 return {
32 statusCode: 200,
33 body: JSON.stringify(result),
34 };
35 } catch (error) {
36 console.error('Error:', error);
37
38 if (error instanceof AppError) {
39 return {
40 statusCode: error.statusCode,
41 body: JSON.stringify({
42 error: error.message,
43 code: error.code,
44 }),
45 };
46 }
47
48 return {
49 statusCode: 500,
50 body: JSON.stringify({
51 error: 'Internal server error',
52 code: 'INTERNAL_ERROR',
53 }),
54 };
55 }
56 };
57}Middleware Pattern#
1type Middleware = (
2 handler: APIGatewayProxyHandler
3) => APIGatewayProxyHandler;
4
5// CORS middleware
6const cors: Middleware = (handler) => async (event, context, callback) => {
7 const response = await handler(event, context, callback);
8
9 if (response && typeof response === 'object') {
10 return {
11 ...response,
12 headers: {
13 ...response.headers,
14 'Access-Control-Allow-Origin': '*',
15 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
16 'Access-Control-Allow-Headers': 'Content-Type,Authorization',
17 },
18 };
19 }
20
21 return response;
22};
23
24// Auth middleware
25const requireAuth: Middleware = (handler) => async (event, context, callback) => {
26 const token = event.headers.Authorization?.replace('Bearer ', '');
27
28 if (!token) {
29 return {
30 statusCode: 401,
31 body: JSON.stringify({ error: 'Unauthorized' }),
32 };
33 }
34
35 try {
36 const user = await verifyToken(token);
37 (event as any).user = user;
38 return handler(event, context, callback);
39 } catch {
40 return {
41 statusCode: 401,
42 body: JSON.stringify({ error: 'Invalid token' }),
43 };
44 }
45};
46
47// Compose middleware
48function compose(...middlewares: Middleware[]): Middleware {
49 return (handler) =>
50 middlewares.reduceRight((h, middleware) => middleware(h), handler);
51}
52
53// Usage
54export const handler = compose(cors, requireAuth)(async (event) => {
55 const user = (event as any).user;
56 return { message: `Hello ${user.name}` };
57});Testing Serverless Functions#
1import { handler } from './handler';
2
3describe('createUser handler', () => {
4 it('creates user with valid input', async () => {
5 const event = {
6 body: JSON.stringify({
7 email: 'test@example.com',
8 name: 'Test User',
9 }),
10 headers: { 'Content-Type': 'application/json' },
11 } as APIGatewayProxyEvent;
12
13 const response = await handler(event, {} as any, () => {});
14
15 expect(response.statusCode).toBe(201);
16 const body = JSON.parse(response.body);
17 expect(body.email).toBe('test@example.com');
18 });
19
20 it('returns 400 for invalid input', async () => {
21 const event = {
22 body: JSON.stringify({ email: 'invalid' }),
23 headers: {},
24 } as APIGatewayProxyEvent;
25
26 const response = await handler(event, {} as any, () => {});
27
28 expect(response.statusCode).toBe(400);
29 });
30});Deployment#
1# serverless.yml
2service: my-api
3
4provider:
5 name: aws
6 runtime: nodejs18.x
7 region: us-east-1
8 environment:
9 DATABASE_URL: ${ssm:/my-app/database-url}
10
11functions:
12 api:
13 handler: dist/handler.main
14 events:
15 - httpApi:
16 path: /{proxy+}
17 method: any
18 memorySize: 512
19 timeout: 10
20
21plugins:
22 - serverless-esbuildBest Practices#
Performance:
✓ Initialize outside handler
✓ Use connection pooling
✓ Keep functions small
✓ Set appropriate timeouts
Security:
✓ Validate all inputs
✓ Use secrets manager
✓ Implement proper auth
✓ Set least-privilege IAM
Cost:
✓ Optimize memory settings
✓ Use provisioned concurrency wisely
✓ Monitor execution duration
✓ Clean up resources
Conclusion#
Serverless functions simplify deployment and scaling. Focus on cold start optimization, proper error handling, and security. Test locally with tools like SAM or serverless-offline before deploying.