Skip to content

utsabpanta/lambda-expressway

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

lambda-expressway

🚀 The fast lane to building Express-style APIs on AWS Lambda - Type-safe REST APIs with zero dependencies.

License: MIT TypeScript Node.js

Why lambda-expressway?

Building REST APIs on AWS Lambda usually means writing messy routing code with nested if/else statements. Lambda-expressway brings the elegance of Express.js to serverless, with production-grade features built-in.

Before lambda-expressway:

export const handler = async (event: APIGatewayProxyEvent) => {
  if (event.path === '/users' && event.httpMethod === 'GET') {
    // handle GET users
  } else if (event.path.match(/^\/users\/\w+$/) && event.httpMethod === 'GET') {
    // manually extract ID, validate, handle errors...
  } else if (event.path === '/users' && event.httpMethod === 'POST') {
    // manually parse body, validate, handle errors...
  }
  // ... dozens more routes
  return { statusCode: 404, body: 'Not found' };
};

With lambda-expressway:

const router = new Router({ cors: true, logging: true });

router.get('/users', getUsers);
router.get('/users/:id', getUserById);
router.post('/users', {
  body: z.object({ name: z.string(), email: z.string().email() }),
  handler: createUser
});

export const handler = router.handler();

Key Benefits

  • 🎯 Zero Dependencies - No bloat. Only optional Zod for validation
  • ⚡ Performance - <5ms overhead, optimized for Lambda cold starts
  • 🔒 Type-Safe - Full TypeScript support with automatic type inference from schemas
  • 🛡️ Production-Ready - Built-in CORS, rate limiting, logging, and error handling
  • 🚀 Developer Experience - Express-like API you already know
  • 🔌 Flexible - Works with your existing custom or Winston/Pino logger, no lock-in

What Makes This Different?

vs. Other Lambda Routers:

  • TypeScript-First: Built from the ground up with TypeScript, not JavaScript with types added later
  • Zod Integration: Automatic request validation with full type inference
  • Express Syntax: Uses :id path params (not {id}), making migration from Express seamless
  • Production Features: Rate limiting, custom loggers, route groups - all built-in
  • Zero Dependencies: Core has no dependencies; Zod is an optional peer dependency

Installation

This package is not published to npm. You can install it directly from GitHub:

Option 1: Install from GitHub (Recommended)

npm install github:utsabpanta/lambda-expressway zod

Or with yarn:

yarn add github:utsabpanta/lambda-expressway zod

Or with pnpm:

pnpm add github:utsabpanta/lambda-expressway zod

Option 2: Install from Specific Branch/Tag/Commit

# From main branch
npm install github:utsabpanta/lambda-expressway#main

# From a specific tag
npm install github:utsabpanta/lambda-expressway#v1.0.0

# From a specific commit
npm install github:utsabpanta/lambda-expressway#abc1234

Option 3: Use in package.json

Add this to your package.json:

{
  "dependencies": {
    "lambda-expressway": "github:utsabpanta/lambda-expressway",
    "zod": "^3.22.0"
  }
}

Then run npm install.

Option 4: Install from Git URL

npm install git+https://github.com/utsabpanta/lambda-expressway.git

Option 5: Local Development

For local development and testing:

# Clone the repository
git clone https://github.com/utsabpanta/lambda-expressway.git
cd lambda-expressway

# Install dependencies and build
npm install
npm run build

# Link for local development
npm link

# In your project directory
npm link lambda-expressway

Note: When installing from GitHub, npm will automatically run the build step if you have a prepare script in package.json.

Quick Start

import { Router, z } from 'lambda-expressway';

const router = new Router({
  cors: true,
  logging: { level: 'info' },
  rateLimit: { max: 100, window: 60000 },
});

// Simple GET
router.get('/health', async () => ({ status: 'ok' }));

// Path parameters
router.get('/users/:id', async (req) => {
  return { user: await getUser(req.params.id) };
});

// With validation
router.post('/users', {
  body: z.object({
    name: z.string().min(1),
    email: z.string().email(),
  }),
  handler: async (req) => {
    // req.body is typed and validated automatically
    return { user: await createUser(req.body) };
  },
});

export const handler = router.handler();

Core Features

Request Validation with Zod

Validate request body, params, and query with automatic TypeScript inference:

