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.
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.
| Type | Meaning |
|---|---|
AWS_PROXY | Lambda proxy: whole request in, whole response out |
AWS | Lambda or another AWS service, with mapping templates |
HTTP_PROXY | Pass through to an HTTP endpoint unchanged |
HTTP | HTTP endpoint with mapping templates |
MOCK | Gateway 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 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.
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.
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_KEYThen 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-16aws 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.
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-0a1b2c3dA 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"}}
}]
}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{"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.
| Task | Command |
|---|---|
| Create | aws apigateway create-rest-api --endpoint-configuration types=REGIONAL |
| Add a resource | aws apigateway create-resource --path-part '{id}' |
| Add a method | aws apigateway put-method --http-method GET |
| Direct service integration | put-integration --type AWS --credentials <role> |
| Validate requests | create-request-validator + --request-models |
| Require a key | update-method --patch-operations op=replace,path=/apiKeyRequired,value=true |
| Usage plan | aws apigateway create-usage-plan --quota ⦠|
| Import a spec | aws apigateway put-rest-api --mode overwrite --body fileb://openapi.yaml |
| Deploy | aws apigateway create-deployment --stage-name prod |
selectionPattern matches the error message, which makes non-proxy error mapping brittle.apiKeyRequired on each method, or your keys meter nothing.<MCQExercise exercise={{ id: 'aws-apigw-rest-quiz1', title: 'Keys that meter nothing', description: '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?', options: [ { id: 'a', text: 'apiKeyRequired was never set to true on the methods, so the gateway does not require or record a key' }, { id: 'b', text: 'Usage plans only apply to HTTP APIs, not REST APIs' }, { id: 'c', text: 'The API key must be passed as a query string parameter rather than a header' }, { id: 'd', text: 'Usage data takes thirty days to appear, so the report is expected to be empty' }, ], correctId: 'a', explanation: 'Creating keys and a usage plan sets up the metering machinery but does not switch it on. Each method must have apiKeyRequired set to true before the gateway looks for an x-api-key header, enforces the plan, or records usage, so without it the API serves everyone and the keys are inert. Set it per method with update-method, then create a deployment, since this is a REST API configuration change like any other.', wrongExplanations: { b: 'It is the reverse: usage plans and API keys are a REST API feature and are not available on HTTP APIs.', c: 'The header x-api-key is the supported mechanism; a query string is not.', d: 'Usage data appears within a day. An empty report here reflects no keyed requests at all.', }, }} />
<MCQExercise exercise={{ id: 'aws-apigw-rest-quiz2', title: 'Removing a Lambda that does nothing', description: '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?', options: [ { id: 'a', text: 'A non-proxy AWS integration with an IAM role and a request template, which calls SQS directly with no Lambda involved' }, { id: 'b', text: 'A MOCK integration, which forwards the request to the configured backend service' }, { id: 'c', text: 'An HTTP_PROXY integration pointed at the SQS queue URL' }, { id: 'd', text: 'Enabling stage caching, which batches requests to SQS automatically' }, ], correctId: 'a', explanation: 'The AWS integration type lets API Gateway call an AWS service API directly, assuming a role you supply and shaping the call with a request mapping template, so a straight-through write to SQS, DynamoDB, Kinesis, EventBridge or Step Functions needs no function at all. That removes cold starts, per-invocation cost and a piece of code to maintain. It is one of the more compelling reasons to accept a REST API's higher per-request price, and it is unavailable on HTTP APIs.', wrongExplanations: { b: 'MOCK returns a response from the gateway itself without calling any backend; it is for stubs and CORS preflight.', c: 'HTTP_PROXY passes requests to an HTTP endpoint unchanged and cannot sign requests with SigV4, which SQS requires.', d: 'Caching stores responses. It performs no writes and does not batch anything.', }, }} />
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.
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.
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.
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.
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.
Connect API Gateway to Lambda with proxy integration and return the response shape that avoids a 502. Covers payload v1 vs v2, CORS and the 29-second limit.
Push, pull and secure Docker images in Amazon ECR. Authenticate with the AWS CLI, tag images, set lifecycle policies, enable scanning and fix common errors.
Understand how Amazon EKS runs Kubernetes on AWS: what AWS manages, node options, access entries, IRSA vs Pod Identity, add-ons and networking pitfalls.