Lambda + API Gateway — Building Serverless REST APIs
In this tutorial, you will learn about Lambda + API Gateway. We cover key concepts, practical examples, and best practices to help you master this topic.
AWS API Gateway is a managed service for creating RESTful and WebSocket APIs that front Lambda functions, handling authentication, throttling, caching, and request validation.
What You'll Learn
By the end of this lesson you will understand how to create REST and HTTP APIs with API Gateway, integrate them with Lambda, enable CORS, handle authentication, and deploy to stages.
Why It Matters
API Gateway is the most common front door for serverless applications. It handles HTTP concerns so your Lambda focuses on business logic -- request validation, Rate Limiting, API keys, and custom domain names are all managed by API Gateway.
Real-World Use
Doda Browser's bookmark sync API is built with API Gateway and Lambda. API Gateway handles JWT validation, rate limiting, and CORS while Lambda functions manage CRUD operations on user bookmarks stored in DynamoDB.
flowchart LR
C[Client] --> AG[API Gateway]
AG -->|Auth| AU[Lambda Authorizer]
AG -->|Request| L[AWS Lambda]
L --> D[DynamoDB]
AG --> R[Response]
style AG fill:#f90,color:#fff
REST API vs HTTP API
REST APIs offer more features including API keys, usage plans, and AWS WAF integration. HTTP APIs are simpler, cheaper, and faster but with fewer features.
# api_types.py
# Comparing API Gateway types
def rest_api_features():
return {
"name": "REST API",
"cost": "$3.50/M req",
"latency": "Higher (transformations)",
"features": [
"API keys and usage plans",
"Request/response transformation",
"WAF integration",
"Custom domain names",
"Canary deployments"
]
}
def http_api_features():
return {
"name": "HTTP API",
"cost": "$1.00/M req",
"latency": "Lower (proxy only)",
"features": [
"JWT authorizers",
"CORS configuration",
"Automatic deployments",
"Cross-account support"
]
}
def print_feature_comparison():
rest = rest_api_features()
http = http_api_features()
print(f"Feature REST API HTTP API")
print(f"{'Cost':16s} {rest['cost']:16s} {http['cost']:16s}")
print(f"{'Latency':16s} {rest['latency']:16s} {http['latency']:16s}")
print(f"\nREST features: {', '.join(rest['features'][:3])}")
print(f"HTTP features: {', '.join(http['features'][:3])}")
print_feature_comparison()
Lambda Proxy Integration
In proxy mode, API Gateway passes the entire HTTP request to Lambda and maps the Lambda response back to HTTP. This gives your function full control over status codes, headers, and body.
# proxy_integration.py
# Lambda proxy integration handler
import json
def lambda_handler(event, context):
method = event.get("httpMethod", "GET")
path = event.get("path", "/")
query = event.get("queryStringParameters", {}) or {}
headers = event.get("headers", {})
body = event.get("body", "null")
print(f"{method} {path} - {json.dumps(query)}")
if method == "GET" and path == "/items":
items = [{"id": 1, "name": "Book"}, {"id": 2, "name": "Pen"}]
if "search" in query:
items = [i for i in items if query["search"].lower() in i["name"].lower()]
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(items)
}
if method == "POST" and path == "/items":
data = json.loads(body)
new_item = {"id": 3, **data}
return {
"statusCode": 201,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(new_item)
}
return {"statusCode": 404, "body": json.dumps({"error": "Not found"})}
test = {"httpMethod": "GET", "path": "/items", "queryStringParameters": {"search": "book"}}
print(lambda_handler(test, None)["body"])
Expected output:
[{"id": 1, "name": "Book"}]
JWT Authorizers
HTTP API JWT authorizers validate tokens before the request reaches your Lambda, reducing cold starts for unauthenticated requests.
# jwt_auth.py
# JWT authorization with API Gateway
def validate_jwt(token):
"""Simulate JWT validation."""
import json
import base64
try:
payload = token.split(".")[1]
padded = payload + "=" * (4 - len(payload) % 4)
decoded = json.loads(base64.b64decode(padded))
return decoded
except:
return None
def lambda_handler(event, context):
claims = event.get("requestContext", {}).get("authorizer", {}).get("jwt", {}).get("claims", {})
user_id = claims.get("sub", "anonymous")
email = claims.get("email", "unknown")
print(f"Authenticated user: {email} ({user_id})")
return {
"statusCode": 200,
"body": json.dumps({"user": email, "message": "Access granted"})
}
mock_claims = {"sub": "user123", "email": "alice@example.com"}
mock_event = {"requestContext": {"authorizer": {"jwt": {"claims": mock_claims}}}}
print(lambda_handler(mock_event, None)["body"])
Expected output:
Authenticated user: alice@example.com (user123)
{"user": "alice@example.com", "message": "Access granted"}
CORS Configuration
Cross-Origin Resource Sharing must be configured at the API Gateway level and in the Lambda response headers.
# cors_config.py
# CORS handler for Lambda
def lambda_handler(event, context):
origin = event.get("headers", {}).get("origin", "*")
if event.get("httpMethod") == "OPTIONS":
return {
"statusCode": 200,
"headers": {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
"Access-Control-Allow-Headers": "Content-Type,Authorization",
"Access-Control-Allow-Credentials": True
},
"body": ""
}
return {
"statusCode": 200,
"headers": {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Credentials": True
},
"body": json.dumps({"message": "CORS enabled"})
}
options_event = {"httpMethod": "OPTIONS", "headers": {"origin": "https://myapp.com"}}
print(f"OPTIONS: {lambda_handler(options_event, None)}")
import json
get_event = {"httpMethod": "GET", "headers": {"origin": "https://myapp.com"}}
print(f"GET: {lambda_handler(get_event, None)['headers']}")
Common Mistakes
Not enabling CORS for browser clients: Without CORS headers, browser requests fail with cross-origin errors. Configure at both API Gateway and Lambda levels.
Forgetting request validation: API Gateway can validate request bodies, query parameters, and headers before they reach Lambda, reducing unnecessary invocations.
Overly permissive Lambda authorizers: Authorizer functions should be lightweight. Avoid making database calls or external API requests in the authorizer.
Not using usage plans for public APIs: Usage plans with API keys prevent abuse by limiting request rates per client.
Misunderstanding proxy vs non-proxy integration: In proxy mode Lambda controls the entire response. In non-proxy mode API Gateway maps the response.
Practice Questions
What is the difference between REST API and HTTP API in API Gateway? REST API offers more features like usage plans and WAF. HTTP API is simpler, cheaper, and faster with JWT support.
How does Lambda proxy integration work? API Gateway passes the entire request to Lambda. Lambda returns a response with statusCode, headers, and body.
What is the purpose of a Lambda authorizer? It validates authentication tokens before the request reaches the Lambda handler, reducing unnecessary cold starts.
How do you handle CORS in serverless APIs? Configure CORS in API Gateway and return CORS headers from Lambda. Handle OPTIONS preflight requests.
Challenge: Build a serverless REST API with API Gateway, Lambda, and DynamoDB that supports CRUD operations with JWT authentication and CORS.
FAQ
Mini Project
Create a serverless CRUD API for managing blog posts with API Gateway and Lambda. Include JWT authentication, input validation, CORS support, and DynamoDB storage.
import json
posts = [
{"id": "1", "title": "Serverless Guide", "author": "Alice", "published": True},
{"id": "2", "title": "Lambda Best Practices", "author": "Bob", "published": False},
]
def lambda_handler(event, context):
method = event["httpMethod"]
path = event["path"]
body = json.loads(event.get("body", "null") or "null") if event.get("body") else None
path_id = event.get("pathParameters", {}).get("id") if event.get("pathParameters") else None
if method == "GET" and path == "/posts":
return {"statusCode": 200, "headers": {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"}, "body": json.dumps(posts)}
if method == "GET" and path_id:
post = next((p for p in posts if p["id"] == path_id), None)
return {"statusCode": 200 if post else 404, "body": json.dumps(post or {"error": "Not found"})}
if method == "POST":
new_post = {"id": str(len(posts) + 1), **body}
posts.append(new_post)
return {"statusCode": 201, "body": json.dumps(new_post)}
if method == "DELETE" and path_id:
posts[:] = [p for p in posts if p["id"] != path_id]
return {"statusCode": 204, "body": ""}
return {"statusCode": 404, "body": json.dumps({"error": "Not found"})}
print(lambda_handler({"httpMethod": "GET", "path": "/posts"}, None)["body"])
What's Next
Next: Lambda + DynamoDB for serverless database patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro