AWS Lesson 45 of 123

Amazon API Gateway, In Depth: REST vs HTTP vs WebSocket APIs, Integrations & Authorizers

In a nutshell

Think of API Gateway as the receptionist and security desk for your backend. Every visitor — an HTTP or WebSocket request — walks up to one front door. The receptionist checks their ID (authorization), makes sure nobody storms in faster than the building allows (throttling), optionally rewrites their paperwork into the format the back office expects (mapping), walks the request to the right department (integration — a Lambda function, an HTTP service, another AWS service, or a private server inside your VPC), and hands the reply back out front. You never hire, house, or patch the receptionist; AWS runs the desk and it scales from zero to millions of visitors on its own.

The one idea that unlocks everything else: there are three different front doors wearing the same uniform. A REST API is the deluxe desk with every feature — and the highest price. An HTTP API is the fast, cheap express lane for the common case: take a request, proxy it to Lambda, done. A WebSocket API keeps the door propped open so the building can call you back — chat, live dashboards, notifications. Almost every “API Gateway is confusing” moment is really just not knowing which of the three you are standing at.

If you are brand new, hold onto three sentences and the rest is detail hung on them. Nothing you configure is live until you deploy it to a stage — a named copy of the API such as dev or prod. An API key is a name tag, not a lock — it meters and rate-limits a caller, it does not prove who they are. And a proxy integration means your code owns the whole request and the whole response shape — get that shape wrong and the caller sees a 502. Everything below hangs off those three hooks.

Level: Intermediate · Time: ~50 min · Prerequisites: an AWS account, AWS CLI v2, and a working grasp of IAM (see the AWS Lambda deep dive — Lambda is the most common backend). After this you can pick the right API type on sight, wire any integration, secure a route with the correct authorizer, and reason about throttling, caching, cost, private access, and the deploy-to-stage lifecycle like an operator.

Amazon API Gateway is the front door for your APIs on AWS. It is a fully managed service that takes an HTTP (or WebSocket) request from the internet, authenticates and authorises it, throttles it, optionally transforms it, routes it to a backend — a Lambda function, an HTTP service, another AWS service, a private VPC resource — and shapes the response on the way back out. You never run a server, patch a reverse proxy, or wire up your own rate limiter; you describe routes and integrations, and API Gateway handles the undifferentiated heavy lifting of being an API front end at scale.

That convenience hides a service with real depth, and most of the confusion engineers carry about API Gateway comes from not knowing which of its three distinct API types they are using. There is the original REST API (the most feature-complete and the most expensive), the newer HTTP API (cheaper, faster, deliberately simpler), and the WebSocket API (for stateful, bidirectional, real-time connections). They share a name and a console but differ in their feature sets, their pricing, their authorizers, and even the request shape your backend sees. Pick the wrong one and you will either pay for features you do not use or discover, mid-project, that the type you chose cannot do request validation or AWS-service integrations.

This lesson is the exhaustive version. We will fix the core concepts, then compare the three API types in a single table you can return to, then walk every integration type, then stages, deployments, stage variables, and canary releases, then all four authorizer types, then request/response mapping and validation, throttling and usage plans and API keys, caching, CORS, custom domains, and finally logging and metrics. By the end you will be able to design and operate a production API on AWS and answer the questions every certification exam and interviewer asks about it.

Learning objectives

By the end of this lesson you will be able to:

Prerequisites & where this fits

You need an AWS account, the AWS CLI v2 configured (aws configure), and a working grasp of IAM — every authorizer, every AWS-service integration, and the SigV4 auth option are gated by IAM. It helps to have read the companion AWS Lambda deep dive, because Lambda is the most common API Gateway backend and the Lambda proxy integration is the default starting point for most serverless APIs. This is a Serverless lesson in the AWS Zero-to-Hero course, sitting between the Lambda deep dive (the compute behind your routes) and the upcoming containers lessons (aws-ecs-ecr-fundamentals-task-definitions-services-fargate), since API Gateway also fronts containerised backends through an HTTP integration or a private VPC Link.

Core concepts: the request lifecycle and key terms

Before the settings, fix the mental model. A request arriving at API Gateway flows through a pipeline, and naming the stages makes everything else click into place.

  1. Method request — the inbound request as the client sent it: HTTP method, path, headers, query string, body. This is where authorization, request validation, and API-key checks happen first.
  2. Integration request — API Gateway maps the method request to whatever the backend expects. For a proxy integration this is a pass-through; for a non-proxy integration you transform it with a mapping template.
  3. The integration (backend) — Lambda, an HTTP endpoint, an AWS service action, a mock, or a private resource reached over VPC Link.
  4. Integration response — the backend’s raw response, optionally transformed and its status code remapped.
  5. Method response — what the client finally receives.

A handful of terms recur throughout and are worth defining once, properly.

The single most common beginner surprise is the deploy gap: you change a method, test it, and nothing happens — because edits live in the API definition but only become live when deployed to a stage. HTTP APIs soften this with auto-deploy on the $default stage; REST APIs always require an explicit deployment.

The three API types: REST vs HTTP vs WebSocket

This is the decision that shapes everything else, so make it first and make it deliberately.

Use this table as your reference.

Capability REST API HTTP API WebSocket API
Primary use Full-featured request/response APIs Lean, low-latency proxy APIs Real-time bidirectional connections
Relative cost Highest ~70% cheaper than REST Per-message + connection-minutes
Latency Slightly higher Lowest Persistent connection
Integrations Lambda (proxy/non-proxy), HTTP, AWS service, mock, VPC Link (NLB) Lambda (proxy), HTTP proxy, private (ALB/NLB/Cloud Map) Lambda, HTTP, AWS service, mock
Authorizers IAM, Cognito, Lambda (request/token), API keys IAM, JWT/OIDC, Lambda IAM, Lambda (on $connect)
Mapping templates (VTL) Yes No (parameter mapping only) Yes
Request validation Yes No No
Caching Yes No No
API keys + usage plans Yes No (throttling only) No
Endpoint types Regional, Edge-optimized, Private Regional only Regional only
AWS WAF Yes No (use CloudFront/ALB in front) No
Private integration transport VPC Link to NLB VPC Link to ALB/NLB/Cloud Map —
Auto-deploy No (explicit deployments) Yes ($default stage) No
OpenAPI import/export Yes Yes No

The practical rule of thumb: default to HTTP API for a new serverless app proxying to Lambda — it is cheaper, faster, and has built-in JWT auth and CORS. Reach for REST API when you need its exclusive features: request validation, mapping templates, API-key usage plans, caching, edge-optimized or private endpoints, AWS service integrations, or WAF. Use a WebSocket API when the interaction is genuinely real-time and bidirectional.

Integrations: every type

An integration binds a method/route to a backend. Getting the type right — and understanding proxy vs non-proxy — is where most of the day-to-day work lives.

Integration type What it connects to Proxy? Available on When to use
Lambda proxy A Lambda function; the whole request is passed through and the function shapes the whole response Yes REST, HTTP The serverless default — least config, most flexibility in code
Lambda (non-proxy) custom A Lambda function via mapping templates that transform request and response No REST When you must reshape input/output in the gateway, not the function
HTTP proxy Any public HTTP(S) endpoint, passed through Yes REST, HTTP Front an existing HTTP service or microservice
HTTP (non-proxy) custom An HTTP endpoint via mapping templates No REST Transform to/from a legacy HTTP backend
AWS service A direct AWS API action (e.g. SQS:SendMessage, DynamoDB:PutItem, StepFunctions:StartExecution) — no Lambda needed No (mapping) REST Lambda-less “service proxy” patterns; lowest cost/latency for simple writes
Mock Nothing — API Gateway returns a canned response n/a REST, WS CORS preflight responses, stubbing an API before the backend exists, health endpoints
VPC Link (private) A resource inside a VPC (private microservice) via PrivateLink Proxy or custom REST (→NLB), HTTP (→ALB/NLB/Cloud Map) Expose a private/internal backend without making it public

Proxy vs non-proxy — the distinction to internalise

This is the most-probed integration concept.

The classic Lambda-proxy gotcha: returning a bare object (e.g. {"message":"hi"}) instead of the required {"statusCode":200,"body":"{\"message\":\"hi\"}"} shape — which surfaces to the client as a 502 Bad Gateway (“Internal server error”) because API Gateway could not parse the response. With proxy integrations, the response contract is on you.

Integration timeout and other settings

Every integration has an integration timeout: default 29 seconds, and historically the maximum was also 29 s for REST/HTTP APIs (you can now request higher REST limits, but treat ~29 s as the design ceiling — APIs are for synchronous, fast calls; offload long work to Step Functions or async patterns). Other per-integration settings include content handling (passthrough/convert to binary/convert to text), payload format version (HTTP API Lambda integrations default to 2.0, which differs in event shape from the REST 1.0 format — a frequent migration trap), and TLS settings for HTTP integrations.

Stages, deployments, stage variables & canary releases

Nothing your API does is live until it is deployed to a stage — this is the operational heart of API Gateway.

Stage variables

A stage variable is a name/value pair attached to a stage that you reference at runtime as ${stageVariables.name}. They let one API definition behave differently per stage without code changes. Two high-value uses:

A worked example: set the integration URI to arn:...:function:${stageVariables.lambdaAlias} and define lambdaAlias=prod on the prod stage and lambdaAlias=dev on the dev stage. (Remember to grant API Gateway permission to invoke each aliased function.)

Canary releases

A canary release lets you shift a percentage of traffic on a stage to a new deployment while the rest stays on the current one — the gateway-level equivalent of a Lambda weighted alias.

# Create a canary on the prod stage sending 10% of traffic to a new deployment
aws apigateway update-stage \
  --rest-api-id abc123 --stage-name prod \
  --patch-operations \
    op=replace,path=/canarySettings/percentTraffic,value=10.0 \
    op=replace,path=/canarySettings/deploymentId,value=dep456

You watch the canary’s separate CloudWatch metrics, then promote (point the whole stage at the new deployment) or roll back (delete the canary). Canary settings also support stage-variable overrides and a use-stage-cache toggle so the canary can share or bypass the production cache.

Authorizers: every type

Authorization is where API Gateway earns its keep. There are four mechanisms; pick by where your identities live.

Authorizer How it works API types When to use Caching
IAM (SigV4) Caller signs the request with AWS credentials; gateway checks IAM policy (execute-api:Invoke) REST, HTTP, WS Service-to-service, internal tools, callers that already have IAM creds n/a
Cognito user pool Gateway validates a Cognito-issued JWT (ID/access token) against a user pool REST Apps with a Cognito user directory Token TTL
Lambda authorizer (custom) Your Lambda inspects the token/request and returns an IAM policy (allow/deny) + optional context REST, HTTP, WS Any custom auth (third-party OIDC, opaque tokens, header/IP rules) Yes, by token/identity (configurable TTL)
JWT authorizer Gateway natively validates an OIDC/OAuth2 JWT against an issuer + audience — no Lambda HTTP only OAuth2/OIDC providers (Cognito, Auth0, Okta, Entra ID) Built-in

IAM authorization (SigV4)

Set the method’s authorization type to AWS_IAM. The caller must sign the request with SigV4 using credentials whose IAM policy allows execute-api:Invoke on the API’s ARN. This is the natural choice for service-to-service calls and internal tooling, and it composes with resource policies on the API to allow specific accounts, VPCs (aws:SourceVpc), or IP ranges. No tokens to manage — the AWS signature is the credential.

Cognito user pool authorizer (REST)

You select a Cognito user pool; clients authenticate against it and send the resulting JWT in the Authorization header. API Gateway validates the token’s signature, expiry, and (optionally) the requested scopes before the request reaches your backend. Ideal when you have already adopted Cognito for sign-up/sign-in. The token’s claims are available to your integration via $context.authorizer.claims.*.

Lambda authorizer (custom — REST, HTTP, WebSocket)

