Back to Blog
ServerlessAWS LambdaVercelCloud

Serverless Functions: From Development to Production

Build and deploy serverless functions. From local development to AWS Lambda to Vercel Edge Functions.

B
Bootspring Team
Engineering
July 28, 2023
7 min read

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-esbuild

Best 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.

Share this article

Help spread the word about Bootspring