AWS API Gateway REST APIs

By Vishvesh Patel · DevOps EngineerUpdated September 16, 2026

The REST API features worth paying for. Resources and methods, integration types, mapping templates, request validation, usage plans, caching, and OpenAPI import.

API Gateway REST APIs

A REST API costs roughly three and a half times what an HTTP API does, so it is worth being clear about what you get for the difference. This page covers exactly that: the features that justify choosing REST, and how to use them.

If none of these apply to you, use an HTTP API.

Resources and methods

A REST API is a tree of resources, each carrying methods:

REST_ID=$(aws apigateway create-rest-api --name orders-api \
  --endpoint-configuration types=REGIONAL --query id --output text)

ROOT=$(aws apigateway get-resources --rest-api-id "$REST_ID" \
  --query 'items[0].id' --output text)

ORDERS=$(aws apigateway create-resource --rest-api-id "$REST_ID" \
  --parent-id "$ROOT" --path-part orders --query id --output text)

ORDER=$(aws apigateway create-resource --rest-api-id "$REST_ID" \
  --parent-id "$ORDERS" --path-part '{orderId}' --query id --output text)

aws apigateway put-method --rest-api-id "$REST_ID" --resource-id "$ORDER" \
  --http-method GET --authorization-type NONE \
  --request-parameters 'method.request.path.orderId=true'

Building this by hand is laborious, which is why most REST APIs are defined in OpenAPI or CloudFormation and imported. A {proxy+} greedy path catches everything below a point and hands routing to your backend, which cuts the resource tree down dramatically when you do not need per-path gateway behaviour.

Integration types

TypeMeaning
AWS_PROXYLambda proxy: whole request in, whole response out
AWSLambda or another AWS service, with mapping templates
HTTP_PROXYPass through to an HTTP endpoint unchanged
HTTPHTTP endpoint with mapping templates
MOCKGateway responds with no backend

AWS_PROXY is the sensible default. See API Gateway and Lambda. The non-proxy AWS type is what enables direct service integrations without a Lambda at all:

aws apigateway put-integration --rest-api-id "$REST_ID" --resource-id "$ORDERS" \
  --http-method POST --type AWS \
  --integration-http-method POST \
  --uri "arn:aws:apigateway:eu-west-1:sqs:path/123456789012/orders" \
  --credentials "arn:aws:iam::123456789012:role/apigw-sqs-role" \
  --request-parameters 'integration.request.header.Content-Type='"'"'application/x-www-form-urlencoded'"'"'' \
  --request-templates '{"application/json":"Action=SendMessage&MessageBody=$util.urlEncode($input.body)"}'

That puts a request straight onto an SQS queue with no Lambda in between, no cold start, no function to maintain, and nothing to pay per invocation. The same pattern works for DynamoDB, Step Functions, Kinesis and EventBridge. It is one of the genuinely good reasons to use a REST API.

MOCK integrations are how you stub an endpoint before the backend exists, and how CORS preflight is implemented on a REST API.

Mapping templates

Mapping templates transform requests and responses using VTL. They are powerful, and they are also code that lives in your infrastructure configuration rather than your repository, so use them for genuine adaptation, not business logic.

## Request template
{
  "orderId": "$input.params('orderId')",
  "body": $input.json('$'),
  "requestId": "$context.requestId",
  "caller": "$context.identity.sourceIp"
}

Useful $input methods: $input.params('name') reads a path, query or header parameter; $input.json('$') inserts the body as JSON; $input.path('$.field') extracts one value.

The most common legitimate use is mapping a backend error to a proper status code:

aws apigateway put-integration-response --rest-api-id "$REST_ID" \
  --resource-id "$ORDER" --http-method GET --status-code 404 \
  --selection-pattern '.*"statusCode":404.*'

selectionPattern is a regex matched against the integration's error output, which is how a Lambda throwing "not found" becomes a 404 rather than a 502. Note it matches the error message, which makes it brittle. Another argument for AWS_PROXY, where your function sets the status code directly.

Request validation

Rejecting bad requests at the gateway means your backend is never invoked, so you do not pay for it and do not have to write the checks:

MODEL=$(aws apigateway create-model --rest-api-id "$REST_ID" \
  --name CreateOrder --content-type application/json \
  --schema '{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "type": "object",
    "required": ["customerId", "items"],
    "properties": {
      "customerId": {"type": "string", "pattern": "^C-[0-9]+$"},
      "items": {"type": "array", "minItems": 1}
    }
  }' --query name --output text)

VALIDATOR=$(aws apigateway create-request-validator --rest-api-id "$REST_ID" \
  --name body-and-params --validate-request-body --validate-request-parameters \
  --query id --output text)

aws apigateway put-method --rest-api-id "$REST_ID" --resource-id "$ORDERS" \
  --http-method POST --authorization-type NONE \
  --request-validator-id "$VALIDATOR" \
  --request-models '{"application/json":"CreateOrder"}'

A failing request gets a 400 without reaching Lambda. This is REST-only and one of its better features, though the error message it returns is terse, so an API with human consumers usually wants friendlier validation in the application as well.

API keys and usage plans

The feature most often responsible for choosing REST. Keys identify consumers; usage plans meter them:

KEY_ID=$(aws apigateway create-api-key --name partner-acme --enabled \
  --query id --output text)

PLAN_ID=$(aws apigateway create-usage-plan --name partner-tier \
  --throttle 'rateLimit=10,burstLimit=20' \
  --quota 'limit=10000,period=MONTH,offset=0' \
  --api-stages "apiId=$REST_ID,stage=prod" \
  --query id --output text)

aws apigateway create-usage-plan-key --usage-plan-id "$PLAN_ID" \
  --key-id "$KEY_ID" --key-type API_KEY

Then require the key on each method:

aws apigateway update-method --rest-api-id "$REST_ID" --resource-id "$ORDERS" \
  --http-method POST \
  --patch-operations 'op=replace,path=/apiKeyRequired,value=true'

Setting apiKeyRequired is the step people forget. Creating keys and plans without it produces an API that happily serves anyone, with the keys metering nobody.

Consumers send the key in the x-api-key header. And again: a key is not authentication. It identifies a caller for metering, nothing more.

aws apigateway get-usage --usage-plan-id "$PLAN_ID" \
  --start-date 2026-09-01 --end-date 2026-09-16

Caching

aws apigateway update-stage --rest-api-id "$REST_ID" --stage-name prod \
  --patch-operations \
    'op=replace,path=/cacheClusterEnabled,value=true' \
    'op=replace,path=/cacheClusterSize,value=0.5' \
    'op=replace,path=/*/*/caching/enabled,value=true' \
    'op=replace,path=/*/*/caching/ttlInSeconds,value=300'

Worth knowing two things. The cache is provisioned capacity billed per hour, not per request, so a small cache on a low-traffic API can cost more than the requests it saves. And by default the cache key is the full request path. Enable caching per method and choose cache key parameters deliberately, or authenticated responses can be served to the wrong caller, which is a genuine security bug rather than a performance one.

Private endpoints

A REST API reachable only from inside your VPCs:

aws apigateway create-rest-api --name internal-api \
  --endpoint-configuration types=PRIVATE

aws ec2 create-vpc-endpoint --vpc-endpoint-type Interface \
  --service-name com.amazonaws.eu-west-1.execute-api \
  --vpc-id vpc-0abc123 --subnet-ids subnet-0a1b2c3d

A private API also needs a resource policy allowing the endpoint, or every request is denied:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow", "Principal": "*", "Action": "execute-api:Invoke",
    "Resource": "arn:aws:execute-api:eu-west-1:123456789012:*/*/*/*",
    "Condition": {"StringEquals": {"aws:SourceVpce": "vpce-0123456789abcdef0"}}
  }]
}

OpenAPI import and canary deployments

aws apigateway import-rest-api --body 'fileb://openapi.yaml'
aws apigateway put-rest-api --rest-api-id "$REST_ID" --mode overwrite --body 'fileb://openapi.yaml'

Defining the API as a spec and importing it is far more maintainable than clicking through resources, and it gives you a reviewable diff.

REST APIs also support canary deployments natively:

aws apigateway update-stage --rest-api-id "$REST_ID" --stage-name prod \
  --patch-operations \
    'op=replace,path=/canarySettings/percentTraffic,value=10' \
    'op=replace,path=/canarySettings/useStageCache,value=false'
aws apigateway update-stage --rest-api-id "$REST_ID" --stage-name prod \
  --patch-operations 'op=replace,path=/canarySettings/deploymentId,value=NONE'   # promote

Common errors

{"message":"Forbidden"}

With a private API, the resource policy does not allow your VPC endpoint. Otherwise, a missing or invalid x-api-key on a method requiring one.

{"message":"Invalid request body"}

Request validation rejected it against the model. The response is deliberately terse.

Execution failed due to configuration error: Invalid permissions on Lambda function

The gateway lacks lambda:InvokeFunction. Add a resource-based permission on the function.

Everything returns the old behaviour. No create-deployment after the change.

Quick reference

TaskCommand
Createaws apigateway create-rest-api --endpoint-configuration types=REGIONAL
Add a resourceaws apigateway create-resource --path-part '{id}'
Add a methodaws apigateway put-method --http-method GET
Direct service integrationput-integration --type AWS --credentials <role>
Validate requestscreate-request-validator + --request-models
Require a keyupdate-method --patch-operations op=replace,path=/apiKeyRequired,value=true
Usage planaws apigateway create-usage-plan --quota …
Import a specaws apigateway put-rest-api --mode overwrite --body fileb://openapi.yaml
Deployaws apigateway create-deployment --stage-name prod

Key takeaways

  • Only choose REST for a feature you need: keys, validation, caching, WAF, private endpoints.
  • Direct service integrations remove Lambda entirely for straight-through calls to SQS or DynamoDB.
  • selectionPattern matches the error message, which makes non-proxy error mapping brittle.
  • Request validation rejects bad input before your backend is invoked: and before you pay for it.
  • Set apiKeyRequired on each method, or your keys meter nothing.
  • Caching is provisioned per hour, and a careless cache key can leak authenticated responses.
  • A private API needs a resource policy naming the VPC endpoint.
  • Define the API as OpenAPI and import it. Clicking through resources does not scale.

Check your understanding

Exercise

Keys that meter nothing

API keys and a usage plan with a monthly quota are configured and attached to the prod stage. Requests without any key still succeed, and usage reports show nothing. What was missed?

Exercise

Removing a Lambda that does nothing

A Lambda function behind API Gateway does nothing but forward the request body to an SQS queue. It adds cold starts and cost. What is the REST API feature that removes it?


Frequently Asked Questions

What can a REST API do that an HTTP API cannot?

Five things worth the price difference: API keys with usage plans for per-consumer quotas, request validation against a JSON Schema model at the gateway, WAF integration, private endpoints reachable only inside a VPC, and response caching. It also supports mapping templates and direct AWS service integrations, which let you call SQS or DynamoDB without a Lambda. If none of those apply, an HTTP API does the same job for roughly 30% of the cost and slightly lower latency.

What are mapping templates and should I use them?

VTL templates that transform a request before it reaches the backend, or a response before it returns. They are genuinely useful for adapting between an external API contract and an internal service shape, and for calling AWS services directly with no Lambda. They are also code living in infrastructure configuration rather than a repository, with limited testing options, so keep them to adaptation and avoid business logic. With AWS_PROXY integrations you rarely need them at all.

How do API keys and usage plans actually work?

An API key identifies a consumer, and a usage plan attaches throttle rates and a daily or monthly quota to one or more API stages. You create the key, create the plan, associate them, and then, the step most often missed, set apiKeyRequired to true on each method that should enforce it. Consumers send the key in the x-api-key header. Keys identify and meter callers; they are not authentication, so pair them with an authorizer for identity.

Is API Gateway caching worth enabling?

Sometimes, and check the arithmetic first. The cache is provisioned capacity billed per hour regardless of traffic, so on a low-volume API it can easily cost more than the backend calls it avoids. The bigger caution is correctness: the default cache key is the request path, so enabling caching on an endpoint whose response depends on the caller can serve one user's data to another. Enable it per method and set cache key parameters explicitly.

How do I make a private API Gateway work?

Create the API with --endpoint-configuration types=PRIVATE, create an interface VPC endpoint for com.amazonaws.<region>.execute-api, and attach a resource policy to the API allowing execute-api:Invoke with an aws:SourceVpce condition naming that endpoint. The resource policy is the part that catches people, without it every request returns Forbidden, because a private API denies everything by default. The endpoint also needs private DNS enabled for the standard hostname to resolve inside the VPC.

Should I define my REST API in OpenAPI?

Yes, for anything beyond a prototype. Building resources and methods through the console or individual CLI calls is slow, error-prone and produces no reviewable artefact. put-rest-api --mode overwrite --body fileb://openapi.yaml applies a spec as the whole definition, so the API is version-controlled, diffable in a pull request and reproducible in another account. AWS-specific integration details go in x-amazon-apigateway-* extensions within the same document.

Continue Learning

Explore Related Topics

When it breaks

Related Resources