A TypeScript framework for building structured, maintainable AWS Lambda functions with decorators, dependency injection, and strong typing.
Features
- Declarative Programming: Use decorators to define handlers, inject dependencies, and manage request/response flows
- Dependency Injection: Built-in DI container for managing services and dependencies
- Request Validation: Automatic request body and parameter validation using class-validator
- Middleware Support: Create and chain middleware for cross-cutting concerns
- Type Safety: Full TypeScript support with strong typing throughout
- Framework Agnostic: Works with AWS CDK, Serverless Framework, or any other deployment tool
- HTTP Utilities: Built-in request/response handling for API Gateway events
- Error Handling: Structured error handling with built-in HTTP error classes
Installation
npm install lambda-forge reflect-metadata class-validator class-transformer
Make sure to enable decorator metadata in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Quick Start
1. Create a Lambda Factory
import { LambdaForge } from 'lambda-forge' import { MyService } from './services/my.service' export const forge = new LambdaForge({ services: [MyService] })
2. Define a Service
import { Service } from 'lambda-forge' @Service() export class MyService { async getData(id: string) { // Implementation return { id, data: 'some data' } } }
3. Create a Handler
import { Lambda, Inject, Param } from 'lambda-forge' import { MyService } from './my.service' @Lambda() class GetDataHandler { constructor(@Inject(MyService) private myService: MyService) {} async main(@Param('id') id: string) { return await this.myService.getData(id) } } export const handler = forge.createHttpHandler(GetDataHandler)
Core Concepts
Decorators
Handler Decorators
@Lambda(): Marks a class as a Lambda handler@Returns(statusCode, type, options): Specifies response type and status code
Parameter Decorators
@Body(dto)
Extracts and validates the request body against a DTO class.
import { IsString, IsEmail } from 'class-validator' class CreateUserDTO { @IsString() name: string; @IsEmail() email: string; } @Lambda() class CreateUserHandler { async main(@Body(CreateUserDTO) user: CreateUserDTO) { // user is validated and typed return { id: '123', ...user }; } }
@Param(name)
Extracts path parameters from the request URL. Can include optional transform functions.
@Lambda() class GetUserHandler { async main(@Param('id', [parseInt]) id: number) { // id is automatically converted to number return { id, name: 'John Doe' }; } }
@Query()
Extracts query parameters from the request URL.
interface UserFilter { role?: string; status?: 'active' | 'inactive'; } @Lambda() class ListUsersHandler { async main(@Query() filter: UserFilter) { // filter contains typed query parameters // e.g., /users?role=admin&status=active return this.userService.findUsers(filter); } }
@Event()
Injects the raw Lambda event object.
@Lambda() class RawEventHandler { async main(@Event() event: APIGatewayProxyEvent) { // Access the raw event object console.log('Request Headers:', event.headers); return { received: true }; } }
@Req()
Injects the Request object for accessing request properties.
@Lambda() class RequestHandler { async main(@Req() req: Request) { // Access request properties const authHeader = req.headers['authorization']; const customContext = req.context; return { authorized: true }; } }
@Res()
Injects the Response object for modifying the response directly.
@Lambda() class ResponseHandler { async main(@Res() res: Response) { // Modify response properties res.setHeader('Cache-Control', 'max-age=3600'); res.status(201); return { created: true }; } }
Service Decorators
@Service(): Marks a class as an injectable service@Inject(token): Injects a dependency
Request Validation
Use class-validator decorators to validate request bodies:
import { IsString, IsNumber } from 'class-validator' export class CreateUserDTO { @IsString() name: string @IsNumber() age: number } @Lambda() class CreateUserHandler { async main(@Body(CreateUserDTO) user: CreateUserDTO) { // user is validated and typed } }
Middleware
Create middleware to handle cross-cutting concerns:
import { Middleware, ForgeMiddleware, Request, Response } from 'lambda-forge' @Middleware export class AuthMiddleware implements ForgeMiddleware { async use(req: Request, res: Response, next: (error?: any) => void) { // Validate authentication const token = req.headers['authorization'] if (!token) { throw new UnauthorizedError('Missing token') } next() } } // Apply middleware to handlers @Lambda() class SecureHandler { @UseMiddlewares([AuthMiddleware]) async main() { // Only runs if middleware passes } }
Error Handling
Built-in error classes for common HTTP scenarios:
import { NotFoundError, BadRequestError } from 'lambda-forge' @Lambda() class GetUserHandler { async main(@Param('id') id: string) { const user = await this.userService.findById(id) if (!user) { throw new NotFoundError('User not found') } return user } }
Dependency Injection and Service Initialization
Services can inject other services and use @OnExecutionStart for one-time initialization:
@Service({ singleton: true }) class DatabaseService { private connection: any; @OnExecutionStart() async initialize() { // This will run once when the service is first used this.connection = await createDatabaseConnection(); console.log('Database connection initialized'); } async query(sql: string) { return this.connection.query(sql); } } @Service() class UserService { constructor( @Inject(DatabaseService) private db: DatabaseService, @Inject(AuthService) private auth: AuthService ) {} }
Framework Integration
lambda-forge is framework-agnostic. Meaning you can use your functions as you would in any IAC tools that support Typescript lambda functions as long as your function exports a createHttpHandler or createHandler.
AWS CDK Example
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs' const handler = new lambda.NodejsFunction(this, 'MyHandler', { entry: 'path/to/handler.ts', handler: 'handler' })
Serverless Framework Example
functions: myHandler: handler: src/handlers/my.handler events: - http: path: /resource method: get
Best Practices
- Service Organization: Group related functionality into services
- DTO Validation: Always validate incoming request bodies using DTOs
- Error Handling: Use built-in error classes for consistent error responses
- Middleware: Use middleware for cross-cutting concerns like authentication
- Testing: Take advantage of dependency injection for easy unit testing
Testing
Lambda Forge makes testing straightforward:
describe('MyHandler', () => { let handler: MyHandler let mockService: jest.Mocked<MyService> beforeEach(() => { mockService = { getData: jest.fn() } handler = new MyHandler(mockService) }) it('should return data', async () => { mockService.getData.mockResolvedValue({ id: '123', data: 'test' }) const result = await handler.main('123') expect(result).toEqual({ id: '123', data: 'test' }) }) })
Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
License
This project is licensed under the MIT License - see the LICENSE file for details.