This is a sample backend with Lambda + TypeScript + LocalStack + DynamoDB.
This setup allows:
- Familiar TDD workflow (w/ vitest).
- Minimal overhead.
- Integration tests using real(-ish) DynamoDB and S3.
- E2E tests via API Gateway.
- Uniform IaC. (You can apply the same Terraform to LocalStack and AWS.)
Limitations:
- Only supports JavaScript (TypeScript).
- No support of Lambda Layers.
- Limited support of AWS services (DynamoDB, S3, SQS, etc).
Why not using AWS SAM, Serverless.tf, etc?
AWS SAM is a big framework that covers a variety of use cases / languages, and it requires to use CloudFormation. Their support for local testing is poor. Serverless.tf does everything with Terraform, which seems acrobatic. Meanwhile, all you need is to compile a TypeScript code and create a zip file. This is a relatively simple task that can be done with a few commands. You don't need a big dependency for this.
Development (Unit tests / Integration tests):
graph LR
NODE["Node.js"] --> LOCALSTACK["LocalStack
(DynamoDB, S3, ...)"]
Deploy to Local (E2E tests):
graph LR
CLIENT["Client
(curl, playwright, ...)"] --> LOCALSTACK["LocalStack
(API Gateway, Lambda,
DynamoDB, S3, ...)"]
todoapp-lambda/
├── README.md # This file.
├── Makefile # Helper scripts.
├── docker-compose.yml # For LocalStack.
├── .nvmrc # Node.js version.
├── terraform/ # IaC.
│ ├── Makefile
│ ├── main.tf
│ ├── provider.tf
│ ├── iam.tf
│ └── modules/ # Terraform modules.
│ └── ...
├── app/ # Sample TODO app.
│ ├── Makefile
│ ├── biome.json
│ ├── package-lock.json
│ ├── package.json
│ ├── src/ # Source code (TypeScript).
│ │ ├── index.test.ts
│ │ └── index.ts
│ ├── e2e/ # E2E tests.
│ │ └── app.test.ts
│ └── tsconfig.json
└── tools/ # Helper scripts.
├── awslocal
├── tflocal
├── get_function_url.sh
└── get_rest_api_url.sh
- Node.js
- Docker
- Terraform
- LocalStack
$ docker compose up
...
$ make test# run tests
$ make lint # run linter
$ make clean # cleanup files
$ make deploy # deploy to LocalStackThe execution logs are stored on LocalStack:
$ cd app/
$ make tail SINCE=2h # show logs of last 2 hours.First, create a Terraform state bucket with globally unique name.
$ aws s3 mb s3://my-terraform-stateNext, initialize the resources on AWS (code isn't uploaded at this point):
$ cd terraform/
$ make init STATE_BUCKET=my-terraform-state
$ make plan
$ make applyThen deploy the code:
$ cd app/
$ make deploy AWSCLI=awsThe execution logs are stored on CloudWatch.
$ cd app/
$ make tail AWSCLI=aws- CI/CD support is incomplete.