Skip to content

AWS API Gateway Pattern for iDrv5

This document outlines how to implement an API Gateway pattern using AWS API Gateway to decouple modules and enforce well-defined API contracts between components.

This document outlines how to implement an API Gateway pattern using AWS API Gateway to decouple modules and enforce well-defined API contracts between components.

  1. Current Architecture Challenges
  2. API Gateway Pattern Overview
  3. AWS API Gateway Setup
  4. Module API Contracts
  5. Implementation Strategy
  6. Security Configuration
  7. Migration Approach
  8. Monitoring and Observability

The current iDrv5 architecture has modules that directly interact with each other’s blueprints and templates:

flowchart TB
subgraph Flask["Flask Application"]
OP[Operations Portal]
FM[Freight Management]
CRM[CRM Module]
AM[Asset Management]
OP <--> FM
FM <--> CRM
CRM <--> AM
OP <--> CRM
FM <--> AM
OP <--> AM
end
DI[/"Direct Imports & Coupling"/]
OP --- DI
FM --- DI
CRM --- DI
AM --- DI
style Flask fill:#ffebee,stroke:#c62828
style DI fill:#ffcdd2,stroke:#c62828

Problems:

  • Tight coupling between modules
  • Circular import risks
  • Difficult to scale individual components
  • No clear API contracts
  • Hard to test modules in isolation
  • Template dependencies across modules

The API Gateway pattern provides a single entry point for all client requests, routing them to appropriate backend services:

flowchart TB
subgraph Gateway["AWS API Gateway"]
subgraph Routes["/api/v1/*"]
RC["/rate-cards"]
CU["/customers"]
ZO["/zones"]
AS["/assets"]
end
end
RC --> RS[Rates Service]
CU --> CS[CRM Service]
ZO --> ZS[Zones Service]
AS --> ASS[Asset Service]
style Gateway fill:#e3f2fd,stroke:#1565c0
style Routes fill:#bbdefb,stroke:#1565c0
style RS fill:#c8e6c9,stroke:#2e7d32
style CS fill:#c8e6c9,stroke:#2e7d32
style ZS fill:#c8e6c9,stroke:#2e7d32
style ASS fill:#c8e6c9,stroke:#2e7d32

Benefits:

  • Clear API contracts between services
  • Independent scaling of components
  • Centralized authentication and authorization
  • Rate limiting and throttling
  • Request/response transformation
  • Caching at the gateway level
  • API versioning support

Using AWS CLI:

Terminal window
# Create REST API
aws apigateway create-rest-api \
--name "iDrv5-API-Gateway" \
--description "API Gateway for iDrv5 Logistics Platform" \
--endpoint-configuration types=REGIONAL \
--region ap-southeast-2
# Store the API ID
API_ID=$(aws apigateway get-rest-apis --query "items[?name=='iDrv5-API-Gateway'].id" --output text)
Terminal window
# Get root resource ID
ROOT_ID=$(aws apigateway get-resources --rest-api-id $API_ID --query "items[?path=='/'].id" --output text)
# Create /api resource
aws apigateway create-resource \
--rest-api-id $API_ID \
--parent-id $ROOT_ID \
--path-part "api"
# Create /api/v1 resource
API_RESOURCE_ID=$(aws apigateway get-resources --rest-api-id $API_ID --query "items[?path=='/api'].id" --output text)
aws apigateway create-resource \
--rest-api-id $API_ID \
--parent-id $API_RESOURCE_ID \
--path-part "v1"
Section titled “Step 3: Terraform Configuration (Recommended)”

Create infrastructure/api_gateway/main.tf:

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-southeast-2"
}
# API Gateway REST API
resource "aws_api_gateway_rest_api" "idrv5" {
name = "idrv5-api-gateway"
description = "API Gateway for iDrv5 Logistics Platform"
endpoint_configuration {
types = ["REGIONAL"]
}
tags = {
Environment = var.environment
Application = "iDrv5"
}
}
# /api resource
resource "aws_api_gateway_resource" "api" {
rest_api_id = aws_api_gateway_rest_api.idrv5.id
parent_id = aws_api_gateway_rest_api.idrv5.root_resource_id
path_part = "api"
}
# /api/v1 resource
resource "aws_api_gateway_resource" "v1" {
rest_api_id = aws_api_gateway_rest_api.idrv5.id
parent_id = aws_api_gateway_resource.api.id
path_part = "v1"
}
# Rate Cards Resource
resource "aws_api_gateway_resource" "rate_cards" {
rest_api_id = aws_api_gateway_rest_api.idrv5.id
parent_id = aws_api_gateway_resource.v1.id
path_part = "rate-cards"
}
# Zones Resource
resource "aws_api_gateway_resource" "zones" {
rest_api_id = aws_api_gateway_rest_api.idrv5.id
parent_id = aws_api_gateway_resource.v1.id
path_part = "zones"
}
# Customers Resource
resource "aws_api_gateway_resource" "customers" {
rest_api_id = aws_api_gateway_rest_api.idrv5.id
parent_id = aws_api_gateway_resource.v1.id
path_part = "customers"
}
# Users Resource
resource "aws_api_gateway_resource" "users" {
rest_api_id = aws_api_gateway_rest_api.idrv5.id
parent_id = aws_api_gateway_resource.v1.id
path_part = "users"
}
# Bookings Resource
resource "aws_api_gateway_resource" "bookings" {
rest_api_id = aws_api_gateway_rest_api.idrv5.id
parent_id = aws_api_gateway_resource.v1.id
path_part = "bookings"
}
# Assets Resource
resource "aws_api_gateway_resource" "assets" {
rest_api_id = aws_api_gateway_rest_api.idrv5.id
parent_id = aws_api_gateway_resource.v1.id
path_part = "assets"
}

Create infrastructure/api_gateway/integrations.tf:

# VPC Link for private integration (if using internal ALB)
resource "aws_api_gateway_vpc_link" "idrv5" {
name = "idrv5-vpc-link"
target_arns = [aws_lb.internal.arn]
}
# Rate Cards - GET method
resource "aws_api_gateway_method" "rate_cards_get" {
rest_api_id = aws_api_gateway_rest_api.idrv5.id
resource_id = aws_api_gateway_resource.rate_cards.id
http_method = "GET"
authorization = "CUSTOM"
authorizer_id = aws_api_gateway_authorizer.jwt.id
}
resource "aws_api_gateway_integration" "rate_cards_get" {
rest_api_id = aws_api_gateway_rest_api.idrv5.id
resource_id = aws_api_gateway_resource.rate_cards.id
http_method = aws_api_gateway_method.rate_cards_get.http_method
integration_http_method = "GET"
type = "HTTP_PROXY"
uri = "${var.backend_url}/api/rate-cards/"
connection_type = "VPC_LINK"
connection_id = aws_api_gateway_vpc_link.idrv5.id
request_parameters = {
"integration.request.header.X-Forwarded-For" = "context.identity.sourceIp"
"integration.request.header.X-Request-Id" = "context.requestId"
}
}
# Rate Cards - POST method
resource "aws_api_gateway_method" "rate_cards_post" {
rest_api_id = aws_api_gateway_rest_api.idrv5.id
resource_id = aws_api_gateway_resource.rate_cards.id
http_method = "POST"
authorization = "CUSTOM"
authorizer_id = aws_api_gateway_authorizer.jwt.id
}
resource "aws_api_gateway_integration" "rate_cards_post" {
rest_api_id = aws_api_gateway_rest_api.idrv5.id
resource_id = aws_api_gateway_resource.rate_cards.id
http_method = aws_api_gateway_method.rate_cards_post.http_method
integration_http_method = "POST"
type = "HTTP_PROXY"
uri = "${var.backend_url}/api/rate-cards/"
connection_type = "VPC_LINK"
connection_id = aws_api_gateway_vpc_link.idrv5.id
}

Create api/openapi/idrv5-api.yaml:

openapi: 3.0.3
info:
title: iDrv5 Logistics Platform API
description: API Gateway contracts for iDrv5 modules
version: 1.0.0
contact:
name: iDrv5 Development Team
servers:
- url: https://api.idrv5.example.com/api/v1
description: Production API Gateway
- url: https://api-staging.idrv5.example.com/api/v1
description: Staging API Gateway
- url: http://localhost:5002/api
description: Local development
paths:
/rate-cards:
get:
summary: List all rate cards
operationId: listRateCards
tags:
- Rate Management
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: per_page
in: query
schema:
type: integer
default: 20
- name: is_active
in: query
schema:
type: boolean
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/RateCardListResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalError'
post:
summary: Create a new rate card
operationId: createRateCard
tags:
- Rate Management
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RateCardCreate'
responses:
'201':
description: Rate card created
content:
application/json:
schema:
$ref: '#/components/schemas/RateCardResponse'
'400':
$ref: '#/components/responses/BadRequest'
'409':
$ref: '#/components/responses/Conflict'
/rate-cards/{id}:
get:
summary: Get rate card by ID
operationId: getRateCard
tags:
- Rate Management
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/RateCardResponse'
'404':
$ref: '#/components/responses/NotFound'
/zones:
get:
summary: List all zones
operationId: listZones
tags:
- Zone Management
parameters:
- name: state
in: query
schema:
type: string
- name: zone_type
in: query
schema:
type: string
enum: [Metro, Regional, Remote]
- name: is_active
in: query
schema:
type: boolean
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/ZoneListResponse'
/customers:
get:
summary: List customers
operationId: listCustomers
tags:
- Customer Management
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/CustomerListResponse'
/bookings:
get:
summary: List bookings
operationId: listBookings
tags:
- Booking Management
parameters:
- name: status
in: query
schema:
type: string
enum: [pending, confirmed, in_transit, delivered, cancelled]
- name: customer_id
in: query
schema:
type: integer
- name: from_date
in: query
schema:
type: string
format: date
- name: to_date
in: query
schema:
type: string
format: date
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/BookingListResponse'
post:
summary: Create a booking
operationId: createBooking
tags:
- Booking Management
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BookingCreate'
responses:
'201':
description: Booking created
content:
application/json:
schema:
$ref: '#/components/schemas/BookingResponse'
/quotes:
post:
summary: Generate a quote
operationId: createQuote
tags:
- Quoting
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/QuoteRequest'
responses:
'200':
description: Quote generated
content:
application/json:
schema:
$ref: '#/components/schemas/QuoteResponse'
components:
schemas:
RateCardListResponse:
type: object
properties:
success:
type: boolean
data:
type: array
items:
$ref: '#/components/schemas/RateCard'
pagination:
$ref: '#/components/schemas/Pagination'
RateCard:
type: object
properties:
id:
type: integer
name:
type: string
rate_type:
type: string
effective_date:
type: string
format: date
expiry_date:
type: string
format: date
is_active:
type: boolean
rate_calculation_method_id:
type: integer
description:
type: string
RateCardCreate:
type: object
required:
- name
- rate_type
- rate_calculation_method_id
properties:
name:
type: string
maxLength: 255
rate_type:
type: string
maxLength: 50
rate_calculation_method_id:
type: integer
effective_date:
type: string
format: date
expiry_date:
type: string
format: date
description:
type: string
is_active:
type: boolean
default: true
RateCardResponse:
type: object
properties:
success:
type: boolean
data:
$ref: '#/components/schemas/RateCard'
Zone:
type: object
properties:
id:
type: integer
name:
type: string
short_name:
type: string
zone_type:
type: string
state:
type: string
is_metro:
type: boolean
is_active:
type: boolean
ZoneListResponse:
type: object
properties:
success:
type: boolean
data:
type: array
items:
$ref: '#/components/schemas/Zone'
Customer:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
phone:
type: string
company:
type: string
is_active:
type: boolean
CustomerListResponse:
type: object
properties:
success:
type: boolean
data:
type: array
items:
$ref: '#/components/schemas/Customer'
Booking:
type: object
properties:
id:
type: integer
reference:
type: string
customer_id:
type: integer
status:
type: string
pickup_date:
type: string
format: date
delivery_date:
type: string
format: date
origin_zone_id:
type: integer
destination_zone_id:
type: integer
BookingCreate:
type: object
required:
- customer_id
- origin_zone_id
- destination_zone_id
- pickup_date
properties:
customer_id:
type: integer
origin_zone_id:
type: integer
destination_zone_id:
type: integer
pickup_date:
type: string
format: date
service_level_id:
type: integer
items:
type: array
items:
$ref: '#/components/schemas/FreightItem'
BookingResponse:
type: object
properties:
success:
type: boolean
data:
$ref: '#/components/schemas/Booking'
BookingListResponse:
type: object
properties:
success:
type: boolean
data:
type: array
items:
$ref: '#/components/schemas/Booking'
FreightItem:
type: object
properties:
description:
type: string
quantity:
type: integer
weight:
type: number
length:
type: number
width:
type: number
height:
type: number
QuoteRequest:
type: object
required:
- origin_zone_id
- destination_zone_id
- items
properties:
origin_zone_id:
type: integer
destination_zone_id:
type: integer
service_level_id:
type: integer
items:
type: array
items:
$ref: '#/components/schemas/FreightItem'
pickup_date:
type: string
format: date
QuoteResponse:
type: object
properties:
success:
type: boolean
data:
type: object
properties:
quote_id:
type: string
total_price:
type: number
breakdown:
type: object
transit_time:
type: integer
valid_until:
type: string
format: date-time
Pagination:
type: object
properties:
page:
type: integer
per_page:
type: integer
total:
type: integer
pages:
type: integer
Error:
type: object
properties:
success:
type: boolean
example: false
error:
type: string
code:
type: string
responses:
BadRequest:
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Unauthorized
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: Resource conflict
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
InternalError:
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
apiKey:
type: apiKey
in: header
name: X-API-Key
security:
- bearerAuth: []