router.post('/users/:id', {
  params: z.object({ id: z.string().uuid() }),
  body: z.object({
    name: z.string().min(1).max(100),
    email: z.string().email(),
    age: z.number().min(18).optional(),
  }),
  query: z.object({
    notify: z.enum(['true', 'false']).optional(),
  }),
  handler: async (req) => {
    // All validated and typed!
    const { id } = req.params;           // string (UUID)
    const { name, email, age } = req.body; // all properly typed
    const { notify } = req.query;         // 'true' | 'false' | undefined
  },
});

Invalid requests automatically return 400 with validation details.

Middleware

// Custom authentication middleware (use a proper JWT library!)
import jwt from 'jsonwebtoken';

const auth = async (req, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) throw new UnauthorizedError('Missing token');

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET!);
    await next();
  } catch (err) {
    throw new UnauthorizedError('Invalid token');
  }
};

// Apply to specific routes
router.get('/protected', auth, async (req) => {
  return { user: req.user };
});

// Or globally
router.use(auth);

// Built-in middleware
import { requestId, timeout, bodyLimit } from 'lambda-expressway';
router.get('/test', requestId(), timeout(5000), handler);

Error Handling

Pre-built error classes with proper HTTP status codes:

import { NotFoundError, UnauthorizedError, ValidationError } from 'lambda-expressway';

router.get('/users/:id', async (req) => {
  const user = await getUser(req.params.id);
  if (!user) throw new NotFoundError('User not found');
  return { user };
});

// Errors are automatically caught and formatted
// { "error": "User not found" } with statusCode: 404

CORS

Simple or advanced CORS configuration:

// Simple
const router = new Router({ cors: true });

// Advanced
const router = new Router({
  cors: {
    origin: ['https://example.com', 'https://app.example.com'],
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    credentials: true,
    maxAge: 86400,
  },
});

Rate Limiting

Protect your API with in-memory rate limiting:

const router = new Router({
  rateLimit: {
    max: 100,              // 100 requests
    window: 60000,         // per 60 seconds
    keyGenerator: (req) => req.user?.id || req.event.requestContext?.identity?.sourceIp,
  },
});

Returns 429 when exceeded, with headers:

  • X-RateLimit-Limit
  • X-RateLimit-Remaining
  • X-RateLimit-Reset

Custom Logging

Use your own logger (Winston, Pino, AWS Powertools, etc.):

import winston from 'winston';

const logger = winston.createLogger({ /* config */ });

const router = new Router({
  logger: {
    info: (msg, meta) => logger.info(msg, meta),
    warn: (msg, meta) => logger.warn(msg, meta),
    error: (msg, err, meta) => logger.error(msg, { ...meta, error: err }),
    debug: (msg, meta) => logger.debug(msg, meta),
  },
});

Or use the built-in logger:

const router = new Router({
  logging: {
    level: 'info', // 'none' | 'error' | 'warn' | 'info' | 'debug'
    logRequest: true,
    logResponse: true,
    logErrors: true,
  },
});

Route Groups

Organize routes with prefixes:

const api = router.group('/api/v1');
api.get('/users', getUsers);       // /api/v1/users
api.post('/users', createUser);     // /api/v1/users

const admin = api.group('/admin');
admin.get('/settings', getSettings); // /api/v1/admin/settings

Complete Example

import { Router, z, UnauthorizedError, NotFoundError } from 'lambda-expressway';
import jwt from 'jsonwebtoken';

const router = new Router({
  cors: true,
  logging: { level: 'info' },
  rateLimit: { max: 100, window: 60000 },
});

// Auth middleware with proper JWT verification
const auth = async (req, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) throw new UnauthorizedError('Missing token');

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET!);
    await next();
  } catch (err) {
    throw new UnauthorizedError('Invalid token');
  }
};

// Public routes
router.get('/health', async () => ({ status: 'ok' }));

// Protected routes
const api = router.group('/api/v1');

api.get('/users', auth, async () => {
  return { users: await getUsers() };
});

api.get('/users/:id', {
  middleware: [auth],
  params: z.object({ id: z.string().uuid() }),
  handler: async (req) => {
    const user = await getUser(req.params.id);
    if (!user) throw new NotFoundError();
    return { user };
  },
});

