A sample project that shows how to build and deploy a Node-Boot
application as an AWS Lambda function, using the @nodeboot/lambda-server
package.
It demonstrates:
- Dependency Injection (
@EnableDI) - AOT Component Scanning for fast cold starts (
@EnableComponentScan) - Request validation with
class-validator(@EnableValidations) - Authorization (
@EnableAuthorization,@Authorized()) - Controllers, services, middleware and a custom error handler
- Local end-to-end testing with LocalStack (real Lambda + API Gateway emulation)
- Infrastructure-as-code deployment with Terraform (works against LocalStack or real AWS)
- Packaging and deploying the function with the Serverless Framework, a plain zip upload, or as a container image
src/
├── app.ts # NodeBootApplication bootstrapped on LambdaServer
├── handler.ts # AWS Lambda entry point (exported `handler`)
├── local-invoke.ts # Script to smoke-test the handler locally
├── controllers/ # HTTP controllers
├── services/ # Business logic (in-memory user store)
├── models/ # DTOs / validation models
├── middlewares/ # Logging middleware + custom error handler
└── auth/ # Authorization/CurrentUser resolvers
terraform/ # Terraform module (Lambda + API Gateway), works with LocalStack or AWS
docker-compose.localstack.yml # LocalStack container definition
scripts/
├── build-zip.sh # Packages dist/ + node_modules into lambda.zip
└── deploy-localstack.sh # Builds, packages and deploys straight to LocalStack via AWS CLI
Unlike the Express/Koa/Fastify samples, this application never "listens" on a port.
NodeBoot.run(LambdaServer) bootstraps the DI container, controllers, middleware and
routes exactly once, and LambdaServer#getHandler() returns a function of shape
(event, context) => Promise<APIGatewayProxyResult> that is invoked by AWS Lambda for
every incoming request:
// src/handler.tsletlambdaHandler: LambdaHandler|null=null;exportconsthandler=async(event: APIGatewayProxyEvent,context: Context)=>{if(!lambdaHandler){constapp=awaitnewLambdaSampleApp().start();lambdaHandler=(app.serverasLambdaServer).getHandler();}returnlambdaHandler(event,context);};The lambdaHandler is cached at module scope so the DI container, controllers and
routes are only rebuilt on a cold start; warm invocations reuse the same instance,
which keeps latency low.
- Node.js 20+ and pnpm (this is a workspace package)
- Docker — for LocalStack and/or the container image build
- AWS CLI v2 — for LocalStack deployment and plain zip uploads
- Terraform >= 1.5 — for the
terraform/module - Real AWS credentials — only needed if you deploy against actual AWS
Install dependencies from the repository root (this is a pnpm workspace):
pnpm installcd samples/sample-lambda
pnpm run build
pnpm run invoke:localThis directly calls the exported handler with synthetic APIGatewayProxyEvent
objects — useful for a fast sanity check. Since this bypasses AWS Lambda's runtime
entirely, NodeBoot's internal lifecycle events keep the Node.js process alive for a
moment after the response is returned, so the script forces process.exit() once
it's done; prefer option B or C below for a more realistic end-to-end test.
cd samples/sample-lambda
pnpm run devThis builds the app and starts serverless-offline,
emulating API Gateway on http://localhost:3000. Try it out:
curl http://localhost:3000/api/hello
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer test" \
-d '{"name":"Ada Lovelace","email":"ada@example.com"}'LocalStack runs an actual Lambda runtime and API Gateway locally in Docker, so this is the closest you can get to production behavior without touching a real AWS account. Requires Docker and the AWS CLI.
cd samples/sample-lambda
# 1. Start LocalStack (docker-compose.localstack.yml)
pnpm run localstack:up
# 2. Build, package and deploy the function + REST API to LocalStack
pnpm run deploy:localstackThe script prints the invoke URL when done, e.g.:
curl "http://localhost:4566/restapis/<api-id>/local/_user_request_/api/hello"
curl -X POST "http://localhost:4566/restapis/<api-id>/local/_user_request_/api/users" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer test" \
-d '{"name":"Ada Lovelace","email":"ada@example.com"}'Tear it down with:
pnpm run localstack:downNote: Running a Lambda inside LocalStack requires LocalStack to spin up its own Docker container to execute your function code, so Docker needs a few GBs of free memory available. If
deploy:localstackfails with connection errors after the Lambda creation step, checkdocker logs nodeboot-lambda-localstack— this is almost always a memory-constrained Docker daemon (bump Docker Desktop's memory limit) rather than a configuration issue.
pnpm run buildThis compiles TypeScript to dist/ and runs @nodeboot/aot's node-boot-aot script
(postbuild), which pre-scans your compiled components (@Controller, @Service,
@Middleware, etc.) into dist/node-boot-beans.json. @EnableComponentScan reads this
manifest at startup instead of walking the filesystem, which meaningfully reduces
Lambda cold-start time.
Four ways to ship this function, pick whichever fits your workflow:
The included terraform/ module provisions everything needed to run
this sample: an IAM execution role, a CloudWatch log group, the Lambda function itself,
and a REST API Gateway with a Lambda proxy integration on ANY / and ANY /{proxy+}
(so NodeBoot's own router handles every method/path). The same module can target
either LocalStack or real AWS — only the use_localstack/localstack_endpoint
variables change (see terraform/providers.tf).
Against LocalStack (requires pnpm run localstack:up first):
pnpm run build && pnpm run package # produces ./lambda.zip, read by terraform/lambda.tf
pnpm run terraform:init
pnpm run terraform:plan # terraform plan -var-file=localstack.tfvars
pnpm run terraform:apply # terraform apply -var-file=localstack.tfvarsTerraform prints a localstack_invoke_url output you can curl directly:
curl "$(terraform -chdir=terraform output -raw localstack_invoke_url)/api/hello"Tear down with pnpm run terraform:destroy.
Against real AWS:
cd terraform
cp terraform.tfvars.example terraform.tfvars # edit as needed
pnpm run build && pnpm run package
pnpm run terraform:init
pnpm run terraform:apply:aws # terraform apply (no -var-file)This prints an invoke_url output pointing at the real API Gateway stage. Tear down
with pnpm run terraform:destroy:aws.
The included serverless.yml provisions the Lambda function, an
HTTP API (API Gateway v2) with a catch-all ANY /{proxy+} route (so NodeBoot's own
router handles every method/path), and IAM permissions.
# Configure AWS credentials first (aws configure / env vars / SSO, etc.)
pnpm run deploy:serverless # builds and runs `serverless deploy`
pnpm run remove:serverless # tears the stack downpnpm run build
pnpm run package # produces ./lambda.zip (dist/ + node_modules/)Then, for example, with the AWS CLI:
aws lambda create-function \
--function-name nodeboot-lambda-sample \
--runtime nodejs20.x \
--handler handler.handler \
--role arn:aws:iam::<account-id>:role/<lambda-execution-role> \
--zip-file fileb://lambda.zip
# subsequent deploys
aws lambda update-function-code \
--function-name nodeboot-lambda-sample \
--zip-file fileb://lambda.ziplambda.zip (and its dist/node_modules contents) can equally be referenced from a
SAM template.yaml or an AWS CDK lambda.Function/lambda.Code.fromAsset construct.
For larger dependency trees or when you prefer container-based deployment:
docker build -t nodeboot-lambda-sample .
docker tag nodeboot-lambda-sample:latest <account-id>.dkr.ecr.<region>.amazonaws.com/nodeboot-lambda-sample:latest
docker push <account-id>.dkr.ecr.<region>.amazonaws.com/nodeboot-lambda-sample:latest
aws lambda create-function \
--function-name nodeboot-lambda-sample \
--package-type Image \
--code ImageUri=<account-id>.dkr.ecr.<region>.amazonaws.com/nodeboot-lambda-sample:latest \
--role arn:aws:iam::<account-id>:role/<lambda-execution-role>Whichever deployment method you choose, configure API Gateway (REST or HTTP API) with:
- Integration type: Lambda proxy integration
- Route/Resource:
{proxy+} - Method:
ANY
This lets NodeBoot's internal find-my-way router handle every HTTP method and path,
exactly as it would with a traditional HTTP server.
- No listening server:
LambdaServer#getHttpServer()and#getFramework()returnundefined— there's no underlying HTTP server/framework instance, since AWS Lambda invokes the handler function directly. - Custom error handlers can't build the response body: in Express/Koa/Fastify, a
@ErrorHandler()writes directly to the mutableresponseobject. In Lambda,action.responseis the AWSContext, which has no such methods — the actualAPIGatewayProxyResultis always assembled by NodeBoot's built-inGlobalErrorHandler. Custom error handlers are still invoked and are the right place for side effects (logging, alerting, metrics), but cannot override the response status/body. SeeErrorMiddleware.ts. - Stateless containers: don't rely on in-memory state surviving between cold
starts (the sample's
UserServiceis in-memory purely for simplicity — replace it with DynamoDB, Aurora Serverless, RDS, etc. in a real application).
This project is licensed under the MIT License.