A boilerplate repository that can be cloned and modified to quickly spin up nearly-free APIs using AWS Lambda.
Follow the Setup Guide for detailed instructions on using this repo to spin up an API.
- Serverless Architecture: Built with AWS Lambda, DynamoDB, and API Gateway
- TypeScript: Full type safety with modern ES6+ features
- Cost Optimized: Pay-per-use model with minimal idle costs (~$0.00-0.02/month)
- Local Development: Complete LocalStack environment for offline development
- Automated CI/CD: GitHub Actions with AWS OIDC authentication
- Infrastructure as Code: Terraform for reproducible deployments
- Comprehensive Testing: Unit tests, integration tests, and coverage reporting
- CORS Enabled: Ready for frontend integration
- Production:
https://{api-id}.execute-api.{region}.amazonaws.com/production - Development:
https://{api-id}.execute-api.{region}.amazonaws.com/development - Local:
http://localhost:4566/restapis/{api-id}/production/_user_request_
All API endpoints require authentication using AWS Cognito User Pool. You need to include a valid JWT token in the Authorization header.
Authentication Header:
Authorization: Bearer <jwt-token>
Getting a JWT Token:
You can obtain a JWT token by authenticating with the Cognito User Pool using AWS SDK or AWS CLI:
# Using AWS CLI to authenticate and get tokens
aws cognito-idp initiate-auth \
--auth-flow USER_PASSWORD_AUTH \
--client-id <your-client-id> \
--auth-parameters USERNAME=<username>,PASSWORD=<password>- Method:
POST - Path:
/todos - Description: Creates a new todo item for the authenticated user
- Authentication: Required
Request Headers:
Content-Type: application/json
Authorization: Bearer <jwt-token>
Request Body:
{
"title": "Buy groceries"
}Success Response (201):
{
"data": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"title": "Buy groceries",
"status": "pending",
"userId": "user-123",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
}Error Response (400):
{
"error": {
"message": "Validation failed",
"code": "VALIDATION_ERROR",
"details": {
"errors": ["Title is required"]
}
}
}Error Response (401):
{
"error": {
"message": "Unauthorized",
"code": "UNAUTHORIZED"
}
}- Method:
GET - Path:
/todos - Description: Retrieves all todo items for the authenticated user
- Authentication: Required
Request Headers:
Authorization: Bearer <jwt-token>
Success Response (200):
{
"data": [
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"title": "Buy groceries",
"status": "pending",
"userId": "user-123",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
},
{
"id": "987fcdeb-51a2-43d1-9c4f-123456789abc",
"title": "Walk the dog",
"status": "completed",
"userId": "user-123",
"createdAt": "2024-01-15T09:15:00.000Z",
"updatedAt": "2024-01-15T11:45:00.000Z"
}
]
}Empty Response (200):
{
"data": []
}- Method:
PUT - Path:
/todos/{id}/complete - Description: Marks a todo as completed for the authenticated user
- Authentication: Required
Request Headers:
Authorization: Bearer <jwt-token>
Success Response (200):
{
"data": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"title": "Buy groceries",
"status": "completed",
"userId": "user-123",
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T12:00:00.000Z"
}
}Not Found Response (404):
{
"error": {
"message": "Todo not found",
"code": "NOT_FOUND"
}
}# First, get a JWT token (replace with your Cognito User Pool details)
JWT_TOKEN=$(aws cognito-idp initiate-auth \
--auth-flow USER_PASSWORD_AUTH \
--client-id <your-client-id> \
--auth-parameters USERNAME=<username>,PASSWORD=<password> \
--query 'AuthenticationResult.AccessToken' \
--output text)
# Create a todo
curl -X POST https://your-api-url/todos \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $JWT_TOKEN" \
-d '{"title": "Learn serverless architecture"}'
# Get all todos
curl https://your-api-url/todos \
-H "Authorization: Bearer $JWT_TOKEN"
# Complete a todo
curl -X PUT https://your-api-url/todos/123e4567-e89b-12d3-a456-426614174000/complete \
-H "Authorization: Bearer $JWT_TOKEN"// Assume you have obtained a JWT token from Cognito
const jwtToken = "your-jwt-token-here";
// Create a todo
const response = await fetch("https://your-api-url/todos", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${jwtToken}`,
},
body: JSON.stringify({
title: "Learn serverless architecture",
}),
});
const newTodo = await response.json();
// Get all todos
const todosResponse = await fetch("https://your-api-url/todos", {
headers: {
Authorization: `Bearer ${jwtToken}`,
},
});
const todos = await todosResponse.json();
// Complete a todo
const completeResponse = await fetch(
`https://your-api-url/todos/${todoId}/complete`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${jwtToken}`,
},
}
);
const completedTodo = await completeResponse.json();- Get JWT Token: Use AWS CLI or Cognito SDK to authenticate and get a token
- Set Authorization Header: In Postman, add
Authorization: Bearer <your-jwt-token> - Make Requests: All API calls will now include user context automatically
- Docker and Docker Compose - For running LocalStack
- Node.js 22+ - Runtime environment
- AWS CLI - For LocalStack interaction (optional)
-
Install dependencies:
npm install
-
Start LocalStack and deploy functions:
npm run local:setup
-
Run integration tests:
npm run test:integration
-
Stop LocalStack:
npm run stop-local
| Script | Description |
|---|---|
npm run start-local |
Start LocalStack and initialize DynamoDB table |
npm run deploy-local |
Build and deploy Lambda functions to LocalStack |
npm run stop-local |
Stop LocalStack containers |
npm run local:setup |
Complete setup (start + deploy) |
npm test |
Run unit tests |
npm run test:integration |
Run integration tests against LocalStack |
npm run test:smoke |
Run smoke tests against deployed API |
npm run test:coverage |
Run tests with coverage report |
npm run build |
Build TypeScript and package Lambda functions |
npm run lint |
Run ESLint on source code |
The local development environment uses:
- LocalStack Endpoint:
http://localhost:4566 - DynamoDB Table:
todos - AWS Region:
eu-west-2 - AWS Credentials:
test/test(LocalStack defaults) - API Gateway: Auto-generated endpoint URL
Once LocalStack is running, you can test the API:
# Get the API Gateway URL from LocalStack logs
# The URL format is: http://localhost:4566/restapis/{api-id}/production/_user_request_
# Create a todo
curl -X POST http://localhost:4566/restapis/{api-id}/production/_user_request_/todos \
-H "Content-Type: application/json" \
-d '{"title": "Test local development"}'
# Get all todos
curl http://localhost:4566/restapis/{api-id}/production/_user_request_/todosFollow the comprehensive setup guide in .github/DEPLOYMENT_SETUP.md to configure:
- AWS OIDC Identity Provider
- IAM roles for GitHub Actions
- Required permissions and policies
Required Secrets (Settings β Secrets and variables β Actions):
AWS_ROLE_ARN_DEV: Development deployment role ARNAWS_ROLE_ARN_PROD: Production deployment role ARN
Required Variables:
AWS_REGION: AWS region for deployment (default:eu-west-2)
# Run the setup script for each environment
./scripts/setup-remote-state.sh development
./scripts/setup-remote-state.sh productionSee .github/REMOTE_STATE_SETUP.md for detailed instructions.
-
Development: Push to
developbranch -
Production: Merge a PR into the
mainbranch
After successful deployments, API URLs are available on the GitHub Deployments Page:
- Visit
https://github.com/foad/lambda_boilerplate/deployments - Click on any deployment to see the environment URL
- Destroy Infrastructure: Use the "Destroy Infrastructure" workflow in GitHub Actions
- Manual Deploy: Trigger workflows manually from the Actions tab
The "Destroy Infrastructure" workflow allows you to tear down AWS resources for a specific environment. Here's what you need to know:
What Gets Destroyed:
- All Lambda functions
- API Gateway REST API
- DynamoDB tables
- IAM roles
- Cognito User Pool (and all user accounts)
- CloudWatch log groups and their contents
What Gets Preserved by Default:
- Terraform state storage (S3 bucket and DynamoDB locking table)
- The state file itself (marked as destroyed but preserved for audit)
How to Use:
- Go to Actions β Destroy Infrastructure in GitHub
- Click Run workflow
- Select the environment (
developmentorproduction) - Type
destroyto confirm - Optionally check "Also destroy Terraform state storage" if you want to delete the state file
Complete Cleanup:
If you want to completely remove everything including shared state storage:
-
Destroy both environments with state storage option enabled
-
Manually delete shared resources (only after both environments are destroyed):
# Delete the S3 bucket aws s3 rb s3://terraform-state-serverless-todo-api --force # Delete the DynamoDB locking table aws dynamodb delete-table --table-name terraform-state-lock-serverless-todo-api
- Each environment has its own state file - destroying development state doesn't affect production and vice-versa
- The S3 bucket and DynamoDB table are shared between environments (only deleted in manual cleanup)
- Without state files, Terraform can't track remaining resources
- Always destroy development before production if doing complete cleanup
| Component | Idle Cost | Light Usage (1000 requests/month) |
|---|---|---|
| API Gateway | $0.00 | ~$0.0035 |
| Lambda Functions | $0.00 | ~$0.0001 |
| DynamoDB | $0.00-0.02 | ~$0.25 |
| Cognito User Pool | $0.00 | ~$0.00 |
| CloudWatch Logs | ~$0.01 | ~$0.50 |
| Total | ~$0.01-0.03/month | ~$0.75/month |
Notes:
- Costs scale with usage (pay-per-request model)
- No charges for idle time on Lambda and API Gateway
- DynamoDB uses on-demand billing
- Free tier includes 50,000 Monthly Active Users (MAUs) for Cognito
- Development environment has similar costs
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
β API Gateway βββββΆβ Lambda Functions βββββΆβ DynamoDB β
β β β β β β
β β’ REST API β β β’ Create Todo β β β’ todos table β
β β’ CORS enabled β β β’ Read Todos β β β’ Pay-per-req β
β β’ Regional β β β’ Update Todo β β β’ Encrypted β
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
- Node.js 18+ - JavaScript runtime
- Docker & Docker Compose - For LocalStack (local development)
- AWS CLI - For deployment (optional for local dev)
-
Setup:
# Clone and install dependencies git clone <repository-url> cd lambda_boilerplate npm install
-
Local Development:
# Start local environment npm run local:setup # Make changes to code # Re-deploy to LocalStack npm run deploy-local
-
Testing:
# Run unit tests npm test # Run integration tests (requires local environment) npm run test:integration # Generate coverage report npm run test:coverage
-
Build:
# Build for production npm run build
Local Development Issues:
# Check LocalStack status
curl http://localhost:4566/health
# View LocalStack logs
docker-compose logs localstack
# List DynamoDB tables
aws --endpoint-url=http://localhost:4566 dynamodb list-tablesThis project is licensed under the MIT License - see the LICENSE file for details.