api.post('/users', {
  middleware: [auth],
  body: z.object({
    name: z.string().min(1).max(100),
    email: z.string().email(),
  }),
  handler: async (req) => {
    const user = await createUser(req.body);
    return { user };
  },
});

export const handler = router.handler();

Response Helpers

import {
  createSuccessResponse,
  createCreatedResponse,
  createNoContentResponse,
  createBinaryResponse,
  createRedirectResponse,
  createHtmlResponse,
  addHeaders,
  addCacheHeaders,
} from 'lambda-expressway';

// Standard responses
return createSuccessResponse({ data: 'value' });
return createCreatedResponse({ id: '123' }, '/users/123');
return createNoContentResponse();

// Binary/special responses
return createBinaryResponse(buffer, 'image/png');
return createRedirectResponse('https://example.com', 301);
return createHtmlResponse('<html>...</html>');

// With headers/cache
let response = createSuccessResponse({ data });
response = addHeaders(response, { 'X-Custom': 'value' });
response = addCacheHeaders(response, 3600); // 1 hour

TypeScript Support

Full type inference from Zod schemas:

const createUserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

router.post('/users', {
  body: createUserSchema,
  handler: async (req) => {
    req.body.name;  // ✓ TypeScript knows this is string
    req.body.age;   // ✗ TypeScript error: Property 'age' does not exist
  },
});

AWS Lambda Integration

Serverless Framework

1. Create your Lambda handler with lambda-expressway:

// src/handler.ts
import { Router, z } from 'lambda-expressway';

const router = new Router({
  cors: true,
  logging: { level: 'info' },
});

router.get('/users', async () => {
  return { users: [] };
});

router.post('/users', {
  body: z.object({
    name: z.string(),
    email: z.string().email(),
  }),
  handler: async (req) => {
    return { user: req.body };
  },
});

export const handler = router.handler();

2. Configure serverless.yml:

# serverless.yml
service: my-api

provider:
  name: aws
  runtime: nodejs18.x
  stage: ${opt:stage, 'dev'}
  region: us-east-1

functions:
  api:
    handler: dist/handler.handler
    events:
      - httpApi:
          path: /{proxy+}
          method: any

plugins:
  - serverless-esbuild  # For TypeScript bundling

3. Deploy:

npm run build
serverless deploy

AWS SAM

1. Create your Lambda handler with lambda-expressway:

// src/app.ts
import { Router } from 'lambda-expressway';

const router = new Router({ cors: true });

router.get('/health', async () => ({ status: 'ok' }));
router.get('/users/:id', async (req) => ({
  user: { id: req.params.id }
}));

export const lambdaHandler = router.handler();

2. Configure SAM template:

# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Timeout: 30
    Runtime: nodejs18.x

Resources:
  ApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: dist/
      Handler: app.lambdaHandler
      Events:
        ApiProxy:
          Type: HttpApi
          Properties:
            Path: /{proxy+}
            Method: ANY
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: "es2020"
        EntryPoints:
          - app.ts

Outputs:
  ApiUrl:
    Description: "API Gateway endpoint URL"
    Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/"

3. Build and deploy:

sam build
sam deploy --guided

AWS CDK

1. Create your Lambda handler with lambda-expressway:

// lambda/handler.ts
import { Router, z } from 'lambda-expressway';

const router = new Router({
  cors: { origin: ['https://yourdomain.com'] },
  rateLimit: { max: 100, window: 60000 },
});

router.get('/api/users', async () => ({ users: [] }));
router.post('/api/users', {
  body: z.object({ name: z.string(), email: z.string().email() }),
  handler: async (req) => ({ user: req.body }),
});

export const handler = router.handler();

2. Create CDK stack:

// lib/api-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as apigateway from 'aws-cdk-lib/aws-apigatewayv2';
import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

export class ApiStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Lambda function with lambda-expressway
    const apiFunction = new NodejsFunction(this, 'ApiFunction', {
      entry: 'lambda/handler.ts',
      handler: 'handler',
      runtime: lambda.Runtime.NODEJS_18_X,
      bundling: {
        externalModules: [],
        minify: true,
      },
      environment: {
        NODE_ENV: 'production',
      },
    });

    // HTTP API Gateway
    const httpApi = new apigateway.HttpApi(this, 'HttpApi', {
      defaultIntegration: new integrations.HttpLambdaIntegration(
        'ApiIntegration',
        apiFunction
      ),
    });

    new cdk.CfnOutput(this, 'ApiUrl', {
      value: httpApi.url ?? 'No URL',
      description: 'API Gateway URL',
    });
  }
}

