Skip to content

silviuglv/lambda-http-router

Repository files navigation

🚀 Lambda HTTP Router

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.

TypeScript License: MIT


✨ Why This Exists

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

Read the full blog post →


🎯 Key Features

Type Safety From Schema to Handler

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

Automatic Validation

Zod schemas validate:

  • Path parameters (/todos/{id})
  • Query strings (?status=completed)
  • Request bodies
  • Response payloads

Invalid data? Automatic 400 response with detailed errors.

Standardized Response Envelope

All responses follow a consistent structure:

{
  "success": true,
  "data": { "id": "...", "title": "..." },
  "meta": {
    "timestamp": "2025-10-11T12:00:00.000Z",
    "requestId": "abc-123"
  }
}

Built-in Error Handling

Use semantic HTTP errors that are automatically caught and formatted:

if (!todo) {
  throw new NotFoundError("Todo not found");
}
// → 404 response with proper error structure

📦 What's Inside

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.


🚀 Quick Start

Prerequisites

  • Node.js 18+
  • npm 10+

Installation

# Clone the repository
git clone https://github.com/silviuglv/lambda-http-router.git
cd lambda-http-router

# Install dependencies
npm install

Run Tests

npm test

Deploy to AWS

# Synthesize CloudFormation template
npx cdk synth

# Deploy the stack
npx cdk deploy

🛠️ Creating Routes

1. Define Your Route

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

2. Register the Route

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

3. That's It!

Your route is now:

  • ✅ Fully typed from request to response
  • ✅ Validated against Zod schemas
  • ✅ Integrated with your Lambda handler
  • ✅ Protected by error handling middleware

🎨 Custom Context

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.


🧪 Testing

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

📚 Full Documentation

For detailed architecture, examples, and best practices, see:

  • CLAUDE.md - Comprehensive project documentation
  • Blog Post - Design rationale and walkthrough

🤝 Contributing

This is a demonstration project, but contributions are welcome!

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Run tests (npm test)
  5. Submit a pull request

📄 License

MIT License - see LICENSE file for details


👨‍💻 Author

Silviu Glavan


⭐ Show Your Support

If this project helped you build better Lambda APIs, give it a star on GitHub!

Read the companion blog post →

About

Fully-Typed HTTP Router for AWS Lambda with Middy and Zod

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published