Before introducing AWS API Gateway, standardize all internal APIs:

1. Create a base API response format:

Create utils/api_response.py:

from flask import jsonify
from functools import wraps
from typing import Any, Dict, Optional, List
class APIResponse:
"""Standardized API response format for all endpoints."""
@staticmethod
def success(
data: Any = None,
message: str = None,
status_code: int = 200,
pagination: Dict = None
):
response = {
'success': True,
'data': data
}
if message:
response['message'] = message
if pagination:
response['pagination'] = pagination
return jsonify(response), status_code
@staticmethod
def error(
error: str,
code: str = None,
status_code: int = 400,
details: Dict = None
):
response = {
'success': False,
'error': error
}
if code:
response['code'] = code
if details:
response['details'] = details
return jsonify(response), status_code
@staticmethod
def created(data: Any = None, message: str = 'Resource created'):
return APIResponse.success(data, message, 201)
@staticmethod
def not_found(resource: str = 'Resource'):
return APIResponse.error(f'{resource} not found', 'NOT_FOUND', 404)
@staticmethod
def conflict(message: str = 'Resource already exists'):
return APIResponse.error(message, 'CONFLICT', 409)
@staticmethod
def validation_error(errors: List[str]):
return APIResponse.error(
'Validation failed',
'VALIDATION_ERROR',
400,
{'validation_errors': errors}
)
def api_endpoint(f):
"""Decorator for consistent API error handling."""
@wraps(f)
def decorated(*args, **kwargs):
try:
return f(*args, **kwargs)
except ValueError as e:
return APIResponse.error(str(e), 'VALIDATION_ERROR', 400)
except PermissionError as e:
return APIResponse.error(str(e), 'FORBIDDEN', 403)
except LookupError as e:
return APIResponse.error(str(e), 'NOT_FOUND', 404)
except Exception as e:
# Log the error
import traceback
traceback.print_exc()
return APIResponse.error('Internal server error', 'INTERNAL_ERROR', 500)
return decorated

2. Create API service layer:

Create services/api_client.py:

import requests
from typing import Dict, Any, Optional
from flask import current_app
import os
class InternalAPIClient:
"""Client for making internal API calls through defined contracts."""
def __init__(self, base_url: str = None):
self.base_url = base_url or os.getenv('API_GATEWAY_URL', 'http://localhost:5002/api')
self.timeout = 30
def _request(
self,
method: str,
endpoint: str,
data: Dict = None,
params: Dict = None,
headers: Dict = None
) -> Dict[str, Any]:
url = f"{self.base_url}/{endpoint.lstrip('/')}"
default_headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
if headers:
default_headers.update(headers)
response = requests.request(
method=method,
url=url,
json=data,
params=params,
headers=default_headers,
timeout=self.timeout
)
return response.json()
def get(self, endpoint: str, params: Dict = None) -> Dict:
return self._request('GET', endpoint, params=params)
def post(self, endpoint: str, data: Dict = None) -> Dict:
return self._request('POST', endpoint, data=data)
def put(self, endpoint: str, data: Dict = None) -> Dict:
return self._request('PUT', endpoint, data=data)
def delete(self, endpoint: str) -> Dict:
return self._request('DELETE', endpoint)
class RateCardsAPI(InternalAPIClient):
"""API client for Rate Cards service."""
def list_rate_cards(self, page: int = 1, per_page: int = 20, is_active: bool = None):
params = {'page': page, 'per_page': per_page}
if is_active is not None:
params['is_active'] = is_active
return self.get('/rate-cards/', params)
def get_rate_card(self, rate_card_id: int):
return self.get(f'/rate-cards/{rate_card_id}')
def create_rate_card(self, data: Dict):
return self.post('/rate-cards/', data)
def update_rate_card(self, rate_card_id: int, data: Dict):
return self.put(f'/rate-cards/{rate_card_id}', data)
def delete_rate_card(self, rate_card_id: int):
return self.delete(f'/rate-cards/{rate_card_id}')
def get_rate_entries(self, rate_card_id: int):
return self.get(f'/rate-cards/{rate_card_id}/entries')
class ZonesAPI(InternalAPIClient):
"""API client for Zones service."""
def list_zones(self, state: str = None, zone_type: str = None):
params = {}
if state:
params['state'] = state
if zone_type:
params['zone_type'] = zone_type
return self.get('/zones/', params)
def get_zone(self, zone_id: int):
return self.get(f'/zones/{zone_id}')
def get_zone_suburbs(self, zone_id: int):
return self.get(f'/zones/{zone_id}/suburbs')
class CustomersAPI(InternalAPIClient):
"""API client for Customers service."""
def list_customers(self, search: str = None, is_active: bool = None):
params = {}
if search:
params['search'] = search
if is_active is not None:
params['is_active'] = is_active
return self.get('/customers/', params)
def get_customer(self, customer_id: int):
return self.get(f'/customers/{customer_id}')
def get_customer_rate_cards(self, customer_id: int):
return self.get(f'/customers/{customer_id}/rate-cards')
class QuotingAPI(InternalAPIClient):
"""API client for Quoting service."""
def generate_quote(self, data: Dict):
return self.post('/quotes/', data)
def get_quote(self, quote_id: str):
return self.get(f'/quotes/{quote_id}')

Refactor modules to use API clients instead of direct imports:

Before (tightly coupled):

# In portals/operations/routes.py
from modules.freight_management.routes import get_rate_cards
from models.rate_cards import RateCard
@operations_bp.route('/booking/new')
def new_booking():
# Direct database access
rate_cards = RateCard.query.filter_by(is_active=True).all()
# Direct function import
zones = get_zones_for_booking()
return render_template('booking.html', rate_cards=rate_cards, zones=zones)

After (decoupled via API):

# In portals/operations/routes.py
from services.api_client import RateCardsAPI, ZonesAPI
@operations_bp.route('/booking/new')
def new_booking():
rate_cards_api = RateCardsAPI()
zones_api = ZonesAPI()
# API calls instead of direct database access
rate_cards_response = rate_cards_api.list_rate_cards(is_active=True)
zones_response = zones_api.list_zones()
return render_template(
'booking.html',
rate_cards=rate_cards_response.get('data', []),
zones=zones_response.get('data', [])
)

1. Configure API Gateway with OpenAPI import:

