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 structure
This 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 install
npm test
# Synthesize CloudFormation template
npx cdk synth
# Deploy the stack
npx cdk deploy
Create 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 lint
For 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!