Skip to content

Lambda + API Gateway — Building Serverless REST APIs

DodaTech Updated 2026-06-28 6 min read

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

  1. Not enabling CORS for browser clients: Without CORS headers, browser requests fail with cross-origin errors. Configure at both API Gateway and Lambda levels.

  2. Forgetting request validation: API Gateway can validate request bodies, query parameters, and headers before they reach Lambda, reducing unnecessary invocations.

  3. Overly permissive Lambda authorizers: Authorizer functions should be lightweight. Avoid making database calls or external API requests in the authorizer.

  4. Not using usage plans for public APIs: Usage plans with API keys prevent abuse by limiting request rates per client.

  5. 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

  1. 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.

  2. How does Lambda proxy integration work? API Gateway passes the entire request to Lambda. Lambda returns a response with statusCode, headers, and body.

  3. What is the purpose of a Lambda authorizer? It validates authentication tokens before the request reaches the Lambda handler, reducing unnecessary cold starts.

  4. How do you handle CORS in serverless APIs? Configure CORS in API Gateway and return CORS headers from Lambda. Handle OPTIONS preflight requests.

  5. Challenge: Build a serverless REST API with API Gateway, Lambda, and DynamoDB that supports CRUD operations with JWT authentication and CORS.

FAQ

What is the API Gateway timeout for Lambda integration?

29 seconds for REST APIs. HTTP APIs have a 30-second timeout. For longer operations, use asynchronous patterns with SQS or Step Functions.

Can API Gateway serve static content?

No. API Gateway is for APIs. Use S3 for static content or CloudFront for combined static and API delivery.

How does API Gateway pricing work?

You pay per million API calls plus data transfer. HTTP APIs cost about $1/M requests, REST APIs about $3.50/M requests.

Can I use custom domains with API Gateway?

Yes. Create a custom domain name in API Gateway and map it to your API stage.

How do I manage API versions?

Use API Gateway stages (dev, staging, prod) mapped to different API deployments. Each stage has its own URL.

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