AWS API Gateway

Fully managed API layer. Routes HTTP requests to Lambda, ECS, EC2, or any HTTP backend.

Fully managed API layer. Routes HTTP requests to Lambda, ECS, EC2, or any HTTP backend. Three flavours: REST API (feature-rich, expensive), HTTP API (80% cheaper, modern), WebSocket API (persistent connections).


REST API vs HTTP API

REST APIHTTP API
Cost$3.50/million$1.00/million
Latency~6ms~1ms
AuthIAM, Cognito, Lambda authoriser, API keysIAM, JWT, Lambda authoriser
Request validationBuilt-inManual
CachingYesNo
Usage plans + throttlingYesBasic
WebSocketNoNo

Choose HTTP API for new projects unless you specifically need request validation, caching, or usage plans.


HTTP API with Lambda Integration

# Create HTTP API
API_ID=$(aws apigatewayv2 create-api \
  --name my-api \
  --protocol-type HTTP \
  --cors-configuration AllowOrigins='["https://myapp.com"]',AllowMethods='["GET","POST"]',AllowHeaders='["Content-Type","Authorization"]' \
  --query 'ApiId' --output text)

# Lambda integration
INTEGRATION_ID=$(aws apigatewayv2 create-integration \
  --api-id $API_ID \
  --integration-type AWS_PROXY \
  --integration-uri arn:aws:lambda:eu-west-1:123456789:function:my-api \
  --payload-format-version 2.0 \
  --query 'IntegrationId' --output text)

# Route
aws apigatewayv2 create-route \
  --api-id $API_ID \
  --route-key 'ANY /{proxy+}' \
  --target "integrations/$INTEGRATION_ID"

# Deploy
aws apigatewayv2 create-stage \
  --api-id $API_ID \
  --stage-name production \
  --auto-deploy

Lambda Handler — HTTP API v2 Payload Format

def handler(event, context):
    # HTTP API v2 event structure
    method = event["requestContext"]["http"]["method"]
    path = event["requestContext"]["http"]["path"]
    query = event.get("queryStringParameters", {}) or {}
    headers = event.get("headers", {})
    body = event.get("body", "")

    # Auth from JWT authoriser
    claims = event["requestContext"].get("authorizer", {}).get("jwt", {}).get("claims", {})
    user_id = claims.get("sub")

    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({"userId": user_id, "path": path})
    }

JWT Authoriser

Validates JWT tokens from Cognito, Auth0, or any OIDC provider. Without Lambda code.

aws apigatewayv2 create-authorizer \
  --api-id $API_ID \
  --authorizer-type JWT \
  --identity-source '$request.header.Authorization' \
  --name jwt-authoriser \
  --jwt-configuration \
    Audience=["my-client-id"],\
    Issuer="https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_XXXXX"

Custom Domain

# Create custom domain
aws apigatewayv2 create-domain-name \
  --domain-name api.example.com \
  --domain-name-configurations \
    CertificateArn=arn:aws:acm:eu-west-1:123456789:certificate/xxx,EndpointType=REGIONAL

# Map to API stage
aws apigatewayv2 create-api-mapping \
  --domain-name api.example.com \
  --api-id $API_ID \
  --stage production

Point your DNS CNAME to the ApiGatewayDomainName from the domain creation output.


Throttling and Usage Plans (REST API)

# Create usage plan: 10,000 req/day, 100 req/second burst
aws apigateway create-usage-plan \
  --name standard-plan \
  --throttle burstLimit=100,rateLimit=50 \
  --quota limit=10000,period=DAY

# Create API key and link to plan
KEY_ID=$(aws apigateway create-api-key --name my-key --enabled --query 'id' --output text)
aws apigateway create-usage-plan-key --usage-plan-id $PLAN_ID --key-id $KEY_ID --key-type API_KEY

WebSocket API

Persistent bidirectional connections. Lambda handles $connect, $disconnect, and $default (message) routes.

# WebSocket Lambda handler
import boto3

def handler(event, context):
    route = event["requestContext"]["routeKey"]
    connection_id = event["requestContext"]["connectionId"]
    domain = event["requestContext"]["domainName"]
    stage = event["requestContext"]["stage"]

    if route == "$connect":
        # Store connection_id in DynamoDB
        save_connection(connection_id)

    elif route == "$disconnect":
        remove_connection(connection_id)

    elif route == "$default":
        # Send message back to client
        apigw = boto3.client("apigatewaymanagementapi",
            endpoint_url=f"https://{domain}/{stage}")
        apigw.post_to_connection(
            ConnectionId=connection_id,
            Data=json.dumps({"message": "received"}).encode()
        )

    return {"statusCode": 200}

Common Failure Cases

Lambda returns 502 Bad Gateway from API Gateway Why: the Lambda function returned a response body that API Gateway cannot parse — typically a raw string instead of the expected {"statusCode": ..., "body": ...} format. Detect: CloudWatch Logs for the Lambda show a successful execution, but the caller receives 502 with {"message": "Internal Server Error"}. Fix: ensure the handler always returns a dict with statusCode, headers, and body (as a string); never return a bare Python object.

CORS preflight returns 403 even though CORS is configured Why: CORS on HTTP API is set at the API level, but the Lambda returns its own Access-Control-Allow-Origin header, and the two conflict; or the OPTIONS route is not correctly handled. Detect: browser console shows CORS error on the preflight OPTIONS request; the Access-Control-Allow-Origin header is missing or duplicated in the response. Fix: either let API Gateway handle CORS entirely (remove headers from Lambda) or disable API Gateway CORS config and handle it fully inside the Lambda — never mix both.

JWT authoriser rejects valid tokens with 401 Why: the Issuer URL in the authoriser config doesn't exactly match the iss claim in the JWT (trailing slash, HTTP vs HTTPS, or wrong region in the Cognito URL). Detect: 401 Unauthorized with {"message": "Unauthorized"} from API Gateway even though the token is freshly issued. Fix: compare the exact string of the iss claim from jwt.io against the Issuer value configured on the authoriser; they must match character-for-character.

Custom domain not resolving after API mapping Why: the DNS CNAME record points to the wrong value, or the ACM certificate is in the wrong region (HTTP API custom domains require a regional ACM certificate in the same region as the API). Detect: nslookup api.example.com returns NXDOMAIN or points to a different endpoint than the ApiGatewayDomainName output. Fix: create the ACM certificate in the same region as the API, verify the certificate is ISSUED, and update the CNAME to the exact ApiGatewayDomainName value from the domain creation response.

Connections

cloud-hub · cloud/aws-core · cloud/aws-lambda-patterns · cloud/secrets-management · cloud/cloud-monitoring

Open Questions

  • What monitoring and alerting matter most when this is deployed in production?
  • At what scale or workload does this approach hit its practical limits?