gs-bun-aws-lambda
Bun AWS Lambda (Clean Architecture)
Handler Pattern - Thin Adapter
Lambda handlers are thin adapters that:
- Initialize DI Container (cold start)
- Parse input
- Resolve Use Case from DI
- Execute and return result
- Map domain exceptions to HTTP status
// src/functions/create-category.ts
import type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda'
import { DIContainer, TOKENS, initializeDI } from '@/backend/di'
import type { CreateCategoryUseCase } from '@/backend/application/category/use-cases'
import { DomainException, NotFoundException } from '@/backend/domain/shared/exceptions'
let initialized = false
export async function handler(
event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2> {
if (!initialized) {
await initializeDI()
initialized = true
}
try {
const input = JSON.parse(event.body ?? '{}')
const userId = event.requestContext.authorizer?.jwt?.claims?.sub
const useCase = DIContainer.resolve<CreateCategoryUseCase>(
TOKENS.CreateCategoryUseCase
)
const result = await useCase.execute({ ...input, userId })
return response(201, result)
} catch (error) {
return handleError(error)
}
}
function response(status: number, data: unknown): APIGatewayProxyResultV2 {
return {
statusCode: status,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
}
}
function handleError(error: unknown): APIGatewayProxyResultV2 {
if (error instanceof NotFoundException) {
return response(404, { error: error.message, code: error.code })
}
if (error instanceof DomainException) {
return response(400, { error: error.message, code: error.code })
}
console.error('Unhandled:', error)
return response(500, { error: 'Internal server error' })
}
Event Source Types
├── HTTP API (API Gateway v2) → APIGatewayProxyEventV2
├── REST API (API Gateway v1) → APIGatewayProxyEvent
├── SQS → SQSEvent
├── SNS → SNSEvent
├── EventBridge → EventBridgeEvent<T>
├── S3 → S3Event
└── DynamoDB Streams → DynamoDBStreamEvent
Deployment: Container Image
Dockerfile
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
COPY src/ ./src/
RUN bun build src/handler.ts --outdir=dist --target=bun --minify
FROM public.ecr.aws/lambda/provided:al2023
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:${PATH}"
WORKDIR ${LAMBDA_TASK_ROOT}
COPY /app/dist/ ./
COPY bootstrap ${LAMBDA_RUNTIME_DIR}/bootstrap
RUN chmod +x ${LAMBDA_RUNTIME_DIR}/bootstrap
CMD ["handler.handler"]
Bootstrap
// bootstrap.ts
const RUNTIME_API = `http://${Bun.env.AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime`
const [moduleName, functionName] = (Bun.env._HANDLER ?? 'handler.handler').split('.')
const handlerModule = await import(`./${moduleName}.js`)
const handler = handlerModule[functionName]
while (true) {
const next = await fetch(`${RUNTIME_API}/invocation/next`)
const requestId = next.headers.get('Lambda-Runtime-Aws-Request-Id')!
const event = await next.json()
try {
const result = await handler(event, { awsRequestId: requestId })
await fetch(`${RUNTIME_API}/invocation/${requestId}/response`, {
method: 'POST',
body: JSON.stringify(result),
})
} catch (error) {
await fetch(`${RUNTIME_API}/invocation/${requestId}/error`, {
method: 'POST',
body: JSON.stringify({ errorMessage: String(error) }),
})
}
}
DI Initialization
// src/backend/di/initialize.ts
import { DIContainer, TOKENS } from './container'
import { CategoryRepositoryImpl } from '@/backend/infrastructure/category/repositories'
import { CreateCategoryUseCase, GetCategoryUseCase } from '@/backend/application/category/use-cases'
export async function initializeDI() {
const table = await getTable() // OneTable instance
// Register repositories
DIContainer.register(TOKENS.CategoryRepository, {
useFactory: () => new CategoryRepositoryImpl(table),
})
// Register use cases
DIContainer.register(TOKENS.CreateCategoryUseCase, {
useFactory: () => new CreateCategoryUseCase(
DIContainer.resolve(TOKENS.CategoryRepository)
),
})
DIContainer.register(TOKENS.GetCategoryUseCase, {
useFactory: () => new GetCategoryUseCase(
DIContainer.resolve(TOKENS.CategoryRepository)
),
})
}
Cold Start Optimization
- Lazy DI init - Initialize container on first request only
- Bundle with Bun - Single file, tree-shaken
- AWS SDK v3 - Modular imports
- Minimal deps - Use native fetch, Bun APIs
// Lazy repository initialization
let repository: ICategoryRepository | null = null
function getRepository(): ICategoryRepository {
if (!repository) {
repository = DIContainer.resolve(TOKENS.CategoryRepository)
}
return repository
}
SQS Handler Example
// src/functions/process-queue.ts
import type { SQSEvent, SQSBatchResponse } from 'aws-lambda'
import { DIContainer, TOKENS, initializeDI } from '@/backend/di'
import type { ProcessMessageUseCase } from '@/backend/application/messaging/use-cases'
let initialized = false
export async function handler(event: SQSEvent): Promise<SQSBatchResponse> {
if (!initialized) {
await initializeDI()
initialized = true
}
const useCase = DIContainer.resolve<ProcessMessageUseCase>(
TOKENS.ProcessMessageUseCase
)
const failures: SQSBatchResponse['batchItemFailures'] = []
for (const record of event.Records) {
try {
const message = JSON.parse(record.body)
await useCase.execute(message)
} catch (error) {
console.error(`Failed record ${record.messageId}:`, error)
failures.push({ itemIdentifier: record.messageId })
}
}
return { batchItemFailures: failures }
}
Anti-Patterns
| Anti-Pattern | Correct Approach |
|---|---|
| Business logic in handler | Use Case classes |
| Direct DB access in handler | Repository via DI |
new Repository() in handler |
DI Container resolution |
| Generic error responses | Map domain exceptions |
References
- Feature Architecture:
skills/feature-architecture/SKILL.md - SST Infrastructure:
skills/sst-infra/SKILL.md
More from gilbertopsantosjr/fullstacknextjs
gs-feature-architecture
Guide for implementing features in Clean Architecture OOP with Next.js. Use when planning new features, understanding the 4-layer structure (Domain, Application, Infrastructure, Presentation), or deciding where code should live.
3sst-infra
Guide for AWS serverless infrastructure using SST v3 (Serverless Stack). Use when configuring deployment, creating stacks, managing secrets, setting up CI/CD, or deploying Next.js applications to AWS Lambda with DynamoDB.
2gs-sst-infra
Guide for AWS serverless infrastructure using SST v3. Covers DynamoDB, Next.js deployment, Lambda handlers with Clean Architecture adapter pattern, and CI/CD configuration.
2feature-architecture
Guide for implementing features in a layered Next.js full-stack architecture. Use when planning new features, understanding the layer structure (Model, DAL, Service, Actions, Components, Pages), or deciding where code should live.
2gs-nextjs-web-client
Guide for building Next.js 15+ React 19+ frontend components with Clean Architecture. Components receive DTOs from server actions, never Entities directly. Use when creating UI components, pages, layouts, forms, or client-side interactivity.
1gs-santry-observability
Sentry observability for Clean Architecture layers. Error tracking per layer, transaction tracing for Use Cases, user context in procedures, and performance monitoring.
1