How to Build a Serverless API with FastAPI and AWS Lambda

How to Build a Serverless API with FastAPI and AWS Lambda
Photo by Artturi Jalli / Unsplash

In modern cloud architecture, the directive is clear: maximize scalability and developer velocity while minimizing operational overhead. Traditional monolithic, server-based APIs impose a tax in management, scaling, and cost. Serverless architectures, specifically AWS Lambda, offer a compelling alternative by abstracting server management and providing true pay-per-invocation scaling.

However, the raw function-as-a-service (FaaS) model can lack the developer experience, structure, and performance optimization of modern web frameworks. This is where FastAPI enters. FastAPI, built on Starlette and Pydantic, provides a high-performance (on par with NodeJS and Go) asynchronous framework with automatic data validation and API documentation.

Combining FastAPI's exceptional developer experience with AWS Lambda's operational efficiency creates a powerful stack for building robust, scalable, and cost-effective APIs. This article provides a complete, production-ready blueprint for deploying a FastAPI application as an AWS Lambda function, fronted by an API Gateway, using the AWS Serverless Application Model (SAM).

Core Components of the Architecture

  1. FastAPI: The Python web framework. We leverage it for routing, dependency injection, Pydantic-based data validation, and automatic OpenAPI (Swagger/ReDoc) generation.
  2. AWS Lambda: The compute service. Our entire FastAPI application will be packaged and deployed as a single Lambda function.
  3. Amazon API Gateway (HttpApi): The entry point. We will use the v2 HTTP API (which is faster and cheaper than the v1 REST API) to receive HTTP requests and proxy them to our Lambda function.
  4. Mangum: The critical adapter. AWS Lambda and API Gateway communicate via a specific JSON event structure. FastAPI, as an ASGI framework, expects an ASGI scope. Mangum is a lightweight adapter that translates the Lambda event into an ASGI-compliant scope that FastAPI understands.
  5. AWS SAM (Serverless Application Model): The Infrastructure as Code (IaC) framework. We use SAM to define our Lambda function, API Gateway, and their permissions in a simple template.yaml file, enabling reproducible and automated deployments.

Section 1: Project Setup and the FastAPI Application

First, establish the project structure. We will isolate the application code in an app/ directory.

serverless-fastapi/
├── app/
│   ├── __init__.py
│   ├── main.py
│   └── requirements.txt
└── template.yaml

1.1. Define Dependencies (app/requirements.txt)

Our application requires three core libraries:

fastapi
uvicorn       # Required by FastAPI, though we won't run it as a server
mangum        # The Lambda-to-ASGI adapter
pydantic      # For data modeling (often a peer dependency of fastapi)

1.2. Create the FastAPI Application (app/main.py)

We will create a simple API with three endpoints to demonstrate core functionality:

  • GET /: A root endpoint.
  • POST /users/: An endpoint demonstrating Pydantic-based request body validation.
  • GET /users/{user_id}: An endpoint demonstrating path parameters and a Pydantic response model.
# app/main.py
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
from typing import Optional

# 1. Define Pydantic Models for data validation
class UserCreate(BaseModel):
    email: EmailStr
    username: str
    is_active: bool = True

class UserResponse(BaseModel):
    id: int
    email: EmailStr
    username: str

# In-memory "database" for demonstration
db_users = {
    1: {"id": 1, "email": "cto@example.com", "username": "chief-tech"},
    2: {"id": 2, "email": "dev@example.com", "username": "lead-dev"},
}

# 2. Create the FastAPI app instance
app = FastAPI(
    title="Serverless FastAPI Demo",
    description="A demonstration of FastAPI on AWS Lambda.",
    version="1.0.0",
)

@app.get("/")
def read_root():
    """
    A simple health-check or root endpoint.
    """
    return {"message": "FastAPI is running on AWS Lambda!"}


