Defining Your AWS API Gateway in Python
Defining an API in Terraform is painful.
The resource model for aws_api_gateway_rest_api is verbose, deeply nested, and disconnected from the code it is meant to describe.
You write the route in Python, then write it again in HCL: the method, the integration type, the request mapping, the response codes.
Neither file references the other.
They are two descriptions of the same interface, maintained in parallel, and they drift.
A route is renamed in Python; the integration config is not updated.
Worse, the OpenAPI spec can be generated from either fastapi or API Gateway, so we now have two sources of truth for the documentation.
fastapi-aws makes the Python code the single source of truth for both the API structure and its infrastructure integration.
The route definition, the authorizer, the CORS policy, and the Lambda binding all live in one file.
The integration spec for API Gateway is generated from that file, not written alongside it.
Future work will extend the same pattern to Kong, covering self-hosted and on-premises deployments where AWS API Gateway is not an option.
How It Works
The library exports AWSAPIRouter, which replaces FastAPI’s standard router.
Routes are defined in the usual way, with extra keyword arguments that specify the AWS integration:
from fastapi_aws import AWSAPIRouter, LambdaAuthorizer
bearer_auth = LambdaAuthorizer(
authorizer_name="${bearer_authorizer_name}",
aws_lambda_uri="${lambda_authorizer_uri}",
aws_iam_role_arn="${lambda_authorizer_iam_role_arn}",
header_names=["Authorization", "x-api-key"]
)
router = AWSAPIRouter()
app = FastAPI()
app.router = router # must replace the router before adding routes
@router.get(
"/user/{name}",
aws_lambda_uri="${user_function_arn}",
tags=["user"],
)
async def get_user(name: str, user=Security(bearer_auth)):
return {"name": name}
The ${user_function_arn} string is a Terraform template placeholder, not a real ARN.
The Python handler body will not be executed; it exists as a type signature and documentation anchor.
The router reads the aws_lambda_uri kwarg and embeds it as the x-amazon-apigateway-integration extension in the exported OpenAPI spec.
From this definition, the library produces two specifications via a CLI command:
fastapi_aws \
--title my-api \
--router my-routes.py \
--out-public ./api_public.json \
--out-private ./api_private.json \
--version 0.0.1
The private spec contains the integration annotations and CORS definitions that API Gateway needs. The public spec is clean OpenAPI, suitable for documentation tools and client generation. The same Python file produces both; neither is written by hand.
The Terraform Connection
The private spec slots directly into Terraform’s templatefile():
data "template_file" "openapi_spec" {
template = file("${path.module}/api_private.json")
vars = {
user_function_arn = aws_lambda_function.user.arn
bearer_authorizer_name = "bearer-auth"
lambda_authorizer_uri = aws_lambda_function.authorizer.invoke_arn
lambda_authorizer_iam_role_arn = aws_iam_role.apigw.arn
}
}
resource "aws_api_gateway_rest_api" "api" {
name = "my-api"
body = data.template_file.openapi_spec.rendered
}
The placeholder strings in the Python route definitions match the Terraform variable names. Terraform substitutes them at apply time. The Python file, the generated spec, and the Terraform resource are tied together by the variable names – change one and the build fails rather than silently misrouting requests.
CORS definitions are generated automatically from the route definitions. Authorizers declared in Python appear in the spec without separate infrastructure configuration: API Gateway reads the authorizer from the spec and provisions it alongside the routes.
There are direct integrations a number of AWS services, including:
DynamoDBS3SNSStep FunctionsMock responses
In Practice
This library runs behind Marigold, Clientlog, and Popstory – three projects we’ve built with distinct route profiles and authorizer configurations, all defined in Python and deployed via the same Terraform pattern. The spec generation step sits in the CI pipeline alongside the container build:
build/api-definition:
docker run -it --rm \
-v $(shell pwd)/src/api:/app/routes:ro \
-v $(shell pwd)/terraform/rest:/out \
fastapi_aws \
--title $(APP_NAME) \
--router routes.routes:router \
--out-public /out/api_public.json \
--out-private /out/api_private.json \
--version $(GIT_TAG)
The Docker image variant is useful in CI where installing the Python dependencies is inconvenient; it runs the spec generation against a mounted routes directory without touching the rest of the build environment.
One constraint worth knowing: the AWSAPIRouter must replace app.router before any routes are added.
FastAPI sanitises route kwargs when using app.include_router(); attaching the router after the fact loses the AWS-specific kwargs and raises a ValueError.
The README is explicit about this, but it is the kind of constraint that costs time if you encounter it mid-refactor.
The aws_dynamodb_table_name integration (direct DynamoDB PutItem and GetItem via API Gateway, without a Lambda intermediary) is present in the codebase but marked as incomplete in places.
Verify current support against the repository before relying on it.
(The source is at github.com/bayinfosys/fastapi-aws. If the AWS integration pattern described here is relevant to a project you are building, get in touch.)
This article reflects fastapi-aws as of April 2026. The AWSAPIRouter interface and CLI export are stable. Verify integration kwargs and Terraform variable conventions against the current README before deploying.