TL;DR
Learn essential patterns and best practices for designing and implementing APIs using Amazon API Gateway, including security, integration patterns, and performance optimization
Amazon API Gateway enables you to create, publish, maintain, monitor, and secure APIs at any scale. But choosing the right API Gateway configuration involves critical trade-offs that impact your application's performance, cost, and operational complexity. This guide provides a decision framework for building robust APIs.
$1
API Gateway is often the front door to your entire backend infrastructure. A poorly designed API layer can become a bottleneck that limits your ability to scale, increases costs exponentially, and creates security vulnerabilities. Conversely, a well-architected API Gateway setup enables you to:
The decisions you make here cascade through your entire system architecture.
`` %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#FF9900', 'primaryTextColor': '#232F3E', 'primaryBorderColor': '#232F3E', 'lineColor': '#232F3E', 'secondaryColor': '#147EB4', 'tertiaryColor': '#232F3E', 'fontFamily': 'system-ui', 'fontSize': '14px' }}}%%
graph TB
subgraph Frontend["Client Applications"]
direction TB
Web["Web"]
Mobile["Mobile"]
IoT["IoT"]
end subgraph Gateway["API Gateway"]
direction TB
subgraph Endpoints["API Endpoints"]
direction LR
REST["REST"]
HTTP["HTTP"]
WebSocket["WebSocket"]
end
subgraph Features["Gateway Features"]
direction LR
Auth["Authentication"]
Cache["Caching"]
Throttle["Throttling"]
end
subgraph Integration["Integration Types"]
direction LR
Lambda["Lambda"]
HTTP_INT["HTTP"]
Mock["Mock"]
end
end subgraph Backend["Backend Services"]
direction TB
subgraph Compute["Compute"]
direction LR
LambdaFn["Lambda"]
ECS["ECS"]
EC2["EC2"]
end
subgraph Data["Data Services"]
direction LR
DynamoDB["DynamoDB"]
RDS["RDS"]
S3["S3"]
end
subgraph Messaging["Event Services"]
direction LR
SNS["SNS"]
SQS["SQS"]
EventBridge["EventBridge"]
end
end Frontend --> Gateway
Gateway --> Backend classDef frontendNode fill:#FF9900,stroke:#232F3E,color:#232F3E,stroke-width:2px,font-weight:bold
classDef gatewayNode fill:#232F3E,stroke:#232F3E,color:#FFFFFF,stroke-width:2px,font-weight:bold
classDef backendNode fill:#147EB4,stroke:#232F3E,color:#FFFFFF,stroke-width:2px,font-weight:bold
classDef groupStyle fill:transparent,stroke:#232F3E,stroke-width:2px,color:#232F3E,font-weight:bold
class Web,Mobile,IoT frontendNode
class REST,HTTP,WebSocket,Auth,Cache,Throttle,Lambda,HTTP_INT,Mock gatewayNode
class LambdaFn,ECS,EC2,DynamoDB,RDS,S3,SNS,SQS,EventBridge backendNode
class Frontend,Gateway,Backend,Endpoints,Features,Integration,Compute,Data,Messaging groupStyle
mermaid
`
$1
This is the first and most impactful decision you'll make. AWS offers two API types with dramatically different capabilities and costs.
$1
Choose HTTP API when:
Real-world example: A mobile app backend with Cognito authentication and Lambda functions. HTTP API is the clear winner here, saving thousands of dollars monthly at scale.
$1
Choose REST API when:
Real-world example: A B2B API where partners need API keys, rate limiting per customer, and you need to transform XML requests to JSON for Lambda.
$1
| Monthly Requests | REST API Cost | HTTP API Cost | Savings |
|-----------------|---------------|---------------|---------|
| 1 million | $3.50 | $1.00 | 71% |
| 10 million | $35.00 | $10.00 | 71% |
| 100 million | $350.00 | $100.00 | 71% |
| 1 billion | $3,500.00 | $1,000.00 | 71% |
$1
$1
| Approach | Latency | Flexibility | Complexity | Cost |
|----------|---------|-------------|------------|------|
| Cognito User Pools | Low | Medium | Low | Free tier available |
| Lambda Authorizer | Medium | High | Medium | Per-invocation cost |
| IAM (SigV4) | Low | Low | Medium | No additional cost |
| API Keys | Lowest | Lowest | Lowest | No additional cost |
Senior insight: Lambda Authorizers add 50-200ms to cold requests. Cache the authorization decision aggressively (TTL up to 3600s) to amortize this cost. For high-throughput APIs, the authorization Lambda cost can exceed your API Gateway costs.
$1
| Cache TTL | Hit Rate | Freshness | Cost Impact |
|-----------|----------|-----------|-------------|
| 0 (disabled) | 0% | Perfect | Highest backend cost |
| 60 seconds | ~60-80% | Good | Medium |
| 300 seconds | ~85-95% | Acceptable | Lower |
| 3600 seconds | ~95%+ | Stale risk | Lowest |
Warning: API Gateway caching is per-stage, not per-customer. If you cache user-specific data without including user identity in the cache key, you'll serve User A's data to User B. Always include authentication headers in your cache key parameters for user-specific endpoints.
$1
Despite its capabilities, API Gateway isn't always the right choice:
$1
$1
$1
$1
$1
A well-designed OpenAPI specification serves as both documentation and contract:
` openapi: 3.0.0
info:
title: Product API
version: 1.0.0
paths:
/products:
get:
summary: List products
parameters:
- name: category
in: query
schema:
type: string
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
- name: cursor
in: query
description: Pagination cursor for next page
schema:
type: string
responses:
'200':
description: List of products
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/Product'
nextCursor:
type: string
yaml
`
$1
Structure your Lambda handlers to maximize reusability and testability:
` // handler.js - API Gateway Lambda integration
const { getProducts, createProduct, updateProduct, deleteProduct } = require('./services/products'); exports.handler = async (event) => {
const pathParams = event.pathParameters || {};
const queryParams = event.queryStringParameters || {};
const body = event.body ? JSON.parse(event.body) : {};
// Add request ID for distributed tracing
const requestId = event.requestContext?.requestId;
try {
let result;
switch (event.httpMethod) {
case 'GET':
result = pathParams.id
? await getProducts.byId(pathParams.id)
: await getProducts.list(queryParams);
break;
case 'POST':
result = await createProduct(body);
return formatResponse(201, result, requestId);
case 'PUT':
result = await updateProduct(pathParams.id, body);
break;
case 'DELETE':
await deleteProduct(pathParams.id);
return formatResponse(204, null, requestId);
default:
return formatResponse(405, { error: 'Method not allowed' }, requestId);
}
return formatResponse(200, result, requestId);
} catch (error) {
console.error('Request failed:', { requestId, error: error.message, stack: error.stack });
if (error.name === 'ValidationError') {
return formatResponse(400, { error: error.message }, requestId);
}
if (error.name === 'NotFoundError') {
return formatResponse(404, { error: 'Resource not found' }, requestId);
}
return formatResponse(500, { error: 'Internal server error' }, requestId);
}
}; function formatResponse(statusCode, body, requestId) {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'X-Request-Id': requestId,
'Cache-Control': statusCode === 200 ? 'max-age=60' : 'no-store',
},
body: body ? JSON.stringify(body) : '',
};
}
javascript
`
$1
Skip Lambda entirely for simple CRUD operations. This pattern reduces latency and cost:
` Resources:
GetItemMethod:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref Api
ResourceId: !Ref ItemResource
HttpMethod: GET
AuthorizationType: AWS_IAM
Integration:
Type: AWS
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:dynamodb:action/GetItem
Credentials: !GetAtt ApiGatewayRole.Arn
RequestTemplates:
application/json: |
{
"TableName": "Items",
"Key": {
"id": {"S": "$input.params('id')"}
}
}
IntegrationResponses:
- StatusCode: 200
ResponseTemplates:
application/json: |
#set($item = $input.path('$.Item'))
{
"id": "$item.id.S",
"name": "$item.name.S",
"price": $item.price.N
}
yaml
`API Gateway -> DynamoDB direct integration
$1
$1
$1
$1
$1
$1
` Need WebSocket support?
├── Yes → REST API (WebSocket APIs)
└── No → Do you need:
├── WAF integration? → REST API
├── API Keys/Usage Plans? → REST API
├── Request transformation? → REST API
├── Caching at Gateway? → REST API
└── None of above? → HTTP API (save 71%)
``
$1
1. [API Gateway Documentation](https://docs.aws.amazon.com/apigateway/)
2. [Choosing Between REST APIs and HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html)
3. [Best Practices](https://docs.aws.amazon.com/apigateway/latest/developerguide/best-practices.html)
4. [Security Best Practices](https://docs.aws.amazon.com/apigateway/latest/developerguide/security.html)
5. [Performance Optimization Guide](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-performance.html)
Why This Matters
Understanding the business and technical context helps you make informed decisions rather than blindly following patterns.
Trade-offs to Consider
Every architectural decision involves trade-offs. Consider your specific requirements, team expertise, and scale when evaluating options.
When NOT to Use This
Knowing when a solution doesn't apply is as valuable as knowing when it does. Consider alternatives for your specific situation.
Decision Framework
Use this framework to evaluate whether this approach is right for your use case based on your specific constraints and requirements.