Getting Started with AWS Lambda
This guide walks you through building a Zelt application on AWS Lambda from scratch.
Prerequisites
Additional requirements for AWS Lambda:
- An AWS account
- AWS CLI or SAM CLI
Installation
pnpm add @zeltjs/core @zeltjs/adapter-lambda
pnpm add -D @types/aws-lambda esbuild
Project Structure
my-app/
├── src/
│ ├── entry/
│ │ ├── controllers/ # HTTP endpoints
│ │ └── commands/ # CLI commands
│ ├── services/ # Business logic
│ ├── configs/ # Configuration classes
│ ├── app.ts # Application definition
│ ├── cli.ts # CLI entry point
│ └── main.ts # HTTP server entry point
├── package.json
└── tsconfig.json
| Directory | Purpose |
|---|---|
entry/ | External entry points (HTTP, CLI) |
services/ | Business logic, injected via DI |
configs/ | Environment variables and settings |
For AWS Lambda, also add template.yaml (SAM template) at the project root.
Hello World
Step 1: Create the Controller
Create src/entry/controllers/hello.controller.ts:
@Controller('/hello')
export class HelloController {
@Get('/:name')
greet(req = request()) {
const name = req.pathParam('name');
return { message: `Hello, ${name}!` };
}
}
Step 2: Create the Application
Create src/app.ts:
export const app = createApp([
http({ controllers: [HelloController] }),
lambdaHandler(),
]);
Step 3: Create the Lambda Handler
Create src/handler.ts:
import { onLambda } from '@zeltjs/adapter-lambda';
import { app } from './app';
const lambdaApp = await onLambda(app);
export const handler = lambdaApp.lambdaHandler.create(lambdaApp.http);
onLambda() injects Lambda environment configs and initializes the runtime. It does not require an HTTP feature and preserves the namespaces of the features you declared.
lambdaHandler() publishes the lambdaHandler namespace by default. lambdaHandler.create(targetHttp) creates an API Gateway v2 function; pass { payloadFormat: '1.0' } to create a v1 function. Export the returned function for AWS to invoke. You can use any HTTP feature name and create separate handlers for multiple HTTP features.
Use lambdaHandler({ name: 'gateway' }) to expose lambdaApp.gateway.create(lambdaApp.http) instead. The usual runtime APIs, including shutdown() and get(Class), remain available.
Step 4: Configure SAM Template
Create template.yaml:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Timeout: 30
Runtime: nodejs20.x
Resources:
HelloFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: dist/
Handler: handler.handler
Events:
Api:
Type: HttpApi
Properties:
Path: /{proxy+}
Method: ANY
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: es2022
EntryPoints:
- src/handler.ts
Outputs:
ApiEndpoint:
Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
Step 5: Deploy
sam build
sam deploy --guided
API Gateway Versions
The adapter supports both API Gateway versions:
HTTP API (v2) — Recommended
export const handler = lambdaApp.lambdaHandler.create(lambdaApp.http);
REST API (v1)
export const handler = lambdaApp.lambdaHandler.create(lambdaApp.http, { payloadFormat: '1.0' });
Warmup Option
By default, onLambda() uses lazy initialization (warmup: false) to minimize cold start time. Controllers are resolved on the first request.
For eager initialization:
const lambdaApp = await onLambda(app, { warmup: true });
| Option | Behavior | Use Case |
|---|---|---|
warmup: false (default) | Controllers resolved on first request | Optimized cold starts |
warmup: true | All controllers resolved at initialization | Provisioned concurrency |
Binary Responses
The adapter automatically handles binary responses (images, audio, video, octet-stream) by encoding them as base64.
@Controller('/files')
export class FileController {
@Get('/image')
getImage() {
const imageBuffer = new Uint8Array([/* ... */]);
return response()
.header('Content-Type', 'image/png')
.body(imageBuffer);
}
}
What's Next?
- Controllers — Route handling and HTTP methods
- Services — Business logic and dependency injection
- Configuration — Environment variables and secrets