@app.get("/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
    """
    Get a user by their ID.
    """
    user = db_users.get(user_id)
    if not user:
        return {"error": "User not found"}, 404
    return user


@app.post("/users/", response_model=UserResponse, status_code=201)
def create_user(user: UserCreate):
    """
    Create a new user.
    The `user: UserCreate` parameter ensures the request body
    is validated against the UserCreate Pydantic model.
    """
    new_user_id = max(db_users.keys()) + 1
    new_user = user.model_dump()
    new_user["id"] = new_user_id
    db_users[new_user_id] = new_user
    
    # Note: Pydantic v2 uses model_dump(), v1 used .dict()
    return new_user

This is a standard, clean FastAPI application. Critically, it has no knowledge of its serverless environment.

Section 2: Integrating Mangum - The ASGI Adapter

To allow Lambda to invoke our FastAPI app, we introduce Mangum. We modify app/main.py to add a Lambda handler.

Add the following lines to the bottom of app/main.py:

# ... (all the FastAPI code from above) ...

# 3. Import Mangum and create the handler
from mangum import Mangum

# This 'handler' is the entry point for AWS Lambda.
# Mangum(app) creates an adapter that AWS Lambda can invoke.
handler = Mangum(app, lifespan="off")

# Note: lifespan="off" is recommended for simple Lambda functions
# to avoid potential issues with startup/shutdown events.
# For more complex needs (e.g., database connection pools),
# you may need to manage the 'on' lifespan carefully.

The handler object is now the callable that AWS Lambda will execute. Mangum inspects the incoming event dictionary from API Gateway, translates it into an ASGI scope, passes it to app, awaits the response, and then formats FastAPI's response back into the dictionary structure API Gateway expects.

Section 3: Defining the Infrastructure with AWS SAM

We will now define our serverless infrastructure using the AWS SAM template.yaml. This file declares our Lambda function and the HTTP API that triggers it.

Place this in the root of your project (template.yaml):

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  A serverless application deploying a FastAPI backend on AWS Lambda.

Globals:
  Function:
    Timeout: 30
    MemorySize: 256
    Runtime: python3.11 # Specify a modern Python runtime

Resources:
  # 1. The Lambda Function resource
  FastApiLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: app/  # Points to the directory containing our app
      Handler: main.handler  # The file is 'main.py', the callable is 'handler'
      Events:
        # 2. The API Gateway (HttpApi) event source
        ApiEvent:
          Type: HttpApi
          Properties:
            # 3. The "catch-all" proxy integration. This is critical.
            # It forwards ALL paths and methods to our Lambda.
            Path: /{proxy+}
            Method: ANY
            
            # This payload format is required by Mangum
            PayloadFormatVersion: '2.0' 

Outputs:
  # 4. Output the URL of the deployed API
  ApiEndpoint:
    Description: "API Gateway endpoint URL for Prod stage"
    Value: !Sub "https"://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/

Architectural Decisions in this Template:

  • Handler: main.handler: This tells Lambda to look in main.py for the handler object (which is our Mangum(app) instance).
  • Type: HttpApi: We explicitly choose the v2 HTTP API for its lower latency and cost.
  • Path: /{proxy+} and Method: ANY: This is the most crucial part. This single integration creates a "catch-all" route. Any request, regardless of path (e.g., /, /users/1, /docs) or method (GET, POST, PUT), is proxied directly to the FastAPI application. This allows FastAPI's internal router to handle all routing logic, just as it would on a traditional server.
  • PayloadFormatVersion: '2.0': Mangum works best with the v2 payload format, which is cleaner and more structured.

Section 4: Build and Deploy Procedure

With the app/ code and template.yaml in place, we use the AWS SAM CLI to build and deploy.

Step 1: Install Dependencies and Build

SAM needs to package your application and its dependencies.

# First, install dependencies locally for SAM to package
# (SAM will copy these from app/ into its build directory)
pip install -r app/requirements.txt -t app/

# Next, build the SAM application
# --use-container is highly recommended. It builds your dependencies
# inside a Docker container that mimics the Lambda environment,
# preventing "glibc" or "openssl" errors from compiled libraries.
sam build --use-container

This command creates a .aws-sam/build directory containing the final deployment package.

Step 2: Deploy the Application

We now deploy the built artifact to AWS.

# The --guided flag will walk you through a one-time setup
# for your stack name, AWS region, and other parameters.
sam deploy --guided

Follow the prompts:

  1. Stack Name: serverless-fastapi-api (or your preferred name).
  2. AWS Region: us-east-1 (or your target region).
  3. Confirm changes before deploy: y. This allows you to review the AWS CloudFormation changeset.
  4. Allow SAM CLI IAM role creation: y.
  5. Save arguments to samconfig.toml: y. This saves your choices, so future deployments only require sam deploy.

SAM will now provision the CloudFormation stack, which creates the API Gateway, the Lambda function, and the necessary IAM roles. Upon completion, it will display the ApiEndpoint from your template.yaml outputs.

---------------------------------------------------------------------------------
Outputs
---------------------------------------------------------------------------------
Key                 ApiEndpoint
Description         API Gateway endpoint URL for Prod stage
Value               https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/
---------------------------------------------------------------------------------

Section 5: Verification and Testing

You can now interact with your fully serverless API. Let $API_URL be the ApiEndpoint value from your output.

Test 1: Root Endpoint

curl $API_URL/

Expected Response:

{"message":"FastAPI is running on AWS Lambda!"}

Test 2: POST with Pydantic Validation

curl -X POST "$API_URL/users/" \
     -H "Content-Type: application/json" \
     -d '{"email": "engineer@example.com", "username": "senior-engineer"}'

Expected Response:

{"id":3,"email":"engineer@example.com","username":"senior-engineer"}

Test 3: GET with Path Parameter

curl $API_URL/users/1

Expected Response:

{"id":1,"email":"cto@example.com","username":"chief-tech"}

Test 4: Automatic API Documentation

The most powerful feature: navigate to $API_URL/docs in your browser. You will see the complete, interactive Swagger UI, automatically generated by FastAPI, running entirely on serverless infrastructure.

Section 6: Advanced Considerations for Production

1. Cold Starts

A "cold start" is the latency incurred on the first request to an idle Lambda function (downloading code, initializing the runtime and your application).

  • Impact: FastAPI and Mangum are lightweight, so cold starts are typically fast (200-800ms). However, this can be significant for latency-critical applications.
  • Mitigation: For p99 latency guarantees, enable Provisioned Concurrency on your Lambda function. This keeps a specified number of instances "warm" and ready, eliminating cold starts entirely, for an additional cost.

2. Dependency Management and Deployment Size

  • Lambda Layers: If your application grows and shares large dependencies (e.g., pandas, numpy, scipy) with other functions, move them into a Lambda Layer. This reduces the size of your function's deployment package, speeding up deployments and cold starts.
  • Container Images: For applications exceeding the 250MB (unzipped) .zip package limit, deploy your function as a Container Image. SAM supports this (PackageType: Image in template.yaml). This allows for packages up to 10GB.

3. Observability

  • Structured Logging: Do not use print(). Use a structured logging library. The AWS Lambda Powertools for Python is the industry standard. It provides structured logging, tracing (X-Ray), and custom metrics with minimal overhead.
  • Tracing: Enable AWS X-Ray active tracing on both API Gateway and your Lambda function (a simple Tracing: Active flag in your template.yaml). This provides a complete trace of requests, from the gateway through the Lambda invocation and any downstream AWS SDK calls.

Conclusion

This architecture provides a "best-of-both-worlds" solution. We retain the entire feature set and world-class developer experience of FastAPI—including dependency injection, Pydantic validation, and automatic documentation—while simultaneously gaining the immense scalability, resilience, and pay-per-use cost model of AWS Lambda.

By leveraging Mangum for adaptation and AWS SAM for reproducible infrastructure, engineering teams can deploy robust, high-performance Python APIs with near-zero operational overhead, allowing them to focus purely on delivering business value.