Terminal window
aws apigateway import-rest-api \
--body 'file://api/openapi/idrv5-api.yaml' \
--region ap-southeast-2

2. Set up Lambda authorizer for JWT validation:

Create infrastructure/lambda/authorizer.py:

import jwt
import os
import json
def handler(event, context):
"""JWT Token Authorizer for API Gateway."""
token = event.get('authorizationToken', '').replace('Bearer ', '')
method_arn = event['methodArn']
try:
# Decode and verify JWT
payload = jwt.decode(
token,
os.environ['JWT_SECRET'],
algorithms=['HS256']
)
# Generate allow policy
return generate_policy(
payload['sub'],
'Allow',
method_arn,
context={
'user_id': payload.get('user_id'),
'role': payload.get('role'),
'tenant_id': payload.get('tenant_id')
}
)
except jwt.ExpiredSignatureError:
raise Exception('Unauthorized: Token expired')
except jwt.InvalidTokenError:
raise Exception('Unauthorized: Invalid token')
def generate_policy(principal_id, effect, resource, context=None):
policy = {
'principalId': principal_id,
'policyDocument': {
'Version': '2012-10-17',
'Statement': [{
'Action': 'execute-api:Invoke',
'Effect': effect,
'Resource': resource
}]
}
}
if context:
policy['context'] = context
return policy

3. Update environment configuration:

Add to .env.production:

Terminal window
# AWS API Gateway
API_GATEWAY_URL=https://api.idrv5.example.com/api/v1
API_GATEWAY_REGION=ap-southeast-2
API_GATEWAY_STAGE=prod
# JWT Configuration
JWT_SECRET=your-secret-key
JWT_ALGORITHM=HS256
JWT_EXPIRY_HOURS=24

sequenceDiagram
participant C as Client
participant AG as API Gateway
participant A as Authorizer
participant B as Backend
C->>AG: 1. Request + JWT Token
AG->>A: 2. Validate Token
A-->>AG: 3. Auth Context
AG->>B: 4. Forward Request + Auth Context
B-->>AG: 5. Response
AG-->>C: 6. Response

Create infrastructure/api_gateway/api_keys.tf:

# API Usage Plan
resource "aws_api_gateway_usage_plan" "standard" {
name = "idrv5-standard"
description = "Standard usage plan for iDrv5 API"
api_stages {
api_id = aws_api_gateway_rest_api.idrv5.id
stage = aws_api_gateway_stage.prod.stage_name
}
quota_settings {
limit = 10000
period = "DAY"
}
throttle_settings {
burst_limit = 100
rate_limit = 50
}
}
resource "aws_api_gateway_usage_plan" "premium" {
name = "idrv5-premium"
description = "Premium usage plan for iDrv5 API"
api_stages {
api_id = aws_api_gateway_rest_api.idrv5.id
stage = aws_api_gateway_stage.prod.stage_name
}
quota_settings {
limit = 100000
period = "DAY"
}
throttle_settings {
burst_limit = 500
rate_limit = 200
}
}
# API Keys for external integrations
resource "aws_api_gateway_api_key" "partner_api" {
name = "partner-integration-key"
description = "API key for partner integrations"
enabled = true
}
resource "aws_api_gateway_usage_plan_key" "partner" {
key_id = aws_api_gateway_api_key.partner_api.id
key_type = "API_KEY"
usage_plan_id = aws_api_gateway_usage_plan.standard.id
}

Add to infrastructure/api_gateway/cors.tf:

# CORS configuration for all resources
resource "aws_api_gateway_method" "options" {
for_each = toset([
aws_api_gateway_resource.rate_cards.id,
aws_api_gateway_resource.zones.id,
aws_api_gateway_resource.customers.id,
aws_api_gateway_resource.bookings.id
])
rest_api_id = aws_api_gateway_rest_api.idrv5.id
resource_id = each.value
http_method = "OPTIONS"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "options" {
for_each = aws_api_gateway_method.options
rest_api_id = aws_api_gateway_rest_api.idrv5.id
resource_id = each.value
http_method = aws_api_gateway_method.options[each.key].http_method
type = "MOCK"
request_templates = {
"application/json" = jsonencode({ statusCode = 200 })
}
}
resource "aws_api_gateway_method_response" "options_200" {
for_each = aws_api_gateway_method.options
rest_api_id = aws_api_gateway_rest_api.idrv5.id
resource_id = each.value
http_method = aws_api_gateway_method.options[each.key].http_method
status_code = "200"
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = true
"method.response.header.Access-Control-Allow-Methods" = true
"method.response.header.Access-Control-Allow-Origin" = true
}
}
resource "aws_api_gateway_integration_response" "options" {
for_each = aws_api_gateway_method.options
rest_api_id = aws_api_gateway_rest_api.idrv5.id
resource_id = each.value
http_method = aws_api_gateway_method.options[each.key].http_method
status_code = aws_api_gateway_method_response.options_200[each.key].status_code
response_parameters = {
"method.response.header.Access-Control-Allow-Headers" = "'Content-Type,Authorization,X-Api-Key'"
"method.response.header.Access-Control-Allow-Methods" = "'GET,POST,PUT,DELETE,OPTIONS'"
"method.response.header.Access-Control-Allow-Origin" = "'*'"
}
}

flowchart TB
subgraph P1["Phase 1: Preparation"]
direction LR
P1A[Standardize API response formats] --> P1B[Create OpenAPI specifications]
P1B --> P1C[Implement API service layer]
P1C --> P1D[Add API versioning support]
end
subgraph P2["Phase 2: Internal Decoupling"]
direction LR
P2A[Refactor module imports] --> P2B[Remove template dependencies]
P2B --> P2C[Event-driven communication]
P2C --> P2D[Add integration tests]
end
subgraph P3["Phase 3: AWS Infrastructure"]
direction LR
P3A[Set up API Gateway] --> P3B[Configure Lambda authorizer]
P3B --> P3C[Set up VPC Link]
P3C --> P3D[Configure monitoring]
end
subgraph P4["Phase 4: Traffic Migration"]
direction LR
P4A[Deploy with feature flags] --> P4B[Route 10% traffic]
P4B --> P4C[Monitor performance]
P4C --> P4D[Increase to 100%]
end
subgraph P5["Phase 5: Optimization"]
direction LR
P5A[Enable API caching] --> P5B[Tune rate limiting]
P5B --> P5C[Add request validation]
P5C --> P5D[Response transformation]
end
P1 ==> P2 ==> P3 ==> P4 ==> P5
style P1 fill:#e8f5e9,stroke:#2e7d32
style P2 fill:#e3f2fd,stroke:#1565c0
style P3 fill:#fff3e0,stroke:#ef6c00
style P4 fill:#fce4ec,stroke:#c2185b
style P5 fill:#f3e5f5,stroke:#7b1fa2

Create utils/feature_flags.py:

import os
from functools import wraps
class FeatureFlags:
"""Feature flags for gradual API Gateway migration."""
@staticmethod
def use_api_gateway():
return os.getenv('USE_API_GATEWAY', 'false').lower() == 'true'
@staticmethod
def api_gateway_percentage():
return int(os.getenv('API_GATEWAY_PERCENTAGE', '0'))
def with_api_fallback(api_call, direct_call):
"""
Execute API call with fallback to direct call.
Used during migration period.
"""
import random
if not FeatureFlags.use_api_gateway():
return direct_call()
if random.randint(1, 100) > FeatureFlags.api_gateway_percentage():
return direct_call()
try:
return api_call()
except Exception as e:
# Log the error and fallback
print(f"API Gateway call failed: {e}, falling back to direct call")
return direct_call()

Create infrastructure/api_gateway/monitoring.tf:

# CloudWatch Dashboard
resource "aws_cloudwatch_dashboard" "api_gateway" {
dashboard_name = "iDrv5-API-Gateway"
dashboard_body = jsonencode({
widgets = [
{
type = "metric"
x = 0
y = 0
width = 12
height = 6
properties = {
metrics = [
["AWS/ApiGateway", "Count", "ApiName", aws_api_gateway_rest_api.idrv5.name],
[".", "4XXError", ".", "."],
[".", "5XXError", ".", "."]
]
period = 300
region = var.aws_region
title = "API Requests and Errors"
}
},
{
type = "metric"
x = 12
y = 0
width = 12
height = 6
properties = {
metrics = [
["AWS/ApiGateway", "Latency", "ApiName", aws_api_gateway_rest_api.idrv5.name, { stat = "Average" }],
["...", { stat = "p99" }]
]
period = 300
region = var.aws_region
title = "API Latency"
}
}
]
})
}
# Alarm for high error rate
resource "aws_cloudwatch_metric_alarm" "api_5xx_errors" {
alarm_name = "idrv5-api-5xx-errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "5XXError"
namespace = "AWS/ApiGateway"
period = 300
statistic = "Sum"
threshold = 10
alarm_description = "High 5XX error rate on API Gateway"
dimensions = {
ApiName = aws_api_gateway_rest_api.idrv5.name
}
alarm_actions = [aws_sns_topic.alerts.arn]
}
# Alarm for high latency
resource "aws_cloudwatch_metric_alarm" "api_high_latency" {
alarm_name = "idrv5-api-high-latency"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 3
metric_name = "Latency"
namespace = "AWS/ApiGateway"
period = 300
extended_statistic = "p99"
threshold = 5000 # 5 seconds
alarm_description = "High API latency detected"
dimensions = {
ApiName = aws_api_gateway_rest_api.idrv5.name
}
alarm_actions = [aws_sns_topic.alerts.arn]
}
# Enable access logging
resource "aws_api_gateway_stage" "prod" {
deployment_id = aws_api_gateway_deployment.prod.id
rest_api_id = aws_api_gateway_rest_api.idrv5.id
stage_name = "prod"
access_log_settings {
destination_arn = aws_cloudwatch_log_group.api_gateway.arn
format = jsonencode({
requestId = "$context.requestId"
ip = "$context.identity.sourceIp"
caller = "$context.identity.caller"
user = "$context.identity.user"
requestTime = "$context.requestTime"
httpMethod = "$context.httpMethod"
resourcePath = "$context.resourcePath"
status = "$context.status"
protocol = "$context.protocol"
responseLength = "$context.responseLength"
latency = "$context.responseLatency"
})
}
xray_tracing_enabled = true
}
resource "aws_cloudwatch_log_group" "api_gateway" {
name = "/aws/api-gateway/idrv5"
retention_in_days = 30
}

ServiceEndpointMethods
Rate Cards/api/v1/rate-cardsGET, POST
Rate Cards/api/v1/rate-cards/{id}GET, PUT, DELETE
Zones/api/v1/zonesGET, POST
Zones/api/v1/zones/{id}GET, PUT, DELETE
Customers/api/v1/customersGET, POST
Customers/api/v1/customers/{id}GET, PUT, DELETE
Bookings/api/v1/bookingsGET, POST
Bookings/api/v1/bookings/{id}GET, PUT, DELETE
Quotes/api/v1/quotesPOST
Users/api/v1/usersGET, POST
Assets/api/v1/assetsGET, POST
Terminal window
# Deploy API Gateway changes
terraform apply -target=module.api_gateway
# View API Gateway logs
aws logs tail /aws/api-gateway/idrv5 --follow
# Test API endpoint
curl -H "Authorization: Bearer $TOKEN" \
https://api.idrv5.example.com/api/v1/rate-cards
# Generate OpenAPI client
openapi-generator generate -i api/openapi/idrv5-api.yaml -g python -o sdk/python
# Invalidate API Gateway cache
aws apigateway flush-stage-cache \
--rest-api-id $API_ID \
--stage-name prod
VariableDescriptionExample
API_GATEWAY_URLGateway base URLhttps://api.idrv5.example.com/api/v1
USE_API_GATEWAYEnable gateway routingtrue
API_GATEWAY_PERCENTAGETraffic percentage100
JWT_SECRETJWT signing secretyour-secret-key
API_GATEWAY_REGIONAWS regionap-southeast-2