3. Deploy:

npm run build
cdk deploy

Key Points for All Frameworks:

  • ✅ Use /{proxy+} path to route all requests to your Lambda
  • ✅ Set method: any to handle all HTTP methods
  • ✅ Lambda-expressway handles all routing internally
  • ✅ Build TypeScript before deploying (npm run build or use bundlers)
  • ✅ The handler exports a single function that processes all routes

Security Best Practices

Authentication

Lambda-expressway provides the middleware pattern but does not include authentication middleware. You should implement authentication using established, secure libraries:

JWT Authentication:

import jwt from 'jsonwebtoken';
import { UnauthorizedError } from 'lambda-expressway';

const jwtAuth = async (req, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) throw new UnauthorizedError('Missing token');

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET!);
    await next();
  } catch (err) {
    throw new UnauthorizedError('Invalid token');
  }
};

router.get('/protected', jwtAuth, handler);

AWS Cognito:

import { CognitoJwtVerifier } from 'aws-jwt-verify';

const verifier = CognitoJwtVerifier.create({
  userPoolId: process.env.USER_POOL_ID!,
  tokenUse: 'access',
  clientId: process.env.CLIENT_ID!,
});

const cognitoAuth = async (req, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) throw new UnauthorizedError();

  try {
    req.user = await verifier.verify(token);
    await next();
  } catch (err) {
    throw new UnauthorizedError('Invalid token');
  }
};

API Key Authentication:

const apiKeyAuth = async (req, next) => {
  const apiKey = req.headers['x-api-key'];
  if (!apiKey || !isValidApiKey(apiKey)) {
    throw new UnauthorizedError('Invalid API key');
  }
  await next();
};

Input Validation

Always validate user input with Zod schemas to prevent injection attacks:

router.post('/users', {
  body: z.object({
    name: z.string().min(1).max(100),
    email: z.string().email(),
    age: z.number().int().min(0).max(150),
  }),
  handler: async (req) => {
    // req.body is validated and safe to use
  },
});

CORS Configuration

Configure CORS properly for production:

const router = new Router({
  cors: {
    origin: ['https://yourdomain.com'],
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    credentials: true,
    maxAge: 86400,
  },
});

Rate Limiting

Protect your API from abuse:

const router = new Router({
  rateLimit: {
    max: 100,
    window: 60000,
    keyGenerator: (req) => req.user?.id || req.event.requestContext?.identity?.sourceIp,
  },
});

API Reference

RouterOptions

interface RouterOptions {
  cors?: boolean | CorsOptions;
  rateLimit?: RateLimitOptions;
  logging?: boolean | LoggingOptions;
  logger?: LoggerInterface;      // Custom logger (Winston, Pino, etc.)
  responseTransform?: (data: any, req: RouterRequest) => any;
  apiKey?: ApiKeyOptions;
}

RouterRequest

interface RouterRequest {
  event: APIGatewayProxyEvent;   // Original Lambda event
  context?: Context;              // Lambda context
  params: Record<string, string>; // Path parameters
  query: Record<string, string>;  // Query parameters
  headers: Record<string, string>; // Normalized headers
  body: any;                      // Parsed body
  path: string;                   // Request path
  method: string;                 // HTTP method
  user?: any;                     // Set by auth middleware
  meta: Record<string, any>;      // Custom metadata
}

Error Classes

HttpError(statusCode, message, details?)    // Base class
ValidationError(message, details?)          // 400
UnauthorizedError(message?)                 // 401
ForbiddenError(message?)                    // 403
NotFoundError(message?)                     // 404
ConflictError(message?)                     // 409
TooManyRequestsError(message?, retryAfter?) // 429
InternalServerError(message?)               // 500

Examples

See the examples/ directory:

  • basic.ts - Simple routing and validation
  • advanced.ts - Full-featured application with auth, validation, CORS, rate limiting

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT © Utsab Pant

Support

If you find this package helpful, please star it on GitHub!

For issues or questions, please file an issue on the GitHub repository.

About

The fast lane to building Express-style APIs on AWS Lambda with TypeScript, Zod validation, and zero dependencies

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors