Production-grade AWS TypeScript Lambda template for teams that want a serious starting point without inheriting a framework-sized codebase.
This template is intentionally small, but it is not bare bones. It ships the parts that make a Lambda service feel production-ready on day one: AWS-native infrastructure, structured observability, typed handlers, fast builds, tests, and CI/CD.
- AWS Lambda Node.js 24 managed runtime
- ARM64 architecture
- ESM TypeScript with strict compiler settings
- Explicit
aws-lambdahandler types - esbuild bundling with source maps and production minification
- Jest unit tests with coverage
- VSCode debug configuration for Jest breakpoints
- ESLint and Prettier with zero-warning CI enforcement
- AWS Powertools Logger, Metrics, and Tracer
- Standard API Gateway JSON responses
- Shared error hierarchy and error normalization
- Runtime environment validation with Zod
- AWS CDK v2 infrastructure for
dev,staging, andproduction - GitHub Actions CI and OIDC-based deployment workflow
Use Node.js 24 or newer.
npm ci
npm test
npm run buildCopy .env.example when you need local environment values for scripts or ad hoc invocation.
The runtime config also provides safe local defaults for development and tests.
Run type checking, linting, formatting, and tests locally:
npm run typecheck
npm run lint
npm run format:check
npm testBuild once or watch handler bundles:
npm run build
npm run build:watchThe build writes bundled Lambda handlers to dist/handlers and declaration files to
dist/types.
The initial application handler lives in src/handlers/app.ts.
Use it as the first implementation reference for real API Gateway Lambda work. It demonstrates the standards this template is opinionated about:
- explicit AWS Lambda event, context, and response types
asynchandler implementation- shared middleware wrapping
- correlation ID propagation
- structured logging
- standardized JSON responses
The health check remains separate in src/handlers/health.ts. For Lambda, this is a lightweight
smoke-test endpoint for deployment verification, routing checks, configuration validation, and
observability signals rather than a long-lived instance readiness probe.
Jest is configured for ESM TypeScript through SWC.
npm test
npm run test:watch
npm run test:coverageUse the VSCode launch configurations Debug Jest Tests or Debug Current Jest File to debug
unit tests with breakpoints.
esbuild bundles every src/handlers/*.ts file as a Lambda entry point.
Production minification is enabled when NODE_ENV=production:
NODE_ENV=production npm run buildSynthesize the default dev environment:
npm run cdk:synthSynthesize a specific environment:
npx cdk synth -c stage=stagingDeploy after bootstrapping the target AWS account and region:
npm run build
npx cdk deploy aws-ts-lambda-template-dev -c stage=devEnvironment settings live in infrastructure/lib/environment.ts.
For local deploys, configure AWS credentials with your usual AWS CLI flow before running CDK. The simplest path is an SSO-backed profile:
aws configure sso
aws sso login --profile my-profile
AWS_PROFILE=my-profile npx cdk deploy aws-ts-lambda-template-dev -c stage=devStatic access keys also work for local experimentation through aws configure, but avoid using
long-lived keys for shared environments. For GitHub Actions deployment, use OIDC instead.
ci.yml runs on pull requests and pushes to main:
- Checkout code
- Install dependencies
- Run lint
- Run format check
- Run tests
- Generate coverage
- Build application
- Synthesize CDK infrastructure
deploy.yml is manually dispatched and uses GitHub OIDC, so deployments do not need stored AWS
access keys. The workflow requests id-token: write, assumes an AWS IAM role, synthesizes CDK,
and deploys the selected environment.
destroy.yml is also manually dispatched and uses the same GitHub Environment and OIDC role. It
destroys the selected CDK stack with cdk destroy --force; to reduce accidental teardown, the
workflow requires the confirmation input to be exactly destroy.
To set it up:
- Add the GitHub OIDC provider in AWS IAM for
https://token.actions.githubusercontent.com. - Create an IAM role for each environment, or one tightly scoped shared role.
- In the role trust policy, restrict access to this repository and GitHub Environment, for example
repo:<github-org>/<repo-name>:environment:dev. - Give the role the permissions required to deploy the CDK stack.
- In GitHub, create the
dev,staging, andproductionEnvironments. - Add
AWS_ROLE_TO_ASSUMEto each Environment with the IAM role ARN trusted by GitHub OIDC.
Use Environment protection rules for approvals on staging and production if your workflow
requires them. See docs/deployment.md for the trust policy shape.
AWS Powertools is configured in src/observability, giving the template real production signals
instead of print statements with good intentions:
- Logger: structured JSON logs, Lambda context enrichment, cold start detection, correlation IDs
- Metrics: CloudWatch EMF,
SuccessfulRequests,FailedRequests,ValidationErrors, custom metrics - Tracer: X-Ray handler instrumentation, service annotation, cold start annotation
The CDK construct enables Lambda active tracing, JSON log format, source maps, and log retention. See docs/observability.md for conventions.
src/handlers/app.ts is the deployed application handler for the root route. Handlers use explicit AWS
Lambda types and async functions:
import type {
APIGatewayProxyEventV2,
APIGatewayProxyStructuredResultV2,
Context
} from 'aws-lambda';
import { withApiGatewayMiddleware } from '../middlewares/api-gateway.js';
import { ok } from '../utils/http.js';
const appHandler = async (
event: APIGatewayProxyEventV2,
context: Context
): Promise<APIGatewayProxyStructuredResultV2> => {
return ok({
requestId: context.awsRequestId,
route: event.routeKey
});
};
export const handler = withApiGatewayMiddleware(appHandler, {
operationName: 'example'
});import { MetricUnit } from '@aws-lambda-powertools/metrics';
import { addCustomMetric } from './observability/index.js';
addCustomMetric('ItemsProcessed', 3, MetricUnit.Count);import { logger } from './observability/index.js';
logger.info('Order processed', {
orderId: 'order-123'
});