Skip to main content
Back to Blog
27 September 202514 min read

AWS API Gateway: Building REST and HTTP APIs

AWSAPI GatewayRESTServerlessCloud

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, dashboards

Feature Comparison

FeatureREST APIHTTP API
CostHigher~70% lower
Latency~30ms overhead~10ms overhead
JWT AuthorisationLambda requiredBuilt-in
API KeysYesNo
Usage PlansYesNo
Request ValidationYesNo
WAF IntegrationYesNo
Private APIsYesYes
VPC LinkYesYes

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-time

Request 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

  1. Choose the right type: HTTP API for simple, low-latency needs; REST API for full features

  2. Validate requests: Use request validation to catch errors before they reach Lambda

  3. Secure your API: Use JWT authorisers, API keys, or Lambda authorisers appropriately

  4. Monitor actively: Set up alarms for 5XX errors, latency, and throttling

  5. Use stages: Separate dev/staging/prod environments properly

  6. Cache responses: Enable caching for read-heavy APIs to reduce latency and cost

  7. VPC integration: Use VPC links for private backend services

  8. 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.

Share this article