Module Project — Serverless API with Lambda, API Gateway & DynamoDB
Checking access...
In this project, you will deploy a serverless REST API for a notes application. The API supports CRUD operations backed by DynamoDB and secured via API Gateway.
Architecture
Client (HTTP) │ ▼┌──────────────────────┐│ API Gateway (REST) ││ /notes ││ ─ GET → list ││ ─ POST → create ││ /notes/{id} ││ ─ GET → read ││ ─ PUT → update ││ ─ DELETE → delete │└──────────┬───────────┘ │ ┌──────┴──────┐ │ Lambda │ │ Functions │ └──────┬──────┘ │ ┌──────┴──────┐ │ DynamoDB │ │ NotesTable │ └─────────────┘Prerequisites
- AWS account with CLI configured
- Terraform installed (v1.5+)
- Node.js or Python runtime installed locally
Step 1: Lambda Function Code
Create lambda/index.py:
import jsonimport osimport boto3import uuidfrom datetime import datetime
dynamodb = boto3.resource('dynamodb')table = dynamodb.Table(os.environ['TABLE_NAME'])
def lambda_handler(event, context): http_method = event['httpMethod'] path_params = event.get('pathParameters') or {} note_id = path_params.get('id')
if http_method == 'GET' and not note_id: return list_notes() elif http_method == 'GET' and note_id: return get_note(note_id) elif http_method == 'POST': return create_note(json.loads(event['body'])) elif http_method == 'PUT' and note_id: return update_note(note_id, json.loads(event['body'])) elif http_method == 'DELETE' and note_id: return delete_note(note_id) else: return respond(400, {'error': 'Invalid request'})
def list_notes(): response = table.scan() return respond(200, response['Items'])
def get_note(note_id): response = table.get_item(Key={'id': note_id}) item = response.get('Item') if not item: return respond(404, {'error': 'Note not found'}) return respond(200, item)
def create_note(data): note = { 'id': str(uuid.uuid4()), 'title': data['title'], 'content': data.get('content', ''), 'created_at': datetime.utcnow().isoformat(), 'updated_at': datetime.utcnow().isoformat() } table.put_item(Item=note) return respond(201, note)
def update_note(note_id, data): table.update_item( Key={'id': note_id}, UpdateExpression='SET title = :t, content = :c, updated_at = :u', ExpressionAttributeValues={ ':t': data['title'], ':c': data.get('content', ''), ':u': datetime.utcnow().isoformat() } ) return respond(200, {'id': note_id, **data})
def delete_note(note_id): table.delete_item(Key={'id': note_id}) return respond(200, {'message': 'Note deleted'})
def respond(status_code, body): return { 'statusCode': status_code, 'headers': { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, 'body': json.dumps(body) }Step 2: Terraform Configuration
Create main.tf:
provider "aws" { region = "us-east-1"}
# DynamoDB tableresource "aws_dynamodb_table" "notes" { name = "NotesTable" billing_mode = "PAY_PER_REQUEST" hash_key = "id"
attribute { name = "id" type = "S" }}
# IAM role for Lambdaresource "aws_iam_role" "lambda_role" { name = "notes-api-lambda-role"
assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Principal = { Service = "lambda.amazonaws.com" } Action = "sts:AssumeRole" } ] })}
resource "aws_iam_role_policy" "lambda_policy" { name = "notes-api-lambda-policy" role = aws_iam_role.lambda_role.id
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Scan" ] Resource = aws_dynamodb_table.notes.arn }, { Effect = "Allow" Action = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ] Resource = "*" } ] })}
# Lambda functionresource "aws_lambda_function" "notes_api" { filename = "lambda.zip" function_name = "notes-api" role = aws_iam_role.lambda_role.arn handler = "index.lambda_handler" runtime = "python3.12" timeout = 10 memory_size = 128
environment { variables = { TABLE_NAME = aws_dynamodb_table.notes.name } }}
# API Gatewayresource "aws_api_gateway_rest_api" "notes_api" { name = "Notes API" description = "Serverless Notes API"}
resource "aws_api_gateway_resource" "notes" { rest_api_id = aws_api_gateway_rest_api.notes_api.id parent_id = aws_api_gateway_rest_api.notes_api.root_resource_id path_part = "notes"}
resource "aws_api_gateway_resource" "note" { rest_api_id = aws_api_gateway_rest_api.notes_api.id parent_id = aws_api_gateway_resource.notes.id path_part = "{id}"}
# Methods - GET /notesresource "aws_api_gateway_method" "list_notes" { rest_api_id = aws_api_gateway_rest_api.notes_api.id resource_id = aws_api_gateway_resource.notes.id http_method = "GET" authorization = "NONE"}
resource "aws_api_gateway_integration" "list_notes_int" { rest_api_id = aws_api_gateway_rest_api.notes_api.id resource_id = aws_api_gateway_resource.notes.id http_method = aws_api_gateway_method.list_notes.http_method type = "AWS_PROXY" integration_http_method = "POST" uri = aws_lambda_function.notes_api.invoke_arn}
# Methods - POST /notesresource "aws_api_gateway_method" "create_note" { rest_api_id = aws_api_gateway_rest_api.notes_api.id resource_id = aws_api_gateway_resource.notes.id http_method = "POST" authorization = "NONE"}
resource "aws_api_gateway_integration" "create_note_int" { rest_api_id = aws_api_gateway_rest_api.notes_api.id resource_id = aws_api_gateway_resource.notes.id http_method = aws_api_gateway_method.create_note.http_method type = "AWS_PROXY" integration_http_method = "POST" uri = aws_lambda_function.notes_api.invoke_arn}
# Lambda permission for API Gatewayresource "aws_lambda_permission" "api_gw" { statement_id = "AllowAPIGatewayInvoke" action = "lambda:InvokeFunction" function_name = aws_lambda_function.notes_api.function_name principal = "apigateway.amazonaws.com" source_arn = "${aws_api_gateway_rest_api.notes_api.execution_arn}/*/*"}
# Deploymentresource "aws_api_gateway_deployment" "prod" { depends_on = [ aws_api_gateway_integration.list_notes_int, aws_api_gateway_integration.create_note_int ]
rest_api_id = aws_api_gateway_rest_api.notes_api.id stage_name = "prod"}
# Outputsoutput "api_url" { value = "${aws_api_gateway_deployment.prod.invoke_url}/notes"}Step 3: Package and Deploy
# Package Lambda codecd lambdazip ../lambda.zip index.pycd ..
# Deployterraform initterraform apply -auto-approve
# Test the APIAPI_URL=$(terraform output -raw api_url)
# Create a notecurl -X POST $API_URL \ -H "Content-Type: application/json" \ -d '{"title":"My Note","content":"Hello from Lambda!"}'
# List all notescurl $API_URL
# Get single notecurl $API_URL/<id>
# Update a notecurl -X PUT $API_URL/<id> \ -H "Content-Type: application/json" \ -d '{"title":"Updated Note","content":"Updated content"}'
# Delete a notecurl -X DELETE $API_URL/<id>Step 4: Clean Up
terraform destroy -auto-approveTip
Use AWS SAM CLI (sam local invoke) for local testing before deploying. SAM provides sam local start-api to test the full API Gateway integration locally.
Deliverables
- Serverless REST API with full CRUD operations
- DynamoDB table with on-demand billing
- Lambda function with least-privilege IAM role
- API Gateway with proxy integration and CORS headers
- Working
curlcommands demonstrating all endpoints