Aws
AwsIntermediate

AWS API Gateway Patterns: Building Scalable APIs

DeveloperHat Team
14 min read
API GatewayRESTServerlessIntegration

TL;DR

Learn essential patterns and best practices for designing and implementing APIs using Amazon API Gateway, including security, integration patterns, and performance optimization

Mastering AWS API Gateway: A Strategic Configuration Guide for Engineers

TL;DR: AWS API Gateway offers powerful ways to expose backend services. The primary decision point is between HTTP APIs and REST APIs. HTTP APIs are more cost-effective and lower-latency for most common use cases (Lambda/HTTP backends, JWT auth), while REST APIs provide advanced features like WAF integration, API key management, and request transformations. Carefully consider authentication, caching, and when to use alternative AWS services like ALB, CloudFront, or AppSync.

$1

API Gateway acts as the critical front door to your backend infrastructure. A misconfigured API layer can become a performance bottleneck, inflate operational costs, and introduce security vulnerabilities. Conversely, a well-architected API Gateway setup provides significant advantages:

* Decoupled Scalability: Scale your API layer independently from your backend services.

* Performance Optimization: Mitigate Lambda cold starts through effective caching strategies.

* Cost Efficiency: Achieve substantial cost reductions (up to 71%) by selecting HTTP APIs over REST APIs for simpler use cases.

* Backend Protection: Shield your backend services from traffic surges and DDoS attacks.

The architectural decisions made at the API Gateway level have far-reaching implications across your entire system.

``mermaid

%%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#FF9900', 'primaryTextColor': '#232F3E', 'primaryBorderColor': '#232F3E', 'lineColor': '#232F3E', 'secondaryColor': '#147EB4', 'tertiaryColor': '#232F3E', 'fontFamily': 'system-ui', 'fontSize': '14px' }}}%%

graph TB

subgraph ClientApplications["Client Applications"]

direction TB

Web["Web Browser"]

Mobile["Mobile App"]

IoT["IoT Device"]

end

subgraph ApiGateway["AWS API Gateway"]

direction TB

subgraph EndpointTypes["Endpoint Types"]

direction LR

REST["REST API"]

HTTP["HTTP API"]

WebSocket["WebSocket API"]

end

subgraph GatewayFeatures["Gateway Features"]

direction LR

AuthN["Authentication & Authorization"]

Cache["Caching"]

Throttle["Throttling & Rate Limiting"]

end

subgraph IntegrationTypes["Integration Types"]

direction LR

LambdaInt["Lambda Function"]

HttpInt["HTTP Endpoint"]

MockInt["Mock Integration"]

StepFunctionsInt["Step Functions"]

DynamoDBInt["DynamoDB"]

end

end

subgraph BackendServices["Backend Services"]

direction TB

subgraph Compute["Compute"]

direction LR

LambdaFn["AWS Lambda"]

ECS["ECS/EKS"]

EC2["EC2 Instances"]

end

subgraph DataStores["Data Stores"]

direction LR

DynamoDB["DynamoDB"]

RDS["RDS"]

S3["S3"]

end

subgraph MessagingQueues["Messaging & Eventing"]

direction LR

SNS["SNS"]

SQS["SQS"]

EventBridge["EventBridge"]

end

end

ClientApplications --> ApiGateway

ApiGateway --> BackendServices

classDef clientNode 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 clientNode

class REST,HTTP,WebSocket,AuthN,Cache,Throttle,LambdaInt,HttpInt,MockInt,StepFunctionsInt,DynamoDBInt gatewayNode

class LambdaFn,ECS,EC2,DynamoDB,RDS,S3,SNS,SQS,EventBridge backendNode

class ClientApplications,ApiGateway,BackendServices,EndpointTypes,GatewayFeatures,IntegrationTypes,Compute,DataStores,MessagingQueues groupStyle

`

$1

The initial and most impactful decision is choosing between HTTP APIs and REST APIs. These two service types offer significantly different feature sets and cost structures.

Choose HTTP API when:

* Your backend integrations are limited to Lambda functions or HTTP endpoints.

* Authentication requirements are met by JWTs (e.g., Amazon Cognito, Auth0, Okta) or Lambda Authorizers.

* Minimizing operational costs is a primary concern (HTTP APIs are up to 71% cheaper).

* Low latency is critical, as HTTP APIs incur less overhead.

* Advanced API Gateway features like request/response transformation, API keys, or usage plans are not required.

Practical Example: A mobile application backend that leverages Cognito for user authentication and Lambda functions for business logic. In this scenario, an HTTP API will provide a more cost-effective and performant solution, potentially saving thousands of dollars monthly at scale.

$1

Choose REST API when:

* Integration with AWS WAF for advanced security filtering is a requirement.

* API key management and granular usage plans (e.g., for B2B partners) are necessary.

* You need to perform request or response transformations using mapping templates.

* Edge-optimized endpoints leveraging CloudFront are desired.

* API Gateway-level caching is a specific architectural need.

* VPC Link integration with private Application Load Balancers (ALBs) or Network Load Balancers (NLBs) is required.

Practical Example: A B2B API serving external partners, requiring distinct API keys per partner, rate limiting based on partner tiers, and the ability to transform incoming XML requests into JSON for backend processing by Lambda.

$1

| Monthly Requests | REST API Cost (approx.) | HTTP API Cost (approx.) | Percentage 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% |

(Note: Costs are illustrative and based on standard pricing for API Gateway requests. Always consult the official AWS pricing page for the most current figures.)

$1

$1

| Approach | Latency Impact | Flexibility | Implementation Complexity | Cost (per invocation) |

| :------------------- | :----------------- | :---------- | :------------------------ | :-------------------- |

| Cognito User Pools | Low | Medium | Low | Free tier, then usage |

| Lambda Authorizer | Medium (50-200ms)¹ | 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 can introduce significant latency, particularly on cold starts. Aggressively cache authorization decisions (TTL up to 3600 seconds) to amortize this cost across multiple requests. For high-volume APIs, the cost of the authorization Lambda itself can rival or exceed API Gateway request costs.

$1

| Cache TTL | Typical Hit Rate | Data Freshness | Impact on Backend Load | Cost Impact |

| :-------- | :--------------- | :------------- | :--------------------- | :---------------------- |

| 0 (Disabled) | 0% | Perfect | Highest | Highest backend cost |

| 60 seconds | ~60-80% | Good | Medium | Medium |

| 300 seconds | ~85-95% | Acceptable | Lower | Lower |

| 3600 seconds | ~95%+ | Risk of Stale | Lowest | Lowest |

Critical Warning: API Gateway caching is configured at the stage level, not on a per-customer basis. If you cache responses containing user-specific data without incorporating user identity into the cache key, you risk serving data intended for one user to another. Always include relevant user authentication headers (e.g., Authorization) in your cache key parameters for any endpoint that returns user-specific information.

$1

While powerful, API Gateway isn't universally the optimal solution. Consider these alternatives:

$1

* You are deploying long-running applications or microservices in container orchestration platforms (ECS, EKS).

* Sticky sessions are a hard requirement for your application architecture.

* You have predictable traffic patterns and can leverage reserved capacity pricing for cost optimization.

* Your application heavily relies on WebSocket connections, potentially exceeding API Gateway's concurrent connection limits (10,000 for REST, 2,000 for HTTP).

$1

* Sub-10ms response times at the edge are critical for performance.

* Your API responses are highly cacheable globally.

* You are architecting a distributed, global API with significant edge computing requirements.

$1

* Your primary requirement is a GraphQL API with real-time subscription capabilities.

* You want to leverage automatic resolvers for direct integration with DynamoDB.

* Your mobile or web applications require offline data synchronization capabilities.

$1

$1

Leveraging OpenAPI (formerly Swagger) provides a formal contract, documentation, and a basis for code generation.

`yaml

openapi: 3.0.0

info:

title: Product Catalog API

version: 1.2.0

description: API for managing product information.

servers:

- url: https://api.example.com/v1

description: Production server

paths:

/products:

get:

summary: Retrieve a list of products

operationId: listProducts

parameters:

- name: category

in: query

description: Filter products by category

schema:

type: string

- name: limit

in: query

description: Maximum number of products to return

schema:

type: integer

minimum: 1

maximum: 100

default: 20

- name: cursor

in: query

description: Pagination cursor for retrieving the next page of results

schema:

type: string

responses:

'200':

description: A list of products matching the query

content:

application/json:

schema:

type: object

properties:

items:

type: array

items:

$ref: '#/components/schemas/Product'

nextCursor:

type: string

description: Cursor for the next page, or null if this is the last page.

'400':

description: Invalid query parameters provided

/products/{productId}:

get:

summary: Retrieve a specific product by ID

operationId: getProductById

parameters:

- name: productId

in: path

required: true

description: The unique identifier of the product

schema:

type: string

responses:

'200':

description: Product details

content:

application/json:

schema:

$ref: '#/components/schemas/Product'

'404':

description: Product not found

components:

schemas:

Product:

type: object

properties:

id:

type: string

readOnly: true

name:

type: string

description:

type: string

price:

type: number

format: float

category:

type: string

required:

- name

- price

- category

`

$1

Design Lambda handlers to be modular and handle API Gateway event structures effectively.

`javascript

// src/handler.js - Optimized Lambda integration for API Gateway

import { Logger } from './utils/logger'; // Assuming a utility for structured logging

import { ProductService } from './services/productService';

import { ApiError, ValidationError, NotFoundError } from './utils/errors';

const logger = new Logger('ProductHandler');

/

* AWS Lambda handler for API Gateway proxy integration.

* @param {object} event - The API Gateway event object.

* @param {object} context - The Lambda context object.

* @returns {Promise} - The API Gateway response object.

*/

export const handler = async (event, context) => {

const { httpMethod, pathParameters, queryStringParameters, body, requestContext } = event;

const requestId = requestContext?.requestId || context.awsRequestId; // Utilize requestContext for correlation ID

logger.setCorrelationId(requestId); // Ensure logs are traceable

// Default to GET for root path if no specific route matches

const resource = pathParameters?.resource || 'products';

const id = pathParameters?.id;

const queryParams = queryStringParameters || {};

const payload = body ? JSON.parse(body) : {};

logger.info('Received request', { httpMethod, resource, id, queryParams });

try {

let result;

switch (httpMethod) {

case 'GET':

result = id

? await ProductService.getById(id, requestId)

: await ProductService.list(queryParams, requestId);

return formatResponse(200, result, requestId);

case 'POST':

if (!id) { // POST to /products

result = await ProductService.create(payload, requestId);

return formatResponse(201, result, requestId);

} else { // POST to /products/{id} is unusual, treat as PUT or error

return formatResponse(405, { error: 'Method POST not allowed on specific resource ID' }, requestId);

}

case 'PUT':

if (id) {

result = await ProductService.update(id, payload, requestId);

return formatResponse(200, result, requestId);

} else {

return formatResponse(400, { error: 'Product ID is required for PUT operation' }, requestId);

}

case 'DELETE':

if (id) {

await ProductService.delete(id, requestId);

return formatResponse(204, null, requestId);

} else {

return formatResponse(400, { error: 'Product ID is required for DELETE operation' }, requestId);

}

default:

throw new ApiError(405, 'Method Not Allowed');

}

} catch (error) {

logger.error('Request processing failed', { error: error.message, stack: error.stack });

if (error instanceof ValidationError) {

return formatResponse(400, { error: error.message }, requestId);

}

if (error instanceof NotFoundError) {

return formatResponse(404, { error: 'Resource not found' }, requestId);

}

if (error instanceof ApiError) {

return formatResponse(error.statusCode, { error: error.message }, requestId);

}

// Catch-all for unexpected server errors

return formatResponse(500, { error: 'An internal server error occurred.' }, requestId);

}

};

/

* Formats the Lambda function response for API Gateway proxy integration.

* Includes caching headers for successful GET requests.

* @param {number} statusCode - The HTTP status code.

* @param {object|null} body - The response body.

* @param {string} requestId - The correlation ID.

* @returns {object} - The API Gateway response object.

*/

function formatResponse(statusCode, body, requestId) {

const headers = {

'Content-Type': 'application/json',

'X-Request-Id': requestId,

};

// Apply caching headers only for successful GET requests with data

if (statusCode === 200 && body && !Array.isArray(body) && Object.keys(body).length > 0) {

// Example: Cache for 5 minutes (300 seconds)

headers['Cache-Control'] = 'public, max-age=300';

} else if (statusCode !== 200) {

// Prevent caching for non-success or error responses

headers['Cache-Control'] = 'no-store';

}

return {

statusCode,

headers,

body: body ? JSON.stringify(body) : '',

};

}

`

$1

For specific AWS services like DynamoDB, direct integration bypasses Lambda entirely, improving performance and reducing cost.

`yaml

AWS CloudFormation snippet for API Gateway -> DynamoDB GetItem direct integration

Resources:

RestApi:

Type: AWS::ApiGateway::RestApi

Properties:

Name: ProductApi

EndpointConfiguration:

Types:

- REGIONAL

ItemResource:

Type: AWS::ApiGateway::Resource

Properties:

RestApiId: !Ref RestApi

ParentId: !GetAtt RestApi.RootResourceId

PathPart: "items" # Example path: /items/{id}

ItemIdResource:

Type: AWS::ApiGateway::Resource

Properties:

RestApiId: !Ref RestApi

ParentId: !Ref ItemResource

PathPart: "{id}" # Example path: /items/{id}

GetItemMethod:

Type: AWS::ApiGateway::Method

Properties:

RestApiId: !Ref RestApi

ResourceId: !Ref ItemIdResource

HttpMethod: GET

AuthorizationType: AWS_IAM # Or other auth types as needed

Integration:

Type: AWS

IntegrationHttpMethod: POST # DynamoDB API actions are POST

Uri: !Sub arn:aws:apigateway:${AWS::Region}:dynamodb:action/GetItem

Credentials: !GetAtt ApiGatewayDynamoDBRole.Arn # IAM Role for API Gateway to access DynamoDB

RequestTemplates:

application/json: |

{

"TableName": "MyProductsTable", # Replace with your actual table name

"Key": {

"productId": {"S": "$input.params('id')"} # Assuming 'id' is the partition key and type String

}

}

# Explicitly define integration response to map DynamoDB output

IntegrationResponses:

- StatusCode: 200

ContentHandling: CONVERT_TO_TEXT

ResponseTemplates:

application/json: |

#set($inputRoot = $input.path('$'))

#set($item = $inputRoot.Item)

{

"productId": $item.productId.S,

"name": $item.name.S,

"price": $item.price.N,

"description": $item.description.S

}

# Define method response for documentation and contract

MethodResponses:

- StatusCode: 200

ResponseModels:

application/json: Empty

- StatusCode: 404

ResponseModels:

application/json: Empty

ApiGatewayDynamoDBRole:

Type: AWS::IAM::Role

Properties:

AssumeRolePolicyDocument:

Version: "2012-10-17"

Statement:

- Effect: "Allow"

Principal:

Service:

- apigateway.amazonaws.com

Action:

- "sts:AssumeRole"

Policies:

- PolicyName: ApiGatewayDynamoDBPolicy

PolicyDocument:

Version: "2012-10-17"

Statement:

- Effect: "Allow"

Action:

- "dynamodb:GetItem"

Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/MyProductsTable" # Replace with your table ARN

Outputs:

ApiEndpoint:

Description: "The endpoint URL for the API Gateway"

