Back to blog
Backend Systemsintermediate

API Gateway: REST APIs, Auth, Throttling & CORS on AWS

Design and deploy production REST APIs with Amazon API Gateway — proxy integration, authorizers, throttling, CORS, request validation, usage plans, and multi-stage deployments.

LearnixoApril 16, 20266 min read
API GatewayAWSREST APIServerlessLambdaAuthCORS
Share:š•

API Gateway Essentials

Amazon API Gateway is the front door to your serverless backend. It handles:

  • HTTP routing — map GET /appointments to a Lambda function
  • Auth — Cognito JWT validation, API keys, Lambda authorizers
  • Throttling — rate limits per stage and per API key
  • Request/response transformation — reshape payloads without code
  • CORS — cross-origin headers for browser clients
  • Caching — cache responses at the edge

There are three flavors: REST API (feature-rich, more config), HTTP API (simpler, 70% cheaper, faster), and WebSocket API. For most CRUD backends use HTTP API. For advanced features (usage plans, request validation, caching) use REST API.


Lambda Proxy Integration

The most common pattern — API Gateway passes the full request to Lambda and returns whatever Lambda returns:

Browser → API Gateway → Lambda proxy → response

The event Lambda receives:

JSON
{
  "version": "2.0",
  "routeKey": "GET /clinics/{id}",
  "rawPath": "/clinics/CLN-001",
  "rawQueryString": "include_staff=true",
  "headers": {
    "authorization": "Bearer eyJ...",
    "content-type": "application/json"
  },
  "pathParameters": { "id": "CLN-001" },
  "queryStringParameters": { "include_staff": "true" },
  "body": null,
  "requestContext": {
    "accountId": "123456789",
    "requestId": "abc-123",
    "authorizer": {
      "jwt": { "claims": { "sub": "user-id", "email": "doc@clinic.com" } }
    }
  }
}

Lambda must return this exact shape:

Python
def handler(event, context):
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": '{"id": "CLN-001", "name": "Sunrise Eye Care"}'
    }

Terraform: HTTP API Setup

HCL
# api.tf

resource "aws_apigatewayv2_api" "portal" {
  name          = "portal-api-${var.env}"
  protocol_type = "HTTP"

  cors_configuration {
    allow_origins = ["https://portal.myapp.com"]
    allow_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
    allow_headers = ["Authorization", "Content-Type"]
    max_age       = 86400
  }
}

resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.portal.id
  name        = "$default"
  auto_deploy = true

  default_route_settings {
    throttling_rate_limit  = 100   # requests per second
    throttling_burst_limit = 200
  }
}

# Route: GET /appointments
resource "aws_apigatewayv2_route" "get_appointments" {
  api_id             = aws_apigatewayv2_api.portal.id
  route_key          = "GET /appointments"
  authorization_type = "JWT"
  authorizer_id      = aws_apigatewayv2_authorizer.cognito.id
  target             = "integrations/${aws_apigatewayv2_integration.appointments.id}"
}

resource "aws_apigatewayv2_integration" "appointments" {
  api_id                 = aws_apigatewayv2_api.portal.id
  integration_type       = "AWS_PROXY"
  integration_uri        = aws_lambda_function.appointments.invoke_arn
  payload_format_version = "2.0"
}

# Grant API GW permission to invoke Lambda
resource "aws_lambda_permission" "appointments_apigw" {
  statement_id  = "AllowAPIGatewayInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.appointments.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.portal.execution_arn}/*/*"
}

Authentication

Cognito JWT Authorizer (recommended)

Validates the Authorization: Bearer <token> header against your Cognito User Pool. Zero Lambda code needed:

HCL
resource "aws_apigatewayv2_authorizer" "cognito" {
  api_id           = aws_apigatewayv2_api.portal.id
  authorizer_type  = "JWT"
  name             = "cognito-authorizer"
  identity_sources = ["$request.header.Authorization"]

  jwt_configuration {
    audience = [aws_cognito_user_pool_client.portal.id]
    issuer   = "https://cognito-idp.${var.region}.amazonaws.com/${aws_cognito_user_pool.main.id}"
  }
}

API Gateway validates the JWT signature, expiry, and audience. Claims are injected into event.requestContext.authorizer.jwt.claims.

In Lambda — read the authenticated user:

Python
def get_user(event: dict) -> dict:
    claims = event["requestContext"]["authorizer"]["jwt"]["claims"]
    return {
        "user_id": claims["sub"],
        "email": claims["email"],
        "groups": claims.get("cognito:groups", "").split(",")
    }

def handler(event, context):
    user = get_user(event)
    # user["user_id"] is verified — no need to trust client-provided IDs

Lambda Authorizer (custom logic)

For API keys, internal service tokens, or custom auth schemes:

Python
# authorizer_lambda/handler.py

def handler(event, context):
    token = event["identitySource"]  # e.g. header value
    
    try:
        payload = verify_custom_token(token)
        return {
            "isAuthorized": True,
            "context": {"clinic_id": payload["clinic_id"]}
        }
    except Exception:
        return {"isAuthorized": False}

CORS in Practice

CORS is required when your React frontend (e.g., https://portal.clinic.com) calls an API on a different domain (https://api.clinic.com).

For HTTP API, configure it in the API resource (shown above). API Gateway handles the OPTIONS preflight automatically.

Manual CORS headers (fallback, if needed in Lambda):

Python
CORS_HEADERS = {
    "Access-Control-Allow-Origin": "https://portal.clinic.com",
    "Access-Control-Allow-Headers": "Authorization, Content-Type",
    "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
}

def response(status: int, body: dict) -> dict:
    return {
        "statusCode": status,
        "headers": {"Content-Type": "application/json", **CORS_HEADERS},
        "body": json.dumps(body, default=str)
    }

Request Validation (REST API)

For REST APIs, API Gateway can validate request bodies and query params before they hit Lambda — rejecting bad requests early:

HCL
resource "aws_api_gateway_request_validator" "body" {
  rest_api_id           = aws_api_gateway_rest_api.portal.id
  name                  = "body-validator"
  validate_request_body = true
}

resource "aws_api_gateway_method" "post_appointment" {
  rest_api_id          = aws_api_gateway_rest_api.portal.id
  resource_id          = aws_api_gateway_resource.appointments.id
  http_method          = "POST"
  authorization        = "COGNITO_USER_POOLS"
  request_validator_id = aws_api_gateway_request_validator.body.id

  request_models = {
    "application/json" = aws_api_gateway_model.appointment.name
  }
}

The model is a JSON Schema:

JSON
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "required": ["clinic_id", "patient_id", "datetime"],
  "properties": {
    "clinic_id":  { "type": "string" },
    "patient_id": { "type": "string" },
    "datetime":   { "type": "string", "format": "date-time" }
  }
}

Invalid requests get a 400 Bad Request response automatically — your Lambda never executes.


Throttling & Usage Plans

Protect your backend from traffic spikes and abusive clients.

Stage-level throttling (applies to all routes):

HCL
resource "aws_apigatewayv2_stage" "default" {
  api_id = aws_apigatewayv2_api.portal.id
  name   = "$default"

  default_route_settings {
    throttling_rate_limit  = 500   # steady-state RPS
    throttling_burst_limit = 1000  # burst capacity
  }
}

Per-route throttling (different limits per endpoint):

HCL
resource "aws_apigatewayv2_stage" "default" {
  route_settings {
    route_key              = "POST /calls/transcribe"
    throttling_rate_limit  = 10    # AI endpoint — more expensive
    throttling_burst_limit = 20
  }
}

Usage Plans + API Keys (for external partners / B2B):

HCL
resource "aws_api_gateway_usage_plan" "partner" {
  name = "partner-plan"

  throttle_settings {
    rate_limit  = 100
    burst_limit = 200
  }

  quota_settings {
    limit  = 10000
    period = "MONTH"
  }
}

resource "aws_api_gateway_api_key" "partner_key" {
  name = "partner-clinic-key"
}

resource "aws_api_gateway_usage_plan_key" "partner" {
  key_id        = aws_api_gateway_api_key.partner_key.id
  key_type      = "API_KEY"
  usage_plan_id = aws_api_gateway_usage_plan.partner.id
}

Multi-Stage Deployments

dev.api.clinic.com   → API GW stage: dev   → Lambda alias: dev
staging.api.clinic.com → stage: staging    → Lambda alias: staging
api.clinic.com       → stage: prod         → Lambda alias: prod
HCL
# Lambda aliases point to specific versions
resource "aws_lambda_alias" "live" {
  name             = var.env   # "dev", "staging", "prod"
  function_name    = aws_lambda_function.portal_api.function_name
  function_version = aws_lambda_function.portal_api.version
}

# Custom domain per stage
resource "aws_apigatewayv2_domain_name" "portal" {
  domain_name = "${var.env == "prod" ? "api" : var.env}.clinic.com"

  domain_name_configuration {
    certificate_arn = aws_acm_certificate.portal.arn
    endpoint_type   = "REGIONAL"
    security_policy = "TLS_1_2"
  }
}

Debugging Tips

| Problem | How to diagnose | |---------|----------------| | 403 on all requests | Check authorizer config — Cognito audience/issuer mismatch | | 502 Bad Gateway | Lambda is crashing — check CloudWatch logs | | CORS errors in browser | OPTIONS preflight failing — check allow_origins | | Timeout errors | Lambda timeout must be less than API GW timeout (API GW max = 29s) | | 429 Too Many Requests | Hit throttle limit — increase or add exponential backoff |

Enable access logs:

HCL
resource "aws_apigatewayv2_stage" "default" {
  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.apigw.arn
  }
}

Log format JSON for CloudWatch Insights querying:

JSON
{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","routeKey":"$context.routeKey","status":"$context.status","responseLength":"$context.responseLength","integrationError":"$context.integrationErrorMessage"}

REST API Knowledge Check

5 questions Ā· Test what you just learned Ā· Instant explanations

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.