FastAPI
FastAPI is a framework for building APIs in Python.
The FastAPI generator creates a new FastAPI with AWS CDK or Terraform infrastructure setup. The generated backend uses AWS Lambda for serverless deployment, exposed via an AWS API Gateway API. It sets up AWS Lambda Powertools for observability, including logging, AWS X-Ray tracing and Cloudwatch Metrics.
Generate a FastAPI
Section titled “Generate a FastAPI”You can generate a new FastAPI in two ways:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - py#api - Fill in the required parameters
- framework: fastapi
- Click
Generate
pnpm nx g @aws/nx-plugin:py#api --framework=fastapiyarn nx g @aws/nx-plugin:py#api --framework=fastapinpx nx g @aws/nx-plugin:py#api --framework=fastapibunx nx g @aws/nx-plugin:py#api --framework=fastapiYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:py#api --framework=fastapi --dry-runyarn nx g @aws/nx-plugin:py#api --framework=fastapi --dry-runnpx nx g @aws/nx-plugin:py#api --framework=fastapi --dry-runbunx nx g @aws/nx-plugin:py#api --framework=fastapi --dry-runOptions
Section titled “Options”| Parameter | Type | Default | Description |
|---|---|---|---|
| name Required | string | - | Name of the API project to generate |
| framework | fastapi | fastapi | The API framework to use. |
| integrationPattern | isolated | shared | isolated | How API Gateway integrations are generated for the API. Choose between isolated (default) and shared. |
| auth | iam | cognito | custom | iam | The method used to authenticate with your API. Choose between iam (default), cognito or custom. |
| directory | string | packages | The directory to store the application in. |
| subDirectory | string | - | The sub directory the project is placed in. By default this is the project name. |
| iac | inherit | cdk | terraform | inherit | The preferred IaC provider. By default this is inherited from your initial selection. |
| moduleName | string | - | Python module name |
| infra | rest-lambda | http-lambda | none | rest-lambda | The type of infrastructure to use to deploy this API. |
| preferInstallDependencies | boolean | true | Whether to prefer installing dependencies after the generator runs. Set to false to defer installing when batching multiple generators (an install still runs if needed so subsequent generators can compute the Nx project graph); install once at the end. |
Generator Output
Section titled “Generator Output”The generator will create the following project structure in the <directory>/<api-name> directory:
- project.json Project configuration and build targets
- pyproject.toml Python project configuration and dependencies
- run.sh Lambda Web Adapter bootstrap script to start the FastAPI app via uvicorn
Directory<module_name>
- __init__.py Module initialisation
- init.py Sets the up FastAPI app and configures powertools middleware
- main.py API implementation
Directoryscripts
- generate_open_api.py Script to generate an OpenAPI schema from the FastAPI app
Infrastructure
Section titled “Infrastructure”Since this generator vends infrastructure as code based on your chosen iacProvider, it will create a project in packages/common which includes the relevant CDK constructs or Terraform modules.
The common infrastructure as code project is structured as follows:
Directorypackages/common/constructs
Directorysrc
Directoryapp/ Constructs for infrastructure specific to a project/generator
- …
Directorycore/ Generic constructs which are reused by constructs in
app- …
- index.ts Entry point exporting constructs from
app
- project.json Project build targets and configuration
Directorypackages/common/terraform
Directorysrc
Directoryapp/ Terraform modules for infrastructure specific to a project/generator
- …
Directorycore/ Generic modules which are reused by modules in
app- …
- project.json Project build targets and configuration
For deploying your API, the following files are generated:
Directorypackages/common/constructs/src
Directoryapp
Directoryapis
- <project-name>.ts CDK construct for deploying your API
Directorycore
Directoryapi
- http-api.ts CDK construct for deploying an HTTP API (if you selected to deploy an HTTP API)
- rest-api.ts CDK construct for deploying a REST API (if you selected to deploy a REST API)
- utils.ts Utilities for the API constructs
Directorypackages/common/terraform/src
Directoryapp
Directoryapis
Directory<project-name>
- <project-name>.tf Module for deploying your API
Directorycore
Directoryapi
Directoryhttp-api
- http-api.tf Module for deploying an HTTP API (if you selected to deploy an HTTP API)
Directoryrest-api
- rest-api.tf Module for deploying a REST API (if you selected to deploy a REST API)
Architecture
Section titled “Architecture”The deployed application has the following architecture:
REST APIs include an AWS WAFv2 Web ACL in front of the API Gateway stage with the AWS managed default ruleset enabled.
HTTP APIs do not support WAF directly — if you need WAF protection, choose REST API instead or front the HTTP API with a CloudFront distribution.
Implementing your FastAPI
Section titled “Implementing your FastAPI”The main API implementation is in main.py. This is where you define your API routes and their implementations. Here’s an example:
from pydantic import BaseModelfrom .init import app, tracer
class Item(BaseModel): name: str
@app.get("/items/{item_id}")@tracer.capture_methoddef get_item(item_id: int) -> Item: return Item(name=...)
@app.post("/items")@tracer.capture_methoddef create_item(item: Item): return ...The generator sets up several features automatically:
- AWS Lambda Powertools integration for observability
- Error handling middleware
- Request/response correlation
- Metrics collection
- AWS Lambda deployment via Lambda Web Adapter with uvicorn
- Type-safe streaming (REST API only)
Observability with AWS Lambda Powertools
Section titled “Observability with AWS Lambda Powertools”Logging
Section titled “Logging”The generator configures structured logging using AWS Lambda Powertools. You can access the logger in your route handlers:
from .init import app, logger
@app.get("/items/{item_id}")def read_item(item_id: int): logger.info("Fetching item", extra={"item_id": item_id}) return {"item_id": item_id}The logger automatically includes:
- Correlation IDs for request tracing
- Request path and method
- Lambda context information
- Cold start indicators
Tracing
Section titled “Tracing”AWS X-Ray tracing is configured automatically. You can add custom subsegments to your traces:
from .init import app, tracer
@app.get("/items/{item_id}")@tracer.capture_methoddef read_item(item_id: int): # Creates a new subsegment with tracer.provider.in_subsegment("fetch-item-details"): # Your logic here return {"item_id": item_id}Metrics
Section titled “Metrics”CloudWatch metrics are collected automatically for each request. You can add custom metrics:
from .init import app, metricsfrom aws_lambda_powertools.metrics import MetricUnit
@app.get("/items/{item_id}")def read_item(item_id: int): metrics.add_metric(name="ItemViewed", unit=MetricUnit.Count, value=1) return {"item_id": item_id}Default metrics include:
- Request counts
- Success/failure counts
- Cold start metrics
- Per-route metrics
Error Handling
Section titled “Error Handling”The generator includes comprehensive error handling:
from fastapi import HTTPException
@app.get("/items/{item_id}")def read_item(item_id: int): if item_id < 0: raise HTTPException(status_code=400, detail="Item ID must be positive") return {"item_id": item_id}Unhandled exceptions are caught by the middleware and:
- Log the full exception with stack trace
- Record a failure metric
- Return a safe 500 response to the client
- Preserve the correlation ID
Accessing the Calling User
Section titled “Accessing the Calling User”When your API is protected by authentication, your route handlers often need to know who is calling. The generated FastAPI runs inside AWS Lambda via the Lambda Web Adapter, which forwards the API Gateway request context as JSON on the x-amzn-request-context header. You can read it from the FastAPI Request to extract the caller’s identity.
As an example, let’s add a /me endpoint that returns details about the calling user. We’ll implement the extraction as a FastAPI dependency so it can be reused across routes. The shape of the request context — and therefore how you extract the identity — depends on both your selected auth method and whether you deployed a REST or HTTP API.
For IAM authentication, we look up the caller in Cognito using the sub extracted from the API Gateway request context. Create identity.py alongside main.py:
import jsonimport osfrom typing import Annotated
from boto3 import clientfrom fastapi import Depends, HTTPException, Requestfrom pydantic import BaseModel
cognito = client("cognito-idp")
class Identity(BaseModel): sub: str username: str
def get_identity(request: Request) -> Identity: # The Lambda Web Adapter forwards the API Gateway request context as JSON request_context_header = request.headers.get("x-amzn-request-context") if not request_context_header: raise HTTPException(status_code=403, detail="Unable to determine calling user")
request_context = json.loads(request_context_header) provider = request_context.get("identity", {}).get("cognitoAuthenticationProvider")
sub = provider.split(":")[-1] if provider else None if not sub: raise HTTPException(status_code=403, detail="Unable to determine calling user")
users = cognito.list_users( # Assumes user pool id is configured in lambda environment UserPoolId=os.environ["USER_POOL_ID"], Limit=1, Filter=f'sub="{sub}"', ).get("Users", [])
if len(users) != 1: raise HTTPException(status_code=403, detail=f"No user found with subjectId {sub}")
return Identity(sub=sub, username=users[0]["Username"])
CurrentUser = Annotated[Identity, Depends(get_identity)]import jsonimport osfrom typing import Annotated
from boto3 import clientfrom fastapi import Depends, HTTPException, Requestfrom pydantic import BaseModel
cognito = client("cognito-idp")
class Identity(BaseModel): sub: str username: str
def get_identity(request: Request) -> Identity: # The Lambda Web Adapter forwards the API Gateway request context as JSON request_context_header = request.headers.get("x-amzn-request-context") if not request_context_header: raise HTTPException(status_code=403, detail="Unable to determine calling user")
request_context = json.loads(request_context_header) amr = ( request_context.get("authorizer", {}) .get("iam", {}) .get("cognitoIdentity", {}) .get("amr", []) ) sign_in = next((s for s in amr if ":CognitoSignIn:" in s), None) sub = sign_in.split(":")[-1] if sign_in else None
if not sub: raise HTTPException(status_code=403, detail="Unable to determine calling user")
users = cognito.list_users( # Assumes user pool id is configured in lambda environment UserPoolId=os.environ["USER_POOL_ID"], Limit=1, Filter=f'sub="{sub}"', ).get("Users", [])
if len(users) != 1: raise HTTPException(status_code=403, detail=f"No user found with subjectId {sub}")
return Identity(sub=sub, username=users[0]["Username"])
CurrentUser = Annotated[Identity, Depends(get_identity)]With auth: 'cognito', the API Gateway Cognito User Pools authorizer verifies the JWT that the caller supplies in the Authorization header and places the verified claims on the request context.
Create identity.py alongside main.py:
import jsonfrom typing import Annotated
from fastapi import Depends, HTTPException, Requestfrom pydantic import BaseModel
class Identity(BaseModel): sub: str username: str
def get_identity(request: Request) -> Identity: # The Lambda Web Adapter forwards the API Gateway request context as JSON request_context_header = request.headers.get("x-amzn-request-context") if not request_context_header: raise HTTPException(status_code=403, detail="Unable to determine calling user")
request_context = json.loads(request_context_header) claims = request_context.get("authorizer", {}).get("claims", {})
sub = claims.get("sub") username = claims.get("username")
if not sub or not username: raise HTTPException(status_code=403, detail="Unable to determine calling user")
return Identity(sub=sub, username=username)
CurrentUser = Annotated[Identity, Depends(get_identity)]HTTP APIs use a JWT authorizer which places the verified claims under authorizer.jwt.claims:
import jsonfrom typing import Annotated
from fastapi import Depends, HTTPException, Requestfrom pydantic import BaseModel
class Identity(BaseModel): sub: str username: str
def get_identity(request: Request) -> Identity: # The Lambda Web Adapter forwards the API Gateway request context as JSON request_context_header = request.headers.get("x-amzn-request-context") if not request_context_header: raise HTTPException(status_code=403, detail="Unable to determine calling user")
request_context = json.loads(request_context_header) claims = request_context.get("authorizer", {}).get("jwt", {}).get("claims", {})
sub = claims.get("sub") username = claims.get("username")
if not sub or not username: raise HTTPException(status_code=403, detail="Unable to determine calling user")
return Identity(sub=sub, username=username)
CurrentUser = Annotated[Identity, Depends(get_identity)]You can then inject the CurrentUser dependency into any route that needs the caller’s identity:
from .identity import CurrentUser, Identityfrom .init import app, tracer
@app.get("/me")@tracer.capture_methoddef me(identity: CurrentUser) -> Identity: return identityStreaming
Section titled “Streaming”The generated FastAPI supports streaming responses out of the box when using a REST API. The infrastructure is configured to use the AWS Lambda Web Adapter to run your FastAPI via uvicorn inside Lambda, with ResponseTransferMode.STREAM in API Gateway for all REST API operations, which enables streaming to work alongside non-streaming operations.
Using JsonStreamingResponse
Section titled “Using JsonStreamingResponse”The generated init.py exports a JsonStreamingResponse class that provides type-safe streaming with proper OpenAPI schema generation. This ensures that the connection generator can produce correctly typed streaming client methods.
from pydantic import BaseModelfrom .init import app, JsonStreamingResponse
class Chunk(BaseModel): message: str
async def generate_chunks(): for i in range(100): yield Chunk(message=f"This is chunk {i}")
@app.post( "/stream", response_class=JsonStreamingResponse, responses={200: JsonStreamingResponse.openapi_response(Chunk, "Stream of chunks")},)async def my_stream() -> JsonStreamingResponse: return JsonStreamingResponse(generate_chunks())The JsonStreamingResponse class:
- Serializes Pydantic models to JSON Lines format (
application/jsonl) - Provides an
openapi_responsehelper that generates the correct OpenAPI schema withitemSchema, enabling theconnectiongenerator to produce type-safe streaming client methods
Consumption
Section titled “Consumption”To consume a stream of responses, you can make use of the connection generator which will provide a type-safe method for iterating over your streamed chunks.
Deploying your FastAPI
Section titled “Deploying your FastAPI”The FastAPI generator creates CDK or Terraform infrastructure as code based on your selected iacProvider. You can use this to deploy your FastAPI.
The CDK construct for deploying your API in the common/constructs folder. You can use this in a CDK application:
import { MyApi } from ':my-scope/common-constructs';
export class ExampleStack extends Stack { constructor(scope: Construct, id: string) { // Add the api to your stack const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), }); }}This sets up:
- An AWS Lambda function for each operation in the FastAPI application
- API Gateway HTTP/REST API as the function trigger
- IAM roles and permissions
- CloudWatch log group
- X-Ray tracing configuration
- CloudWatch metrics namespace
The Terraform modules for deploying your API are in the common/terraform folder. You can use this in a Terraform configuration.
The API module stages its Lambda deployment zip in a shared S3 asset bucket — see the Terraform infrastructure guide for details. Instantiate the core/asset-bucket module once per deployment and pass its bucket_name output into every API / Lambda module via the asset_bucket_name input:
module "asset_bucket" { source = "../../common/terraform/src/core/asset-bucket"}
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Environment variables for the Lambda function env = { ENVIRONMENT = var.environment LOG_LEVEL = "INFO" }
# Additional IAM policies if needed additional_iam_policy_statements = [ # Add any additional permissions your API needs ]
tags = local.common_tags}This sets up:
- An AWS Lambda function that serves all FastAPI routes
- API Gateway HTTP/REST API as the function trigger
- IAM roles and permissions
- CloudWatch log group
- X-Ray tracing configuration
- CORS configuration
The Terraform module provides several outputs you can use:
# Access the API endpointoutput "api_url" { value = module.my_api.stage_invoke_url}
# Access Lambda function detailsoutput "lambda_function_name" { value = module.my_api.lambda_function_name}
# Access IAM role for granting additional permissionsoutput "lambda_execution_role_arn" { value = module.my_api.lambda_execution_role_arn}You can customize CORS settings by passing variables to the module:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Custom CORS configuration cors_allow_origins = ["https://myapp.com", "https://staging.myapp.com"] cors_allow_methods = ["GET", "POST", "PUT", "DELETE"] cors_allow_headers = [ "authorization", "content-type", "x-custom-header" ]
tags = local.common_tags}For REST APIs, the generated construct associates an AWS WAFv2 Web ACL with the API Gateway stage by default. The Web ACL uses the AWS managed default ruleset (AWSManagedRulesCommonRuleSet and AWSManagedRulesKnownBadInputsRuleSet), providing protection against common web exploits including the OWASP Top 10. WAF request logs are written to a CloudWatch Logs group.
You can edit the generated rest-api construct to add, remove, or adjust rules (for example, to add rate-based rules or additional managed rule groups).
To opt out (for example, to attach your own Web ACL), set enableWaf to false:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), enableWaf: false,});To opt out (for example, to attach your own Web ACL), set enable_waf to false:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name enable_waf = false}Access logging
Section titled “Access logging”For REST APIs, the generated infrastructure enables access logging by default, writing one structured JSON line per request to a dedicated CloudWatch Logs group. The log group is encrypted with a customer-managed KMS key and retained for one year.
API Gateway writes access logs using an account-level CloudWatch Logs role. This role is configured on the AWS::ApiGateway::Account setting, which is a singleton per region per account — there is only one role for every REST API in the region. To manage this safely across multiple independently-deployed stacks, the generated infrastructure:
- Creates a shared CloudWatch Logs role and configures it on the account only when no working role is already set, so deployments never overwrite a role another stack owns.
- Leaves the account setting untouched on teardown, so destroying one stack never disables logging for other REST APIs in the region.
The account role is managed by the ApiGatewayAccount construct, a stack-scoped singleton resolved via ApiGatewayAccount.ensure(scope). Each REST API’s stage depends on it, and the role is configured by a Lambda-backed custom resource.
You can customise the access log format by passing deployOptions when constructing your API:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(), deployOptions: { accessLogFormat: AccessLogFormat.clf(), },});The account role is managed by the core/api/api-gateway-account module, which is instantiated by the generated API module. It configures the account idempotently and is never reset on terraform destroy.
You can customise the access log format by editing the access_log_settings block on the aws_api_gateway_stage resource in the generated API module.
Integrations
Section titled “Integrations”The REST/HTTP API CDK constructs are configured to provide a type-safe interface for defining integrations for each of your operations.
Default Integrations
Section titled “Default Integrations”You can use the static defaultIntegrations to make use of the default pattern, which defines an individual AWS Lambda function for each operation:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});Terraform modules automatically use the router pattern with a single Lambda function. No additional configuration is needed:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# The module automatically creates a single Lambda function # that handles all API operations tags = local.common_tags}Accessing Integrations
Section titled “Accessing Integrations”You can access the underlying AWS Lambda functions via the API construct’s integrations property, in a type-safe manner. For example, if your API defines an operation named sayHello and you need to add some permissions to this function, you can do so as follows:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
// sayHello is typed to the operations defined in your APIapi.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, actions: [...], resources: [...],}));If your API uses the shared pattern, the shared router Lambda is exposed as api.integrations.$router:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this).build(),});
api.integrations.$router.handler.addEnvironment('LOG_LEVEL', 'DEBUG');With Terraform’s router pattern, there’s only one Lambda function. You can access it via the module outputs:
# Grant additional permissions to the single Lambda functionresource "aws_iam_role_policy" "additional_permissions" { name = "additional-api-permissions" role = module.my_api.lambda_execution_role_name
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:PutObject" ] Resource = "arn:aws:s3:::my-bucket/*" } ] })}Customising Default Options
Section titled “Customising Default Options”If you would like to customise the options used when creating the Lambda function for each default integration, you can use the withDefaultOptions method. For example, if you would like all of your Lambda functions to reside in a Vpc:
const vpc = new Vpc(this, 'Vpc', ...);
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withDefaultOptions({ vpc, }) .build(),});To customize options like VPC configuration, you need to edit the generated Terraform module. For example, to add VPC support to all Lambda functions:
# Add VPC variablesvariable "vpc_subnet_ids" { description = "List of VPC subnet IDs for Lambda function" type = list(string) default = []}
variable "vpc_security_group_ids" { description = "List of VPC security group IDs for Lambda function" type = list(string) default = []}
# Update the Lambda function resourceresource "aws_lambda_function" "api_lambda" { # ... existing configuration ...
# Add VPC configuration vpc_config { subnet_ids = var.vpc_subnet_ids security_group_ids = var.vpc_security_group_ids }}Then use the module with VPC configuration:
module "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# VPC configuration vpc_subnet_ids = [aws_subnet.private_a.id, aws_subnet.private_b.id] vpc_security_group_ids = [aws_security_group.lambda_sg.id]
tags = local.common_tags}Customising Options Per-Operation
Section titled “Customising Options Per-Operation”To customise the options used to create the default integration for specific operations (without affecting the others), you can use the withOperationOptions method. For example, if you would like to increase the Lambda function timeout for just one operation:
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOperationOptions({ sayHello: { timeout: Duration.seconds(60), }, }) .build(),});
// The selected operations remain default integrations, so they're still typed accordingly:api.integrations.sayHello.handler.addToRolePolicy(new PolicyStatement({ ... }));The options you specify are merged with the default integration options (and any options set via withDefaultOptions). Note that you cannot specify options for operations which you have replaced via withOverrides, since these no longer use the default integration.
You will encounter a type error if the same operation is targeted by both withOperationOptions and withOverrides, regardless of the order in which you call them.
To customise options for specific operations with Terraform, you need to edit the generated Terraform module to configure individual Lambda functions per operation (see the Explicit Integrations section below).
Overriding Integrations
Section titled “Overriding Integrations”You can also override integrations for specific operations using the withOverrides method. Each override must specify an integration property which is typed to the appropriate CDK integration construct for the HTTP or REST API. The withOverrides method is also type-safe. For example, if you would like to override a getDocumentation API to point to documentation hosted by some external website you could achieve this as follows:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), }, }) .build(),});You will also notice that the overridden integration no longer has a handler property when accessing it via api.integrations.getDocumentation.
You can add additional properties to an integration which will also be typed accordingly, allowing for other types of integration to be abstracted but remain type-safe, for example if you have created an S3 integration for a REST API and later wish to reference the bucket for a particular operation, you can do so as follows:
const storageBucket = new Bucket(this, 'Bucket', { ... });
const apiGatewayRole = new Role(this, 'ApiGatewayS3Role', { assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),});
storageBucket.grantRead(apiGatewayRole);
const api = new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getFile: { bucket: storageBucket, integration: new AwsIntegration({ service: 's3', integrationHttpMethod: 'GET', path: `${storageBucket.bucketName}/{fileName}`, options: { credentialsRole: apiGatewayRole, requestParameters: { 'integration.request.path.fileName': 'method.request.querystring.fileName', }, integrationResponses: [{ statusCode: '200' }], }, }), options: { requestParameters: { 'method.request.querystring.fileName': true, }, methodResponses: [{ statusCode: '200', }], } }, }) .build(),});
// Later, perhaps in another file, you can access the bucket property we defined// in a type-safe mannerapi.integrations.getFile.bucket.grantRead(...);Overriding Authorizers
Section titled “Overriding Authorizers”You can also supply options in your integration to override particular method options such as authorizers, for example if you wished to use Cognito authentication for your getDocumentation operation:
new MyApi(this, 'MyApi', { integrations: MyApi.defaultIntegrations(this) .withOverrides({ getDocumentation: { integration: new HttpIntegration('https://example.com/documentation'), options: { authorizer: new CognitoUserPoolsAuthorizer(...) // for REST, or HttpUserPoolAuthorizer for an HTTP API } }, }) .build(),});Explicit Integrations
Section titled “Explicit Integrations”If you prefer, you can choose not to use the default integrations and instead directly supply one for each operation. This is useful if, for example, each operation needs to use a different type of integration or you would like to receive a type error when adding new operations:
new MyApi(this, 'MyApi', { integrations: { sayHello: { integration: new LambdaIntegration(...), }, getDocumentation: { integration: new HttpIntegration(...), }, },});For explicit per-operation integrations with Terraform, you should modify the generated app-specific module to replace the default proxy integration with specific integrations for each operation.
Edit packages/common/terraform/src/app/apis/my-api/my-api.tf:
- Remove the default proxy routes (e.g.,
resource "aws_apigatewayv2_route" "proxy_routes") - Replace the single Lambda function with individual functions for each operation
- Create specific integrations and routes for each operation, reusing the same ZIP bundle:
# Remove the default single Lambda function resource "aws_lambda_function" "api_lambda" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApiHandler" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... rest of configuration }
# Remove the default proxy integration resource "aws_apigatewayv2_integration" "lambda_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.api_lambda.invoke_arn # ... rest of configuration }
# Remove the default proxy routes resource "aws_apigatewayv2_route" "proxy_routes" { for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]) api_id = module.http_api.api_id route_key = "${each.key} /{proxy+}" target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" # ... rest of configuration }
# Add individual Lambda functions for each operation using the same bundle resource "aws_lambda_function" "say_hello_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
# Add specific integrations for each operation resource "aws_apigatewayv2_integration" "say_hello_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.say_hello_handler.invoke_arn payload_format_version = "2.0" timeout_milliseconds = 30000 }
resource "aws_apigatewayv2_integration" "get_documentation_integration" { api_id = module.http_api.api_id integration_type = "HTTP_PROXY" integration_uri = "https://example.com/documentation" integration_method = "GET" }
# Add specific routes for each operation resource "aws_apigatewayv2_route" "say_hello_route" { api_id = module.http_api.api_id route_key = "POST /sayHello" target = "integrations/${aws_apigatewayv2_integration.say_hello_integration.id}" authorization_type = "AWS_IAM" }
resource "aws_apigatewayv2_route" "get_documentation_route" { api_id = module.http_api.api_id route_key = "GET /documentation" target = "integrations/${aws_apigatewayv2_integration.get_documentation_integration.id}" authorization_type = "NONE" }
# Add Lambda permissions for each function resource "aws_lambda_permission" "say_hello_permission" { statement_id = "AllowExecutionFromAPIGateway-SayHello" action = "lambda:InvokeFunction" function_name = aws_lambda_function.say_hello_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.http_api.api_execution_arn}/*/*" }
resource "aws_lambda_permission" "get_documentation_permission" { statement_id = "AllowExecutionFromAPIGateway-GetDocumentation" action = "lambda:InvokeFunction" function_name = aws_lambda_function.get_documentation_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.http_api.api_execution_arn}/*/*" }# Remove the default single Lambda function resource "aws_lambda_function" "api_lambda" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApiHandler" role = aws_iam_role.lambda_execution_role.arn handler = "index.handler" runtime = "nodejs22.x" timeout = 30 # ... rest of configuration }
# Remove the default proxy integration resource "aws_apigatewayv2_integration" "lambda_integration" { api_id = module.http_api.api_id integration_type = "AWS_PROXY" integration_uri = aws_lambda_function.api_lambda.invoke_arn # ... rest of configuration }
# Remove the default proxy routes resource "aws_apigatewayv2_route" "proxy_routes" { for_each = toset(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"]) api_id = module.http_api.api_id route_key = "${each.key} /{proxy+}" target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" # ... rest of configuration }
# Add individual Lambda functions for each operation using the same bundle resource "aws_lambda_function" "say_hello_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-SayHello" role = aws_iam_role.lambda_execution_role.arn handler = "sayHello.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
resource "aws_lambda_function" "get_documentation_handler" { filename = data.archive_file.lambda_zip.output_path function_name = "MyApi-GetDocumentation" role = aws_iam_role.lambda_execution_role.arn handler = "getDocumentation.handler" # Specific handler for this operation runtime = "nodejs22.x" timeout = 30 source_code_hash = data.archive_file.lambda_zip.output_base64sha256
tracing_config { mode = "Active" }
environment { variables = var.env }
tags = var.tags }
# Add specific resources and methods for each operation resource "aws_api_gateway_resource" "say_hello_resource" { rest_api_id = module.rest_api.api_id parent_id = module.rest_api.api_root_resource_id path_part = "sayHello" }
resource "aws_api_gateway_method" "say_hello_method" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.say_hello_resource.id http_method = "POST" authorization = "AWS_IAM" }
resource "aws_api_gateway_integration" "say_hello_integration" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.say_hello_resource.id http_method = aws_api_gateway_method.say_hello_method.http_method
integration_http_method = "POST" type = "AWS_PROXY" uri = aws_lambda_function.say_hello_handler.invoke_arn }
resource "aws_api_gateway_resource" "get_documentation_resource" { rest_api_id = module.rest_api.api_id parent_id = module.rest_api.api_root_resource_id path_part = "documentation" }
resource "aws_api_gateway_method" "get_documentation_method" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.get_documentation_resource.id http_method = "GET" authorization = "NONE" }
resource "aws_api_gateway_integration" "get_documentation_integration" { rest_api_id = module.rest_api.api_id resource_id = aws_api_gateway_resource.get_documentation_resource.id http_method = aws_api_gateway_method.get_documentation_method.http_method
integration_http_method = "GET" type = "HTTP" uri = "https://example.com/documentation" }
# Update deployment to depend on new integrations~ resource "aws_api_gateway_deployment" "api_deployment" { rest_api_id = module.rest_api.api_id
depends_on = [ aws_api_gateway_integration.lambda_integration, aws_api_gateway_integration.say_hello_integration, aws_api_gateway_integration.get_documentation_integration, ]
lifecycle { create_before_destroy = true }
triggers = { redeployment = sha1(jsonencode([ aws_api_gateway_integration.say_hello_integration, aws_api_gateway_integration.get_documentation_integration, ])) } }
# Add Lambda permissions for each function resource "aws_lambda_permission" "say_hello_permission" { statement_id = "AllowExecutionFromAPIGateway-SayHello" action = "lambda:InvokeFunction" function_name = aws_lambda_function.say_hello_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.rest_api.api_execution_arn}/*/*" }
resource "aws_lambda_permission" "get_documentation_permission" { statement_id = "AllowExecutionFromAPIGateway-GetDocumentation" action = "lambda:InvokeFunction" function_name = aws_lambda_function.get_documentation_handler.function_name principal = "apigateway.amazonaws.com" source_arn = "${module.rest_api.api_execution_arn}/*/*" }Integration Pattern
Section titled “Integration Pattern”Generated CDK API constructs support two integration patterns:
isolatedcreates one Lambda function per operation. This is the default for generated APIs.sharedcreates a single default router Lambda and reuses it for every operation unless you override specific integrations.
isolated gives you finer-grained permissions and configuration per operation. shared reduces Lambda and API Gateway integration sprawl while still allowing selective overrides.
For example, setting pattern to 'shared' creates a single function instead of one per integration:
export class MyApi<...> extends ... {
public static defaultIntegrations = (scope: Construct) => { ... return IntegrationBuilder.rest({ pattern: 'shared', ... }); };}Terraform modules automatically use the router pattern - this is the default and only supported approach. The generated module creates a single Lambda function that handles all API operations.
You can simply instantiate the default module to get the router pattern:
# Default router pattern - single Lambda function for all operationsmodule "my_api" { source = "../../common/terraform/src/app/apis/my-api"
asset_bucket_name = module.asset_bucket.bucket_name
# Single Lambda function handles all operations automatically tags = local.common_tags}Code Generation
Section titled “Code Generation”Since operations in FastAPI are defined in Python and CDK infrastructure in TypeScript, we instrument code-generation to supply metadata to the CDK construct to provide a type-safe interface for integrations.
A generate:<ApiName>-metadata target is added to the common constructs project.json to facilitate this code generation, which emits a file such as packages/common/constructs/src/generated/my-api/metadata.gen.ts. Since this is generated at build time, it is ignored in version control.
Granting Access (IAM Only)
Section titled “Granting Access (IAM Only)”If you selected to use IAM authentication, you can use the grantInvokeAccess method to grant access to your API:
api.grantInvokeAccess(myIdentityPool.authenticatedRole);# Create an IAM policy to allow invoking the APIresource "aws_iam_policy" "api_invoke_policy" { name = "MyApiInvokePolicy" description = "Policy to allow invoking the FastAPI"
policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = "execute-api:Invoke" Resource = "${module.my_api.api_execution_arn}/*/*" } ] })}
# Attach the policy to an IAM role (e.g., for authenticated users)resource "aws_iam_role_policy_attachment" "api_invoke_access" { role = aws_iam_role.authenticated_user_role.name policy_arn = aws_iam_policy.api_invoke_policy.arn}
# Or attach to an existing role by nameresource "aws_iam_role_policy_attachment" "api_invoke_access_existing" { role = "MyExistingRole" policy_arn = aws_iam_policy.api_invoke_policy.arn}The key outputs from the API module that you can use for IAM policies are:
module.my_api.api_execution_arn- For granting execute-api:Invoke permissionsmodule.my_api.api_arn- The API Gateway ARNmodule.my_api.lambda_function_arn- The Lambda function ARN
Local Development
Section titled “Local Development”The generator configures a local development server that you can run with:
pnpm nx serve my-apiyarn nx serve my-apinpx nx serve my-apibunx nx serve my-apiThis starts a local FastAPI development server with:
- Auto-reload on code changes
- Interactive API documentation at
/docsor/redoc - OpenAPI schema at
/openapi.json
Invoking your FastAPI
Section titled “Invoking your FastAPI”To invoke your API from a React website, you can use the connection generator.
Connections
Section titled “Connections”Use the connection generator to integrate this project with others in your workspace. The following connections involve this project: