or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

appsync-events.mdappsync-graphql.mdbedrock-agent.mdhttp-middleware.mdhttp-routing.mdindex.md
tile.json

http-middleware.mddocs/

HTTP Middleware

Composable middleware for CORS, compression, and custom request/response processing.

CORS Middleware

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

  • Handles OPTIONS preflight: Returns 200 with appropriate headers
  • Adds CORS headers to responses based on Origin header
  • origin: '*' allows all; string/array matches exactly

Compression Middleware

Compresses 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):

  • Response not already encoded (no Content-Encoding)
  • Response not chunked (no Transfer-Encoding: chunked)
  • Request method is not HEAD
  • Response size >= threshold
  • Cache-Control doesn't contain 'no-transform'
  • Response has body
  • Client supports compression (Accept-Encoding header)

Encoding Selection: Prefers configured encoding, falls back to any supported by client

Compose Middleware

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: After

Middleware Type

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

Custom Middleware Patterns

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

Middleware Execution Order

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 after

Router 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