Value: !Sub "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/prod"

`

$1

$1

  • [ ] Consistency: Employ consistent naming conventions for resources and parameters (e.g., kebab-case for URLs, camelCase for JSON properties).
  • [ ] Versioning: Implement API versioning, typically via the URL path (e.g., /v1/users, /v2/users).
  • [ ] Backward Compatibility: Strive for backward compatibility within versions where possible. Introduce breaking changes in new major versions.
  • [ ] HTTP Semantics: Utilize appropriate HTTP methods (GET, POST, PUT, DELETE, PATCH) and status codes (2xx for success, 4xx for client errors, 5xx for server errors).
  • [ ] Pagination: For collections returning many items, implement cursor-based pagination for efficient data retrieval.
  • $1

  • [ ] WAF Integration: For REST APIs, integrate AWS WAF for enhanced protection against common web exploits.
  • [ ] Robust Authentication: Prioritize Cognito or Lambda Authorizers over simple API Keys for authenticating users or services. API Keys are suitable for identification and basic rate limiting, not secure authentication.
  • [ ] Input Validation: Implement request validation at the API Gateway level (supported by REST APIs) to filter malformed or malicious requests before they reach your backend.
  • [ ] Auditing: Enable AWS CloudTrail for comprehensive logging of API calls for compliance and auditing purposes.
  • [ ] Transport Security: Enforce HTTPS exclusively. API Gateway handles SSL/TLS termination.
  • $1

  • [ ] Caching Strategy: Implement caching for read-heavy, infrequently changing data. Configure appropriate TTLs.
  • [ ] Timeout Management: Set judicious API Gateway integration timeouts, respecting the maximum 29-second limit for Lambda integrations.
  • [ ] Throttling: Configure throttling limits to protect backend services from overload and ensure fair usage.
  • [ ] Endpoint Selection: Use regional endpoints for lower latency within a region, and edge-optimized endpoints (REST API only) for global distribution via CloudFront.
  • [ ] Compression: Enable gzip compression for responses to reduce bandwidth usage and improve transfer times.
  • $1

  • [ ] Error Alarms: Set up CloudWatch alarms on API Gateway metrics for 4xx and 5xx error rates.
  • [ ] Structured Logging: Configure access logging with a structured JSON format for easier parsing and analysis.
  • [ ] Distributed Tracing: Enable AWS X-Ray integration for end-to-end tracing across API Gateway and backend services.
  • [ ] Cache Performance: Monitor API Gateway cache hit/miss ratios and adjust TTLs based on observed performance.
  • [ ] Latency Metrics: Track latency percentiles (p50, p95, p99) to understand user experience under various load conditions.
  • $1

    `mermaid

    graph TD

    A[Start] --> B{Need WebSocket support?};

    B -- Yes --> C[REST API (WebSocket APIs)];

    B -- No --> D{Need WAF integration?};

    D -- Yes --> E[REST API];

    D -- No --> F{Need API Keys/Usage Plans?};

    F -- Yes --> G[REST API];

    F -- No --> H{Need Request/Response Transformation?};

    H -- Yes --> I[REST API];

    H -- No --> J{Need Caching at Gateway level?};

    J -- Yes --> K[REST API];

    J -- No --> L[HTTP API (Cost-effective, Lower Latency)];

    C --> Z[End];

    E --> Z;

    G --> Z;

    I --> Z;

    K --> Z;

    L --> Z;

    ``

    $1

    1. [AWS API Gateway Documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html)

    2. [Choosing Between REST APIs and HTTP APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html)

    3. [API Gateway Best Practices](https://docs.aws.amazon.com/apigateway/latest/developerguide/best-practices.html)

    4. [API Gateway Security Best Practices](https://docs.aws.amazon.com/apigateway/latest/developerguide/security.html)

    5. [API Gateway Performance Optimization Guide](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-performance.html)

    6. [AWS Lambda Authorizers](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html)

    7. [API Gateway Caching](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-caching.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.