The most flexible option: a Lambda function you write receives the incoming request (or just a token) and returns an IAM policy document that allows or denies the call, plus an optional context object of key/values passed downstream. Two flavours:

Results are cached by an identity source (the token value) for a configurable TTL (default 300 s, set to 0 to disable) — crucial for performance and cost, since otherwise the authorizer runs on every call. A return of Deny yields 403 Forbidden; an authorizer error or an unrecognised token yields 401 Unauthorized. Use a Lambda authorizer for third-party OIDC, opaque/reference tokens, HMAC-signed webhooks, IP allow-lists, or any bespoke rule.

JWT authorizer (HTTP APIs only)

HTTP APIs include a native JWT authorizer — no Lambda required. You configure the issuer URL (the OIDC provider’s /.well-known/openid-configuration) and the allowed audience(s); API Gateway validates the token’s signature (via the issuer’s JWKS), exp/nbf, issuer, and audience, and can enforce scopes per route. This is the cleanest, cheapest way to protect an HTTP API with an OAuth2/OIDC provider (Cognito, Auth0, Okta, Microsoft Entra ID). Claims are exposed as $context.authorizer.jwt.claims.*.

A note on resource policies (REST): independent of the authorizer, a REST API can carry a resource policy that allows or denies invocation based on source IP, source VPC/VPC endpoint, or AWS account/principal — the mechanism behind private APIs and account-restricted APIs. Authorizer (who you are) and resource policy (where the call may come from) are evaluated together.

Request & response mapping and validation (REST)

These are REST-API exclusives and a major reason to choose REST over HTTP.

Mapping templates (VTL)

In a non-proxy integration, mapping templates written in Velocity Template Language (VTL) transform the request before it hits the backend and the response before it returns to the client. You can pull values from the path ($input.params('id')), the body ($input.path('$.field') / $util.parseJson(...)), headers, query string, stage variables, and the request context ($context.identity.sourceIp, $context.requestId). They are selected by Content-Type (e.g. a template for application/json).

A minimal request template that forwards a renamed field and injects the caller’s IP:

{
  "orderId": "$input.params('id')",
  "payload": $input.json('$.body'),
  "sourceIp": "$context.identity.sourceIp"
}

Integration responses then map backend status codes to method-response status codes (e.g. a backend “ITEM_NOT_FOUND” → HTTP 404) using regex selection patterns on the error, and a response template reshapes the body. This is how you keep a clean public contract over a messy backend without writing transformation code in the function.

Request validation

API Gateway can reject invalid requests at the edge — before they cost you a Lambda invocation — by validating against a model (a JSON Schema attached to the API) and/or required parameters. A request validator checks one or both of:

Invalid requests get a 400 Bad Request without ever invoking the backend. This is a cheap, high-leverage protection — turn it on for any write endpoint with a known payload shape.

Throttling, usage plans & API keys

API Gateway protects your backend (and your bill) with layered rate limiting.

Throttling levels

Throttling uses a token-bucket model with a steady-state rate (requests/second) and a burst (bucket size for momentary spikes). It applies at several levels, evaluated from broadest to narrowest:

Level Scope Notes
Account All APIs in the Region A default 10,000 req/s rate and 5,000 burst per Region (soft limit, raisable)
Stage A whole stage Default stage-level rate/burst limits
Method / route A single method or route Override limits for a hot or sensitive endpoint
Usage plan (per key) A specific API key Per-client rate/burst and quota (see below)

When a request exceeds a limit, API Gateway returns 429 Too Many Requests with a Retry-After hint — clients should back off and retry.

API keys and usage plans (REST)

An API key is a token a client sends in the x-api-key header to identify itself. Crucially, an API key is not authentication — it identifies a caller for metering and throttling, not for proving identity (use an authorizer for that). To enforce limits, you attach keys to a usage plan:

This is the standard pattern for tiered API products (free vs paid tiers with different limits) and for giving each partner their own quota. HTTP APIs do not support API-key usage plans — only route/stage throttling — which is one reason a metered, multi-tenant public API often stays on REST.

Caching (REST)

A REST API stage can enable a dedicated cache that stores integration responses keyed by the request, so repeated identical reads return instantly without hitting the backend.

Setting What it does Detail
Cache capacity Size of the cache 0.5 GB up to 237 GB — larger holds more entries
Default TTL How long entries live 300 s default; 0 disables; max 3600 s
Per-method override Enable/disable or change TTL per method Cache only safe, cacheable reads
Cache key parameters Which path/query/header params form the key Without these, all calls to a method share one entry
Encrypt cache data Encrypt cached responses at rest Turn on for sensitive payloads
Cache invalidation Clients with Cache-Control: max-age=0 can bust the cache Gate this with the InvalidateCache IAM permission so only authorised callers can flush

Caching cuts both latency and backend cost for read-heavy endpoints, but it is not free — you pay an hourly rate for the cache capacity regardless of hit rate, so size it to the working set and only cache idempotent reads. HTTP and WebSocket APIs have no built-in cache (put CloudFront in front if you need edge caching).

CORS (Cross-Origin Resource Sharing)

If a browser on https://app.example.com calls your API on a different origin, the browser enforces CORS: for “non-simple” requests it first sends a preflight OPTIONS request, and your API must answer with the right Access-Control-Allow-* headers or the browser blocks the call.

The two evergreen CORS gotchas: (1) with a Lambda proxy integration on a REST API, the gateway’s “Enable CORS” only handles the OPTIONS preflight — your function still has to add Access-Control-Allow-Origin to the real response; and (2) a 5xx error never carries CORS headers, so a backend bug masquerades as a “CORS error” in the browser console. Check the actual status before blaming CORS.

Custom domains & TLS

Out of the box your API lives at an ugly https://{api-id}.execute-api... URL. A custom domain name maps a friendly name (api.example.com) to one or more APIs and stages.

Logging, metrics & tracing

Operability is configured per stage.

API Gateway architecture

The diagram below ties the pieces together: clients on the left hit the API’s edge, where authorization, throttling, and (for REST) request validation and caching sit; the request then flows through the integration to a backend — Lambda, an HTTP service, an AWS service, or a private VPC resource over VPC Link — and the response shapes on the way back out per stage.

