Composable middleware for CORS, compression, and custom request/response processing.
Adds CORS headers and handles OPTIONS preflight requests.
cors(options?: CorsOptions): Middleware;
interface CorsOptions {
origin?: string | string[]; // default: '*'
allowMethods?: string[]; // default: ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']
allowHeaders?: string[]; // default: ['Authorization', 'Content-Type', 'X-Amz-Date', 'X-Api-Key', 'X-Amz-Security-Token']
exposeHeaders?: string[]; // default: []
credentials?: boolean; // default: false
maxAge?: number; // seconds
}Usage
import { Router } from '@aws-lambda-powertools/event-handler/http';
import { cors } from '@aws-lambda-powertools/event-handler/http/middleware';
const router = new Router();
router.use(cors()); // Allow all origins
router.use(cors({ origin: 'https://example.com', credentials: true, maxAge: 3600 }));
router.use(cors({ origin: ['https://app.example.com', 'https://admin.example.com'] }));
router.use(cors({ exposeHeaders: ['X-Request-Id', 'X-Total-Count'] }));Behavior
origin: '*' allows all; string/array matches exactlyCompresses responses with gzip or deflate when size exceeds threshold and client supports it.
compress(options?: CompressionOptions): Middleware;
interface CompressionOptions {
encoding?: 'gzip' | 'deflate'; // default: 'gzip'
threshold?: number; // bytes, default: 1024
}Usage
import { compress } from '@aws-lambda-powertools/event-handler/http/middleware';
router.use(compress()); // 1KB threshold, gzip
router.use(compress({ threshold: 2048, encoding: 'deflate' }));
router.get('/large-data', [compress()], async () => ({ data: await getLargeData() }));Compression Conditions (all must be true):
Encoding Selection: Prefers configured encoding, falls back to any supported by client
Combines multiple middleware into one with onion model execution.
composeMiddleware(middleware: Middleware[]): Middleware;Usage
import { composeMiddleware, cors, compress } from '@aws-lambda-powertools/event-handler/http/middleware';
const standardMiddleware = composeMiddleware([
cors({ origin: 'https://example.com' }),
compress({ threshold: 1024 }),
async ({ reqCtx, next }) => {
console.log(`${reqCtx.req.method} ${reqCtx.req.url}`);
await next();
},
]);
router.use(standardMiddleware);
router.get('/api/data', [standardMiddleware], async () => ({ data: 'value' }));Execution Order (Onion model):
const m1 = async ({ reqCtx, next }) => { console.log('1: Before'); await next(); console.log('1: After'); };
const m2 = async ({ reqCtx, next }) => { console.log('2: Before'); await next(); console.log('2: After'); };
const composed = composeMiddleware([m1, m2]);
// Output: 1: Before → 2: Before → Handler → 2: After → 1: Aftertype Middleware = (args: { reqCtx: RequestContext; next: NextFunction }) => Promise<HandlerResponse | void>;
type NextFunction = () => Promise<HandlerResponse>;
interface RequestContext {
req: Request;
event: APIGatewayProxyEvent | APIGatewayProxyEventV2 | ALBEvent;
context: Context;
res: Response;
params: Record<string, string>;
responseType: ResponseType;
isBase64Encoded?: boolean;
}Authentication
const requireAuth: Middleware = async ({ reqCtx, next }) => {
const authHeader = reqCtx.req.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
throw new UnauthorizedError('Missing authorization');
}
const token = authHeader.substring(7);
const user = await validateToken(token);
if (!user) throw new UnauthorizedError('Invalid token');
reqCtx.context.user = user;
return next();
};Validation
const validateBody = (schema) => async ({ reqCtx, next }) => {
try {
const body = await reqCtx.req.json();
reqCtx.validatedBody = await schema.validate(body);
return next();
} catch (error) {
throw new BadRequestError('Invalid body', {}, { errors: error.errors });
}
};Rate Limiting
const rateLimit = ({ maxRequests, windowMs }) => {
const requests = new Map();
return async ({ reqCtx, next }) => {
const ip = reqCtx.event.requestContext.identity?.sourceIp || 'unknown';
const now = Date.now();
let timestamps = (requests.get(ip) || []).filter(ts => ts > now - windowMs);
if (timestamps.length >= maxRequests) {
return new Response(JSON.stringify({ error: 'Too many requests' }), {
status: 429,
headers: { 'Retry-After': String(Math.ceil(windowMs / 1000)) }
});
}
timestamps.push(now);
requests.set(ip, timestamps);
return next();
};
};Response Headers
const addResponseHeaders = (headers: Record<string, string>) => async ({ reqCtx, next }) => {
const response = await next();
if (response instanceof Response) {
Object.entries(headers).forEach(([key, value]) => response.headers.set(key, value));
}
return response;
};
router.use(addResponseHeaders({
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Strict-Transport-Security': 'max-age=31536000'
}));Logging
const requestLogger: Middleware = async ({ reqCtx, next }) => {
const start = Date.now();
const { method, url } = reqCtx.req;
console.log(`[${method}] ${url} - Started`);
try {
const response = await next();
const status = response instanceof Response ? response.status : 200;
console.log(`[${method}] ${url} - ${status} (${Date.now() - start}ms)`);
return response;
} catch (error) {
console.error(`[${method}] ${url} - Error (${Date.now() - start}ms)`, error);
throw error;
}
};Global vs Route-Specific
router.use(cors()); // Global: all routes
router.use(compress()); // Global: all routes
router.get('/api/protected', [requireAuth, requireAdmin], handler);
// Execution: cors → compress → requireAuth → requireAdmin → handler → reverse order afterRouter Composition
const apiRouter = new Router();
apiRouter.use(requireAuth);
const publicRouter = new Router();
publicRouter.use(cors());
const mainRouter = new Router();
mainRouter.use(compress());
mainRouter.includeRouter(apiRouter, { prefix: '/api' });
mainRouter.includeRouter(publicRouter, { prefix: '/public' });
// /api/data: compress → requireAuth → handler
// /public/info: compress → cors → handler