Fully-Typed HTTP Router for AWS Lambda with Middy and Zod
Build type-safe, validated REST APIs on AWS Lambda without the boilerplate. This monorepo demonstrates a lightweight routing solution that brings the DX of modern web frameworks to serverless functions.
When building REST APIs on AWS Lambda, you're faced with a dilemma:
- Lambda per endpoint? Leads to configuration sprawl and cold start overhead
- Single Lambda monolith? Loses the clarity and type safety of individual handlers
This project solves that by providing a thin, type-safe routing layer that:
- ✅ Keeps your handlers small, focused, and fully typed
- ✅ Automatically validates requests and responses with Zod
- ✅ Infers TypeScript types from your schemas (no manual typing!)
- ✅ Provides consistent error handling and response formatting
- ✅ Integrates seamlessly with Middy middleware ecosystem
Define your route once, get end-to-end types everywhere:
export const getTodo = createRoute({
  method: "GET",
  path: "/todos/{id}",
  schemas: {
    params: z.object({ id: z.string().uuid() }),
    response: z.object({
      id: z.string().uuid(),
      title: z.string(),
      completed: z.boolean(),
    }),
  },
  handler: async (event, ctx) => {
    // event.params.id is typed as string
    // return type must match response schema
    const todo = await ctx.ddb.get(/* ... */);
    return todo; // ✅ Validated against schema
  },
});Zod schemas validate:
- Path parameters (/todos/{id})
- Query strings (?status=completed)
- Request bodies
- Response payloads
Invalid data? Automatic 400 response with detailed errors.
All responses follow a consistent structure:
{
  "success": true,
  "data": { "id": "...", "title": "..." },
  "meta": {
    "timestamp": "2025-10-11T12:00:00.000Z",
    "requestId": "abc-123"
  }
}Use semantic HTTP errors that are automatically caught and formatted:
if (!todo) {
  throw new NotFoundError("Todo not found");
}
// → 404 response with proper error structureThis is a Turborepo monorepo with:
lambda-http-router/
├── packages/
│   └── http-router/          # 📦 Reusable router package
│       ├── create-route.ts   # Route factory with validation
│       ├── route-parser.ts   # Request/response middleware
│       ├── error-handler.ts  # Standardized error handling
│       └── types.ts          # Core TypeScript types
│
└── api/                      # 🎬 Demo API (Todos CRUD)
    ├── lambda/               # Lambda handler entry point
    ├── routes/               # Route definitions
    ├── models/               # Data models
    └── cdk/                  # Infrastructure (optional)
The @repo/http-router package is framework-agnostic and can be extracted to any Lambda project.
- Node.js 18+
- npm 10+
# Clone the repository
git clone https://github.com/silviuglv/lambda-http-router.git
cd lambda-http-router
# Install dependencies
npm installnpm test# Synthesize CloudFormation template
npx cdk synth
# Deploy the stack
npx cdk deployCreate a new file in api/src/routes/:
import { createRoute } from "@repo/http-router";
import { z } from "zod";
export const createItem = createRoute({
  method: "POST",
  path: "/items",
  schemas: {
    body: z.object({
      name: z.string().min(1),
      price: z.number().positive(),
    }),
    response: z.object({
      id: z.string(),
      name: z.string(),
      price: z.number(),
    }),
  },
  handler: async (event, ctx) => {
    // event.body is typed and validated
    const item = await ctx.ddb.put({
      id: crypto.randomUUID(),
      ...event.body,
    });
    return item;
  },
});Add it to api/src/routes/index.ts:
import { defineRoutes } from "@repo/http-router";
import { createItem } from "./create-item";
export const routes = defineRoutes(
  createItem,
  // ... other routes
);Your route is now:
- ✅ Fully typed from request to response
- ✅ Validated against Zod schemas
- ✅ Integrated with your Lambda handler
- ✅ Protected by error handling middleware
Inject dependencies (database clients, config, etc.) into all handlers:
// In api/src/lambda/execute-request.ts
export const context = {
  ddb: dynamoDbClient,
  env: { tableName: process.env.TABLE_NAME },
};
// Register the context type
declare module "@repo/http-router" {
  interface Register {
    context: typeof context;
  }
}
// Apply via middleware
export const handler = middy()
  .use(httpContext(context))  // ← Injects context
  .handler(httpRouterHandler({ routes }));Now ctx is typed and available in all route handlers.
The project includes comprehensive tests for:
- Route creation and validation
- Request/response parsing
- Error handling
- Schema validation failures
# Run all tests
npm test
# Test specific workspace
cd packages/http-router && npm test
cd api && npm test
# Lint
npm run lintFor detailed architecture, examples, and best practices, see:
This is a demonstration project, but contributions are welcome!
- Fork the repository
- Create a feature branch
- Make your changes
- Run tests (npm test)
- Submit a pull request
MIT License - see LICENSE file for details
Silviu Glavan
- Website: silviu.dev
- GitHub: @silviuglv
If this project helped you build better Lambda APIs, give it a star on GitHub!