Amazon API Gateway deep dive

Trace one path through it. A browser calls GET /orders on the prod stage; the JWT authorizer validates the token; the stage throttle admits the request; a cache miss falls through to a Lambda proxy integration that returns the orders; API Gateway caches the response and returns it with the right CORS header. Now contrast that with a partner calling POST /events with an API key: the usage plan meters and rate-limits the key, request validation rejects a malformed body with a 400 before any backend runs, and a valid body goes straight to an AWS-service integration that pushes to SQS — no Lambda at all.

Hands-on lab

You will build a tiny HTTP API fronting a Lambda function, deploy it, call it, then add a JWT-style consideration and clean up. Everything here is within the AWS Free Tier — API Gateway’s free tier includes 1 million HTTP-API calls per month for 12 months, and the Lambda calls fall under Lambda’s free tier.

Run these as an administrator (not the root user). Replace the account ID 111122223333 with your own and use AWS CLI v2.

Step 1 — Create an execution role and a trivial Lambda backend.

cat > trust.json <<'EOF'
{ "Version": "2012-10-17",
  "Statement": [{ "Effect": "Allow",
    "Principal": { "Service": "lambda.amazonaws.com" },
    "Action": "sts:AssumeRole" }] }
EOF

aws iam create-role --role-name lab-apigw-role \
  --assume-role-policy-document file://trust.json

aws iam attach-role-policy --role-name lab-apigw-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

cat > app.py <<'EOF'
import json
def handler(event, context):
    # HTTP API payload format 2.0: method/path live under requestContext.http
    name = (event.get("queryStringParameters") or {}).get("name", "world")
    return {
        "statusCode": 200,
        "headers": {"content-type": "application/json"},
        "body": json.dumps({"message": f"hello, {name}"})
    }
EOF
zip function.zip app.py

aws lambda create-function --function-name lab-apigw-fn \
  --runtime python3.13 --handler app.handler --architectures arm64 \
  --role arn:aws:iam::111122223333:role/lab-apigw-role \
  --zip-file fileb://function.zip

Step 2 — Create an HTTP API with a Lambda proxy integration (one command).

# The quick-create form wires a $default route + integration + auto-deployed $default stage
aws apigatewayv2 create-api \
  --name lab-http-api \
  --protocol-type HTTP \
  --target arn:aws:lambda:us-east-1:111122223333:function:lab-apigw-fn

Note the returned ApiId and ApiEndpoint.

Step 3 — Grant API Gateway permission to invoke the function.

aws lambda add-permission --function-name lab-apigw-fn \
  --statement-id apigw-invoke --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:us-east-1:111122223333:<ApiId>/*/*"

Step 4 — Call the API.

curl "https://<ApiId>.execute-api.us-east-1.amazonaws.com/?name=Vinod"

Expected output: {"message": "hello, Vinod"}. Because HTTP APIs auto-deploy to $default, there was no separate deployment step — contrast this with a REST API, which would need aws apigateway create-deployment.

Step 5 — Add stage-level throttling and turn on access logs (post-creation config).

# Lower the default-stage throttle to prove it works (1 req/s, burst 2)
aws apigatewayv2 update-stage --api-id <ApiId> --stage-name '$default' \
  --default-route-settings ThrottlingRateLimit=1,ThrottlingBurstLimit=2

# Hammer it to see 429s appear
for i in $(seq 1 10); do \
  curl -s -o /dev/null -w "%{http_code}\n" \
  "https://<ApiId>.execute-api.us-east-1.amazonaws.com/?name=x"; done

Expected output: a mix of 200 and 429 — the throttle is rejecting the burst, exactly as designed.

Step 6 — Validation checklist.

Cleanup (so nothing lingers):

aws apigatewayv2 delete-api --api-id <ApiId>
aws lambda delete-function --function-name lab-apigw-fn
aws iam detach-role-policy --role-name lab-apigw-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name lab-apigw-role
rm -f app.py function.zip trust.json

Cost note: at this scale the lab is effectively free — a handful of HTTP-API calls and Lambda invocations sit comfortably inside both free tiers. The two ways an API Gateway setup actually costs money are (1) leaving a REST API cache enabled (it bills per hour for the provisioned capacity regardless of traffic) and (2) high request volume on a REST API (~3.5× the price of HTTP) — neither of which this lab creates. Deleting the API and function removes any footprint.

Going deeper

The sections above are enough to design and run an API. This one is for the reader who wants the internals — the exact shapes, the JSON, the gotchas that only bite in production, and the decisions that sit one level above “REST or HTTP”.

The two payload formats (1.0 vs 2.0) — the copy-paste trap

An HTTP API and a REST API do not hand your Lambda the same event. REST proxy integrations use payload format version 1.0; HTTP API Lambda integrations default to 2.0. The differences are small, silent, and exactly the kind that make a function that “worked in the other API” throw KeyError in this one.

Format 1.0 (REST proxy) Format 2.0 (HTTP API default)
HTTP method event.httpMethod event.requestContext.http.method
Path event.path event.rawPath (+ event.routeKey)
Multi-value headers event.multiValueHeaders event.headers (comma-joined)
Multi-value query event.multiValueQueryStringParameters event.queryStringParameters (comma-joined)
Cookies inside headers dedicated event.cookies array
Source IP event.requestContext.identity.sourceIp event.requestContext.http.sourceIp

The subtler half is the response contract. With format 1.0 (and therefore all REST proxy integrations), your function must return the full object — { "statusCode": 200, "headers": {...}, "body": "<string>" } — or API Gateway cannot build a response and the caller gets a 502. With format 2.0, API Gateway is more forgiving: if the function returns a value without a statusCode, the gateway assumes 200, treats the return value as the JSON body, and sets content-type: application/json. So the famous “bare object → 502” gotcha is a 1.0/REST behaviour, not a universal law — but relying on the 2.0 shortcut makes a function non-portable, so returning the explicit object everywhere is the safer habit. You can also force payloadFormatVersion=1.0 on an HTTP API integration to reuse a REST-shaped handler unchanged.

Lambda authorizer internals — the actual JSON and the caching trap

A Lambda authorizer does not return “allow” or “deny” — it returns an IAM policy document that API Gateway then evaluates. For a REST (or HTTP with full response) authorizer the shape is:

{
  "principalId": "user-42",
  "policyDocument": {
    "Version": "2012-10-17",
    "Statement": [{
      "Action": "execute-api:Invoke",
      "Effect": "Allow",
      "Resource": "arn:aws:execute-api:us-east-1:123456789012:abc123def/prod/GET/orders"
    }]
  },
  "context": { "tenant": "acme", "role": "reader" },
  "usageIdentifierKey": "optional-api-key-for-metering"
}

context values flow to the backend as $context.authorizer.<key> (proxy integrations see them under event.requestContext.authorizer). The caching trap is the part that surprises people: when the authorizer’s result TTL is greater than 0, API Gateway caches the returned policy keyed by the identity source (the token). If your policy names one specific method ARN and the same token is then used against a different method, the cached policy is reused and may not match — producing a confusing 403. The fix is to return a wildcard resource (arn:...:abc123def/prod/*/*) when caching is on, or narrow the cache/TTL deliberately. HTTP APIs add a lighter option: set enableSimpleResponses = true (with authorizer payload format 2.0) and return just { "isAuthorized": true, "context": {...} } — no policy document at all.

