AWS API Gateway Patterns: Building Scalable APIs
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.
Why This Matters
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:
- Scale independently from your backend services
- Reduce Lambda cold starts through proper caching strategies
- Cut costs by 40-70% with HTTP APIs vs REST APIs for simple use cases
- Protect backend services from traffic spikes and DDoS attacks
The decisions you make here cascade through your entire system architecture.
Decision Framework: REST API vs HTTP API
This is the first and most impactful decision you'll make. AWS offers two API types with dramatically different capabilities and costs.
When to Choose HTTP API (Recommended for Most Use Cases)
Choose HTTP API when:
- You need Lambda or HTTP backend integrations only
- JWT authentication (Cognito, Auth0, Okta) is sufficient
- You want to minimize costs (up to 71% cheaper)
- Low latency is critical (HTTP APIs have lower overhead)
- You don't need API Gateway features like request/response transformation, API keys, or usage plans
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.
When to Choose REST API
Choose REST API when:
- You need AWS WAF integration for security
- API key management and usage plans are required (B2B APIs)
- You need request/response transformation (mapping templates)
- Edge-optimized endpoints with CloudFront integration
- You need caching at the API Gateway level
- VPC Link with private ALB/NLB is required
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.
Cost Comparison at Scale
| 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% |
Trade-offs to Consider
Authentication Trade-offs
| 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.
Caching Trade-offs
| 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.
When NOT to Use API Gateway
Despite its capabilities, API Gateway isn't always the right choice:
Use Application Load Balancer Instead When:
- You're running containers (ECS, EKS) with long-running HTTP connections
- You need sticky sessions
- You have predictable traffic patterns and want reserved capacity pricing
- WebSocket usage exceeds API Gateway's 10,000 concurrent connection limit
Use CloudFront + Lambda@Edge Instead When:
- You need sub-10ms response times at the edge
- Your API responses are highly cacheable
- You're building a global API with edge computing requirements
Use AppSync Instead When:
- You need GraphQL with real-time subscriptions
- You want automatic DynamoDB resolvers
- Your mobile app needs offline-first capabilities with built-in sync
API Design Patterns
1. RESTful API Structure
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
2. Lambda Integration Best Practices
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) : '', }; }
3. Direct Service Integration Pattern
Skip Lambda entirely for simple CRUD operations. This pattern reduces latency and cost:
# API Gateway -> DynamoDB direct integration 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 }
Best Practices Checklist
API Design
- [ ] Use consistent naming conventions (kebab-case for URLs)
- [ ] Implement proper versioning (path-based: /v1/products)
- [ ] Design for backward compatibility
- [ ] Use appropriate HTTP methods and status codes
- [ ] Implement cursor-based pagination for large datasets
Security
- [ ] Enable AWS WAF for REST APIs
- [ ] Use Cognito or Lambda authorizers (never API keys alone for authentication)
- [ ] Implement request validation at the API Gateway level
- [ ] Enable CloudTrail logging for audit compliance
- [ ] Use HTTPS only (API Gateway enforces this by default)
Performance
- [ ] Enable caching for read-heavy endpoints
- [ ] Set appropriate timeout values (29s max for Lambda)
- [ ] Implement throttling to protect backends
- [ ] Use regional endpoints unless you need edge optimization
- [ ] Enable gzip compression for large responses
Monitoring
- [ ] Set up CloudWatch alarms for 4xx/5xx error rates
- [ ] Configure access logging with structured JSON
- [ ] Enable X-Ray tracing for distributed debugging
- [ ] Monitor cache hit ratios and adjust TTLs accordingly
- [ ] Track latency percentiles (p50, p95, p99)
Quick Reference: Decision Flowchart
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%)