AWS API Gateway: Building REST and HTTP APIs
A complete guide to AWS API Gateway. REST APIs vs HTTP APIs, authentication strategies, rate limiting, and integration patterns with Lambda and other services.
AWS API Gateway: Building REST and HTTP APIs
AWS API Gateway is a fully managed service for creating, publishing, and securing APIs at any scale. It handles traffic management, authorisation, monitoring, and API version management, letting you focus on building your application logic.
API Gateway Types
REST API vs HTTP API
API Gateway Options:
├── REST API (API Gateway v1)
│ ├── Full feature set
│ ├── Request/response transformation
│ ├── API keys and usage plans
│ ├── WAF integration
│ ├── Resource policies
│ └── Higher cost
│
├── HTTP API (API Gateway v2)
│ ├── Simplified, lower latency
│ ├── Built-in JWT authorisation
│ ├── OIDC/OAuth 2.0 native
│ ├── ~70% lower cost
│ └── Limited transformation
│
└── WebSocket API
├── Real-time bidirectional
├── Persistent connections
└── Chat, gaming, dashboardsFeature Comparison
| Feature | REST API | HTTP API |
|---|---|---|
| Cost | Higher | ~70% lower |
| Latency | ~30ms overhead | ~10ms overhead |
| JWT Authorisation | Lambda required | Built-in |
| API Keys | Yes | No |
| Usage Plans | Yes | No |
| Request Validation | Yes | No |
| WAF Integration | Yes | No |
| Private APIs | Yes | Yes |
| VPC Link | Yes | Yes |
HTTP API Configuration
Basic Setup with Terraform
# http-api.tf
resource "aws_apigatewayv2_api" "main" {
name = "${var.project}-api"
protocol_type = "HTTP"
description = "HTTP API for ${var.project}"
cors_configuration {
allow_origins = var.allowed_origins
allow_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
allow_headers = ["Content-Type", "Authorization", "X-Request-ID"]
expose_headers = ["X-Request-ID"]
max_age = 300
allow_credentials = true
}
tags = var.tags
}
resource "aws_apigatewayv2_stage" "main" {
api_id = aws_apigatewayv2_api.main.id
name = "$default"
auto_deploy = true
access_log_settings {
destination_arn = aws_cloudwatch_log_group.api.arn
format = jsonencode({
requestId = "$context.requestId"
ip = "$context.identity.sourceIp"
requestTime = "$context.requestTime"
httpMethod = "$context.httpMethod"
routeKey = "$context.routeKey"
status = "$context.status"
protocol = "$context.protocol"
responseLength = "$context.responseLength"
latency = "$context.responseLatency"
errorMessage = "$context.error.message"
})
}
default_route_settings {
throttling_burst_limit = 1000
throttling_rate_limit = 500
}
}
# Lambda Integration
resource "aws_apigatewayv2_integration" "lambda" {
api_id = aws_apigatewayv2_api.main.id
integration_type = "AWS_PROXY"
integration_uri = aws_lambda_function.api.invoke_arn
payload_format_version = "2.0"
timeout_milliseconds = 30000
}
resource "aws_apigatewayv2_route" "get_users" {
api_id = aws_apigatewayv2_api.main.id
route_key = "GET /users"
target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
authorization_type = "JWT"
authorizer_id = aws_apigatewayv2_authorizer.jwt.id
}
resource "aws_apigatewayv2_route" "create_user" {
api_id = aws_apigatewayv2_api.main.id
route_key = "POST /users"
target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
authorization_type = "JWT"
authorizer_id = aws_apigatewayv2_authorizer.jwt.id
}
# Lambda permission
resource "aws_lambda_permission" "api_gateway" {
statement_id = "AllowAPIGateway"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.api.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.main.execution_arn}/*/*"
}JWT Authorisation
# jwt-authorizer.tf
resource "aws_apigatewayv2_authorizer" "jwt" {
api_id = aws_apigatewayv2_api.main.id
authorizer_type = "JWT"
identity_sources = ["$request.header.Authorization"]
name = "cognito-jwt"
jwt_configuration {
issuer = "https://cognito-idp.${var.region}.amazonaws.com/${aws_cognito_user_pool.main.id}"
audience = [aws_cognito_user_pool_client.main.id]
}
}REST API Configuration
OpenAPI Specification
# openapi.yaml
openapi: "3.0.1"
info:
title: User Service API
version: "1.0"
description: API for user management
x-amazon-apigateway-request-validators:
all:
validateRequestBody: true
validateRequestParameters: true
paths:
/users:
get:
summary: List users
operationId: listUsers
parameters:
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
- name: cursor
in: query
schema:
type: string
responses:
"200":
description: List of users
content:
application/json:
schema:
$ref: "#/components/schemas/UserList"
x-amazon-apigateway-integration:
type: aws_proxy
httpMethod: POST
uri: arn:aws:apigateway:eu-west-1:lambda:path/2015-03-31/functions/${lambda_arn}/invocations
x-amazon-apigateway-request-validator: all
post:
summary: Create user
operationId: createUser
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserRequest"
responses:
"201":
description: User created
"400":
description: Validation error
x-amazon-apigateway-integration:
type: aws_proxy
httpMethod: POST
uri: arn:aws:apigateway:eu-west-1:lambda:path/2015-03-31/functions/${lambda_arn}/invocations
x-amazon-apigateway-request-validator: all
/users/{userId}:
get:
summary: Get user by ID
operationId: getUser
parameters:
- name: userId
in: path
required: true
schema:
type: string
pattern: "^[a-zA-Z0-9-]+$"
responses:
"200":
description: User details
"404":
description: User not found
x-amazon-apigateway-integration:
type: aws_proxy
httpMethod: POST
uri: arn:aws:apigateway:eu-west-1:lambda:path/2015-03-31/functions/${lambda_arn}/invocations
components:
schemas:
CreateUserRequest:
type: object
required:
- email
- name
properties:
email:
type: string
format: email
name:
type: string
minLength: 1
maxLength: 100
UserList:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/User"
nextCursor:
type: string
User:
type: object
properties:
id:
type: string
email:
type: string
name:
type: string
createdAt:
type: string
format: date-timeRequest Mapping Templates
# rest-api.tf
resource "aws_api_gateway_rest_api" "main" {
name = "${var.project}-rest-api"
body = templatefile("${path.module}/openapi.yaml", {
lambda_arn = aws_lambda_function.api.arn
})
endpoint_configuration {
types = ["REGIONAL"]
}
}
# Custom request transformation for non-proxy integration
resource "aws_api_gateway_integration" "custom" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.users.id
http_method = aws_api_gateway_method.create_user.http_method
integration_http_method = "POST"
type = "AWS"
uri = aws_lambda_function.api.invoke_arn
request_templates = {
"application/json" = <<EOF
{
"action": "createUser",
"body": $input.json('$'),
"requestId": "$context.requestId",
"sourceIp": "$context.identity.sourceIp",
"userAgent": "$context.identity.userAgent",
"claims": {
"sub": "$context.authorizer.claims.sub",
"email": "$context.authorizer.claims.email"
}
}
EOF
}
}
resource "aws_api_gateway_integration_response" "success" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.users.id
http_method = aws_api_gateway_method.create_user.http_method
status_code = aws_api_gateway_method_response.success.status_code
response_templates = {
"application/json" = <<EOF
#set($inputRoot = $input.path('$'))
{
"id": "$inputRoot.id",
"email": "$inputRoot.email",
"name": "$inputRoot.name"
}
EOF
}
}Authentication Patterns
Lambda Authoriser
// authorizer.ts
import { APIGatewayTokenAuthorizerEvent, APIGatewayAuthorizerResult } from 'aws-lambda';
import { verify, JwtPayload } from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
export const handler = async (
event: APIGatewayTokenAuthorizerEvent
): Promise<APIGatewayAuthorizerResult> => {
try {
const token = event.authorizationToken.replace('Bearer ', '');
const decoded = verify(token, JWT_SECRET) as JwtPayload;
return generatePolicy(decoded.sub!, 'Allow', event.methodArn, {
userId: decoded.sub,
email: decoded.email,
roles: decoded.roles?.join(',')
});
} catch (error) {
console.error('Authorization failed:', error);
return generatePolicy('anonymous', 'Deny', event.methodArn);
}
};
const generatePolicy = (
principalId: string,
effect: 'Allow' | 'Deny',
resource: string,
context?: Record<string, string>
): APIGatewayAuthorizerResult => {
return {
principalId,
policyDocument: {
Version: '2012-10-17',
Statement: [{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: resource.replace(/\/[^/]+\/[^/]+$/, '/*/*') // Allow all methods/resources
}]
},
context
};
};API Keys and Usage Plans
# usage-plans.tf
resource "aws_api_gateway_api_key" "partner" {
name = "partner-api-key"
}
resource "aws_api_gateway_usage_plan" "standard" {
name = "standard-plan"
description = "Standard rate limiting"
api_stages {
api_id = aws_api_gateway_rest_api.main.id
stage = aws_api_gateway_stage.prod.stage_name
}
quota_settings {
limit = 10000
period = "MONTH"
}
throttle_settings {
burst_limit = 100
rate_limit = 50
}
}
resource "aws_api_gateway_usage_plan" "premium" {
name = "premium-plan"
description = "Premium rate limiting"
api_stages {
api_id = aws_api_gateway_rest_api.main.id
stage = aws_api_gateway_stage.prod.stage_name
}
quota_settings {
limit = 100000
period = "MONTH"
}
throttle_settings {
burst_limit = 500
rate_limit = 200
}
}
resource "aws_api_gateway_usage_plan_key" "partner_standard" {
key_id = aws_api_gateway_api_key.partner.id
key_type = "API_KEY"
usage_plan_id = aws_api_gateway_usage_plan.standard.id
}VPC Integration
Private API with VPC Link
# vpc-link.tf
resource "aws_apigatewayv2_vpc_link" "main" {
name = "${var.project}-vpc-link"
security_group_ids = [aws_security_group.vpc_link.id]
subnet_ids = var.private_subnet_ids
}
resource "aws_apigatewayv2_integration" "alb" {
api_id = aws_apigatewayv2_api.main.id
integration_type = "HTTP_PROXY"
integration_uri = aws_lb_listener.api.arn
integration_method = "ANY"
connection_type = "VPC_LINK"
connection_id = aws_apigatewayv2_vpc_link.main.id
request_parameters = {
"overwrite:header.X-Forwarded-For" = "$request.header.X-Forwarded-For"
"overwrite:header.X-Request-ID" = "$context.requestId"
}
}
resource "aws_apigatewayv2_route" "proxy" {
api_id = aws_apigatewayv2_api.main.id
route_key = "ANY /{proxy+}"
target = "integrations/${aws_apigatewayv2_integration.alb.id}"
}Monitoring and Observability
CloudWatch Metrics and Alarms
# monitoring.tf
resource "aws_cloudwatch_metric_alarm" "api_5xx" {
alarm_name = "${var.project}-api-5xx"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "5XXError"
namespace = "AWS/ApiGateway"
period = 60
statistic = "Sum"
threshold = 10
alarm_description = "API Gateway 5XX errors"
alarm_actions = [aws_sns_topic.alerts.arn]
dimensions = {
ApiName = aws_apigatewayv2_api.main.name
Stage = aws_apigatewayv2_stage.main.name
}
}
resource "aws_cloudwatch_metric_alarm" "api_latency" {
alarm_name = "${var.project}-api-latency"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "Latency"
namespace = "AWS/ApiGateway"
period = 60
extended_statistic = "p95"
threshold = 3000
alarm_description = "API Gateway p95 latency"
alarm_actions = [aws_sns_topic.alerts.arn]
dimensions = {
ApiName = aws_apigatewayv2_api.main.name
Stage = aws_apigatewayv2_stage.main.name
}
}Key Takeaways
-
Choose the right type: HTTP API for simple, low-latency needs; REST API for full features
-
Validate requests: Use request validation to catch errors before they reach Lambda
-
Secure your API: Use JWT authorisers, API keys, or Lambda authorisers appropriately
-
Monitor actively: Set up alarms for 5XX errors, latency, and throttling
-
Use stages: Separate dev/staging/prod environments properly
-
Cache responses: Enable caching for read-heavy APIs to reduce latency and cost
-
VPC integration: Use VPC links for private backend services
-
Rate limiting: Protect your backend with throttling and usage plans
API Gateway handles the undifferentiated heavy lifting of API management, letting you focus on business logic rather than infrastructure.