A Deny result surfaces as 403 Forbidden; an authorizer that errors or returns an unparseable result yields 401 Unauthorized. Both are worth alarming on separately.

WebSocket APIs, end to end

The lesson introduced route keys; here is the whole loop. A WebSocket API has three reserved routes — $connect (the handshake, where you authorize and can reject with a non-200), $disconnect (best-effort cleanup, not guaranteed to fire), and $default (the catch-all for messages that match no custom route) — plus any custom routes you define (sendMessage, join, …). Which route a message hits is decided by the route selection expression, $request.body.action by default: a client sends {"action":"sendMessage","data":"hi"} and the sendMessage route runs. Authorization happens once, on $connect (IAM or a Lambda REQUEST authorizer) — there is no per-message authorizer, so long-lived sessions need their own token-refresh discipline.

The piece beginners miss is how the server pushes back. You do not “return” a message to the client from your Lambda; you call the @connections callback API against the connection you want to reach:

# Push a payload to one connected client (needs execute-api:ManageConnections on the ARN)
aws apigatewaymanagementapi post-to-connection \
  --connection-id "$CONNECTION_ID" \
  --data '{"type":"tick","price":42}' \
  --endpoint-url "https://abc123def.execute-api.us-east-1.amazonaws.com/prod"

The --endpoint-url is the API’s own stage URL, not the default apigatewaymanagementapi endpoint — a very common first mistake. Store each connectionId (from event.requestContext.connectionId on $connect) in something like DynamoDB, fan out by iterating stored IDs, and treat a 410 Gone from post-to-connection as “this client vanished — delete the stored ID.” The backend role needs execute-api:ManageConnections on arn:aws:execute-api:<region>:<acct>:<api-id>/<stage>/POST/@connections/*.

Private REST APIs and resource policies — the network gate in JSON

An authorizer answers “who are you?”; a resource policy answers “where may the call come from?” — and they are ANDed together. A private API is the headline use: set the endpoint type to PRIVATE, create an interface VPC endpoint for the com.amazonaws.<region>.execute-api service (with private DNS enabled), and attach a resource policy that refuses anything not arriving through your VPC endpoint:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Deny",
    "Principal": "*",
    "Action": "execute-api:Invoke",
    "Resource": "arn:aws:execute-api:us-east-1:123456789012:abc123def/*",
    "Condition": { "StringNotEquals": { "aws:SourceVpce": "vpce-0abc123def456" } }
  }]
}

The same mechanism, with aws:SourceIp or aws:PrincipalOrgID or an account list, is how you build IP-allow-listed or org-only public APIs. Two facts save hours: a resource policy change on a REST API needs a redeployment to take effect, and an explicit Deny here beats any Allow from an authorizer — the network gate wins.

REST vs HTTP vs AppSync vs ALB — the decision one level up

Not every “API in front of Lambda” should be API Gateway at all. Widen the choice:

REST API HTTP API AWS AppSync ALB → Lambda
Style REST, full-featured REST, lean Managed GraphQL HTTP routing rules
Best at API products (keys, quotas, validation, caching, WAF, private/edge) Cheap, fast Lambda/HTTP proxy with JWT/CORS Client-shaped queries, real-time subscriptions, stitching many data sources Simple path/host routing at high volume, mixed EC2/ECS/Lambda targets
Auth IAM, Cognito, Lambda, keys IAM, JWT/OIDC, Lambda API key, IAM, Cognito, OIDC, Lambda Offloaded (Cognito on ALB / OIDC / your own)
Per-request pricing Highest ~70% cheaper Per query/resolver + real-time LCU-based, no per-request gateway fee
Reach for it when You need validation, mapping, usage plans, caching, edge/private, or WAF-on-the-API Most new serverless HTTP apps The frontend wants exactly the fields it asks for, plus live updates You already run an ALB and want cheap Lambda routing without API-management features

The trap is reaching for REST’s power on an app that only needs HTTP’s speed, or bolting request-shaping onto REST when the frontend really wanted GraphQL (AppSync) or when a plain ALB with a Lambda target would route the traffic for a fraction of the per-request cost — at the price of losing usage plans, validation, native JWT, and caching.

Choosing an endpoint type — edge, regional, or private

The three endpoint types are not a quality ladder; they answer where the API is reachable, and each has a real cost. Edge-optimized (REST only) fronts the API with CloudFront POPs so the TLS handshake and first byte happen close to a globally scattered audience — but it adds a CloudFront hop, forces the ACM certificate into us-east-1, and does nothing for callers already in your Region. Regional is the modern default: lowest overhead for in-Region clients, and the right base when you want to put your own CloudFront or WAF in front and control the cache behaviours yourself — it is also the only way to get edge behaviour for an HTTP API, which has no edge-optimized mode of its own. Private (REST only) publishes the API exclusively inside your VPC through an interface endpoint plus a resource policy, with no public surface at all. Rule of thumb: regional by default; edge only when a genuinely global browser audience measurably benefits and you accept the us-east-1 cert quirk; private whenever the only legitimate caller is another internal service. Switching an existing REST API between types is a real change — it alters the hostname and, for private, the whole reachability model — so decide before you hand out the URL.

Direct AWS-service integrations — a Lambda-less write

REST APIs can call an AWS API directly, with no function in between — the cheapest, lowest-latency path for a simple write. The integration request is a non-proxy mapping template that builds the target service’s request, and the integration runs under an IAM role you attach as its credentials. Pushing a message straight to SQS on POST /events needs three things: integration type AWS, a request template, and a Content-Type: application/x-www-form-urlencoded integration header:

## Integration request template -> SQS SendMessage (no Lambda)
Action=SendMessage&MessageBody=$util.urlEncode($input.body)

The integration URI points at the queue (arn:aws:apigateway:us-east-1:sqs:path/123456789012/orders-queue) and the integration credentials are a role whose policy allows exactly sqs:SendMessage on that one queue — least privilege, no compute to patch or scale. The identical shape does dynamodb:PutItem, states:StartExecution, or kinesis:PutRecord. The trade-off: every bit of request/response shaping lives in VTL, which is powerful but awkward to unit-test, so reserve service integrations for genuinely simple, high-volume paths and reach for Lambda the moment the logic branches.

Reading the two latencies (and a real access-log format)

The single most useful operability habit is watching Latency (end-to-end — gateway plus backend) against IntegrationLatency (backend only). If the two track together, your backend is the slow part; if Latency sits well above IntegrationLatency, the gateway layer is adding time — a slow Lambda authorizer, a heavy mapping template, or TLS negotiation. Make that queryable with a structured access log; a production-friendly JSON format string on the stage:

{ "requestId":"$context.requestId", "ip":"$context.identity.sourceIp",
  "method":"$context.httpMethod", "path":"$context.resourcePath",
  "status":"$context.status", "latency":"$context.responseLatency",
  "integLatency":"$context.integrationLatency",
  "principal":"$context.authorizer.principalId", "waf":"$context.wafResponseCode" }

Ship that to CloudWatch Logs and you can slice p99 latency, 4XX/5XX rates, and authorizer failures per route with Logs Insights — without turning on execution logs, which can capture full request/response bodies and belong off (or scrubbed) in production. When a call fails at the gateway itself, $context.error.message and $context.error.responseType name the reason.

Economics, quotas, and mTLS — the operational fine print

The cost gap is not a rounding error. Representative US-region list prices (always verify current pricing — these change): REST ≈ $3.50 per million requests; HTTP ≈ $1.00 per million (the ~70% saving); WebSocket ≈ $1.00 per million messages plus $0.25 per million connection-minutes; the optional REST cache bills per hour by capacity (roughly $0.02/hr at 0.5 GB up to several dollars/hr at 237 GB) regardless of hit rate; data transfer out is extra. The two ways a bill quietly balloons are an idle cache left switched on and high volume left on REST when HTTP would do.

A few quotas worth committing to memory (most are soft and raisable via Service Quotas): the default account throttle of 10,000 req/s steady, 5,000 burst per Region; the 29 s integration timeout design ceiling (higher REST/HTTP limits are now grantable on request, but design for fast synchronous calls and offload long work to Step Functions); a 10 MB REST payload limit; and roughly 300 resources/routes and 10 stages per API by default. Finally, mutual TLS (mTLS) for partner APIs: you upload a truststore — a PEM bundle of the CA certificates you trust — to S3 and point the custom domain’s mutualTlsAuthentication.truststoreUri (with an optional truststoreVersion) at it. mTLS is available only on regional custom domains, and the verified client-certificate fields show up as $context.identity.clientCert.* in access logs and to Lambda authorizers, so you can make fine-grained decisions on the presented cert.

Practice challenges

Work these in order — they climb from “make it respond” to “lock it to a VPC.” Each has a worked solution; try it before you open it. Account IDs and API IDs are placeholders.

1 (Beginner) — Fix the 502. A Lambda behind a REST proxy integration returns return {"message": "hi"} and every call gives 502 Bad Gateway. Change the function so the caller gets a real 200 with that JSON.

<details><summary>Solution</summary>

def handler(event, context):
    import json
    return {
        "statusCode": 200,
        "headers": {"content-type": "application/json"},
        "body": json.dumps({"message": "hi"})
    }

Why: a REST proxy integration (payload format 1.0) requires the full {statusCode, headers, body} object with body as a string; a bare object cannot be parsed, so the gateway emits a 502. </details>

2 (Beginner) — Make an edit go live. You changed a method on a REST API and tested the stage URL, but it still serves the old behaviour. Push the change to the prod stage from the CLI.

<details><summary>Solution</summary>

aws apigateway create-deployment \
  --rest-api-id abc123def --stage-name prod \
  --description "Ship method change"

Why: REST API edits live in the API definition and only become live when deployed to a stage — HTTP APIs auto-deploy to $default, REST APIs never do. </details>

3 (Intermediate) — One integration, per-stage backends. Make prod invoke orders:prod and dev invoke orders:dev without duplicating the integration.

<details><summary>Solution</summary>

Set the integration URI to reference a stage variable, then define the variable per stage:

# Integration URI uses the variable:
#   arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/
#   arn:aws:lambda:us-east-1:123456789012:function:orders:${stageVariables.alias}/invocations
aws apigateway update-stage --rest-api-id abc123def --stage-name prod \
  --patch-operations op=replace,path=/variables/alias,value=prod
# ...and value=dev on the dev stage. Grant invoke on each alias:
aws lambda add-permission --function-name orders:prod --qualifier prod \
  --statement-id apigw-prod --action lambda:InvokeFunction \
  --principal apigateway.amazonaws.com \
  --source-arn "arn:aws:execute-api:us-east-1:123456789012:abc123def/*/*/*"

Why: a stage variable (${stageVariables.alias}) parameterises one API definition per stage; remember each aliased function needs its own invoke permission. </details>

4 (Intermediate) — Reject bad input before Lambda runs. On a REST POST /events, make a body missing the required type field return 400 without ever invoking the backend.

<details><summary>Solution</summary>

# 1) A JSON Schema model
aws apigateway create-model --rest-api-id abc123def --name EventModel \
  --content-type application/json --schema '{
    "$schema":"http://json-schema.org/draft-04/schema#","type":"object",
    "required":["type"],"properties":{"type":{"type":"string"}}}'
# 2) A validator that checks the body
aws apigateway create-request-validator --rest-api-id abc123def \
  --name body-only --validate-request-body
# 3) Attach both to the method (validator id + model from the calls above)
aws apigateway update-method --rest-api-id abc123def \
  --resource-id r1a2b3 --http-method POST \
  --patch-operations \
    op=replace,path=/requestValidatorId,value=val123 \
    op=replace,path=/requestModels/application~1json,value=EventModel

Why: attaching a model + request validator makes API Gateway reject a malformed body with 400 at the edge — no Lambda invocation, no cost. (~1 is the JSON-Pointer escape for the / in application/json.) </details>

5 (Advanced) — Push to a WebSocket client. From a Lambda, send {"type":"tick"} to a connected client whose connectionId you stored on $connect. Name the IAM permission it needs and the one error you must handle.

<details><summary>Solution</summary>

import boto3
def push(connection_id, api_id, region, stage):
    client = boto3.client("apigatewaymanagementapi",
        endpoint_url=f"https://{api_id}.execute-api.{region}.amazonaws.com/{stage}")
    try:
        client.post_to_connection(ConnectionId=connection_id,
                                  Data=b'{"type":"tick"}')
    except client.exceptions.GoneException:
        pass  # 410 -> client disconnected; delete the stored connectionId

Why: server push uses the @connections callback against the stage endpoint URL; the role needs execute-api:ManageConnections, and a 410 Gone means the connection is dead and its stored ID should be pruned. </details>

6 (Advanced) — Lock a REST API to one VPC. Make an API reachable only through the interface VPC endpoint vpce-0abc123def456, and nowhere else.

<details><summary>Solution</summary>

# 1) Endpoint type PRIVATE (on create or via patch), then attach the policy:
aws apigateway update-rest-api --rest-api-id abc123def --patch-operations \
  op=replace,path=/policy,value='{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Principal":"*","Action":"execute-api:Invoke","Resource":"arn:aws:execute-api:us-east-1:123456789012:abc123def/*","Condition":{"StringNotEquals":{"aws:SourceVpce":"vpce-0abc123def456"}}}]}'
aws apigateway create-deployment --rest-api-id abc123def --stage-name prod

Plus: create the com.amazonaws.us-east-1.execute-api interface endpoint with private DNS enabled.

Why: a resource policy keyed on aws:SourceVpce is the network gate for private APIs; it is ANDed with the authorizer, an explicit Deny wins, and the change needs a redeploy to take effect. </details>

Common mistakes & troubleshooting

Symptom Likely cause Fix
Edits to the API have no effect Not deployed to the stage (REST), or testing the wrong stage URL create-deployment to the stage; for HTTP APIs confirm auto-deploy/$default
502 Bad Gateway / “Internal server error” Lambda proxy returned the wrong shape (not {statusCode, body}) or threw Return the required proxy response object; check execution logs / function logs
403 Missing Authentication Token Wrong path/method, or the route doesn’t exist Verify the resource/route and method; check the full stage-prefixed URL
401/403 on every call Authorizer denying, token expired, or wrong issuer/audience (JWT) Inspect the authorizer config and token claims; check authorizer cache TTL
“CORS error” in the browser Missing Access-Control-Allow-Origin on the real response, or a 5xx (errors carry no CORS headers) Add the header in the Lambda (proxy) or method response; fix the underlying 5xx first
429 Too Many Requests Hit a stage/method/usage-plan throttle or the account limit Raise the relevant throttle/quota; have clients honour Retry-After
HTTP-API Lambda gets an unexpected event shape Payload format 2.0 differs from REST’s 1.0 (method/path under requestContext.http) Code to the 2.0 shape, or pin the integration to format 1.0
Private/VPC backend returns errors via API Misconfigured VPC Link / target group / security group Check the VPC Link, the NLB/ALB target health, and SG rules to the backend
Custom domain TLS fails on an edge-optimized API ACM certificate not in us-east-1 Issue/import the cert in us-east-1 (edge) or the API’s Region (regional)

Common beginner mistakes

These are misconceptions, not error messages — the wrong mental model that leads you to build the wrong thing. (The symptom→cause→fix table above is for when something is already broken.)

Best practices

Security notes

Interview & exam questions

  1. When would you choose an HTTP API over a REST API, and vice versa? Choose HTTP API for cost (~70% cheaper) and low latency when you mostly proxy to Lambda/HTTP with JWT/IAM auth and built-in CORS. Choose REST API when you need its exclusive features: request validation, mapping templates, API-key usage plans, caching, edge-optimized or private endpoints, AWS-service integrations, or WAF.

  2. Explain the difference between a Lambda proxy and a non-proxy integration. Proxy passes the whole request to the function as one event and expects a specifically shaped response (statusCode, headers, body); your code owns parsing and response shaping, with no mapping templates. Non-proxy (custom) uses VTL mapping templates in the gateway to transform request and response — more control, more setup, and the only way to do AWS-service and mock integrations.

  3. A Lambda-backed API returns 502 Bad Gateway. Why? The proxy integration’s Lambda returned the wrong shape (a bare object instead of {statusCode, body, headers}) or threw an unhandled error, so API Gateway could not build a response. Fix the function to return the required proxy response object.

  4. Name the four authorizer options and when to use each. IAM/SigV4 (service-to-service, callers with AWS creds); Cognito user pool (REST apps with a Cognito directory); Lambda authorizer (custom logic — third-party OIDC, opaque tokens, header/IP rules; REST/HTTP/WS); JWT authorizer (HTTP-only native OIDC/OAuth2 validation against an issuer + audience, no Lambda).

  5. Is an API key authentication? How do you enforce per-client limits? No — an API key only identifies a caller for metering/throttling, not authentication. Enforce per-client limits by attaching the key to a usage plan that defines a throttle (rate + burst) and a quota, associated with API stages.

  6. Why might changes to your API not be live, and how do you fix it? Edits live in the API definition but only go live when deployed to a stage. REST APIs need an explicit deployment; HTTP APIs auto-deploy to $default. Deploy (or check you are calling the right stage URL).

  7. What is a stage variable and give a concrete use. A name/value pair on a stage referenced at runtime as ${stageVariables.x}. A common use is pointing each stage at a different Lambda alias/function (so prod → myFn:prod, dev → myFn:dev) from one integration definition, or passing per-stage config into mapping templates.

  8. How do canary releases work in API Gateway? A stage can send a configurable percentage of traffic to a new deployment while the rest stays on the current one, with separate metrics. You watch the canary, then promote (whole stage to the new deployment) or roll back (delete the canary). It is the gateway-level analogue of a weighted Lambda alias.

  9. Someone reports a “CORS error” — how do you diagnose it? First check the real status: a 5xx never carries CORS headers, so a backend bug looks like CORS. If it is genuinely CORS, ensure the preflight OPTIONS returns Access-Control-Allow-* and the actual response includes Access-Control-Allow-Origin — for a REST proxy integration that header must come from your Lambda, not just the gateway’s “Enable CORS”.

  10. How do you expose a private backend in a VPC through API Gateway? With a VPC Link / PrivateLink integration: REST APIs link to a Network Load Balancer; HTTP APIs can link to an ALB, NLB, or Cloud Map service. The backend stays private (no public endpoint) and API Gateway reaches it over the VPC link.

  11. What does the API Gateway cache cost and when should you use it? A REST stage cache bills per hour for the provisioned capacity (0.5–237 GB) regardless of hit rate, with a TTL up to 3600 s. Use it for idempotent, read-heavy endpoints where the latency/backend-cost savings outweigh the hourly charge; size it to the working set. HTTP/WebSocket APIs have no built-in cache.

  12. What is special about WebSocket APIs compared with REST/HTTP? They maintain a stateful, bidirectional connection: clients connect once ($connect), messages are routed by a route key selected from the body (e.g. $default, sendMessage), and the server can push to connected clients via the connection’s callback URL. Authorization happens on $connect (IAM or Lambda). Use them for real-time apps (chat, live dashboards, notifications).

  13. For a custom domain on an edge-optimized REST API, where must the ACM certificate live? In us-east-1 (CloudFront’s region), because edge-optimized endpoints are fronted by CloudFront. A regional custom domain instead needs the certificate in the API’s own Region.

Quick check

  1. Which API type is the cheapest and lowest-latency for a simple Lambda proxy?
  2. What response shape must a Lambda proxy integration return?
  3. True or false: an API key authenticates the caller.
  4. Which authorizer is native to HTTP APIs and needs no Lambda?
  5. Why might a freshly edited REST API still serve the old behaviour?

Answers

  1. The HTTP API — roughly 70% cheaper than REST and the lowest latency.
  2. A JSON object with at least statusCode and body (plus optional headers and isBase64Encoded); returning a bare object causes a 502.
  3. False. An API key only identifies a caller for metering/throttling; pair it with a real authorizer for authentication.
  4. The JWT authorizer — validates an OIDC/OAuth2 token against an issuer and audience.
  5. Edits are not live until deployed to the stage; REST requires an explicit deployment (HTTP APIs auto-deploy to $default).

Exercise

Build a small metered, secured REST API and exercise its signature features:

  1. Create a REST API with a POST /events resource backed by a Lambda proxy integration; attach a JSON-Schema model and a request validator so a malformed body returns a 400 without invoking Lambda.
  2. Add a Cognito (or Lambda) authorizer to require a valid token, and confirm an unauthenticated call returns 401/403.
  3. Create a usage plan with a throttle (e.g. 5 req/s, burst 10) and a small quota, generate an API key, associate both with the stage, and prove that exceeding the quota/throttle returns 429.
  4. Add a GET /events/{id} resource, enable the stage cache with cache-key parameters on {id} and a 60 s TTL, and observe CacheHitCount rise on repeated reads.
  5. Deploy a change behind a canary at 10% traffic, watch the canary metrics, then promote it. Bonus: attach a custom domain with an ACM certificate and a Route 53 alias, and turn on access logs to capture each request.

Certification mapping

Exam Objective area this supports
DVA-C02 (Developer – Associate) Development with AWS services — building REST/HTTP/WebSocket APIs, Lambda proxy vs non-proxy integrations, mapping templates, request validation, stages and stage variables, authorizers (IAM/Cognito/Lambda/JWT), API keys and usage plans; deployment with canary releases.
SAA-C03 (Solutions Architect – Associate) Design secure, resilient, cost-optimised architectures — choosing API type, securing APIs (authorizers, resource policies, WAF, private APIs/VPC Link), throttling and caching for scale and cost, and custom domains/edge vs regional vs private endpoints.
SOA-C02 (SysOps Administrator – Associate) Monitoring and reliability — CloudWatch metrics (Latency vs IntegrationLatency, 4XX/5XX), access and execution logs, throttling alarms, and stage-level operational settings.

Glossary

Next steps

Continue the course with Amazon ECS & ECR, In Depth: Task Definitions, Services, Fargate vs EC2 & the Registry — since API Gateway also fronts containerised backends through an HTTP integration or a private VPC Link, the natural next step is learning how to run those containers. Build on this lesson with:

AWSAPI GatewayServerlessREST APIWebSocketAuthorizers
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments