Contents

Building Backend APIs with FastAPI on AWS Lambda

One of the trade-offs of working in the cloud is the risk of vendor lock-in. While cloud platforms offer powerful tools and scalability, they can also tie you into their ecosystem, making it harder to pivot or migrate down the road.

When it comes to building backend APIs in Python, you have a few great frameworks at your disposal—Django, FastAPI, and Flask—each with its own level of complexity and opinionation. Django is feature-rich and great for full-stack apps, Flask is minimal and flexible, but FastAPI stands out as a modern, async-first framework that makes it incredibly easy to build fast, clean, and production-ready APIs.

FastAPI is built on top of Starlette and Pydantic, which means it’s both high-performance and deeply integrated with Python’s type system. With automatic data validation, interactive documentation (via Swagger and ReDoc), and a strong developer experience, it’s quickly becoming a go-to choice for building APIs in Python.

If you’d like to follow along with the code or explore the full project, you can find the source on GitHub here.

In this post, we’ll walk through how to deploy a FastAPI backend on AWS Lambda, using Lambda Layers and the AWS Serverless Application Model (SAM). This approach lets you combine the speed and elegance of FastAPI with the scalability and cost-efficiency of serverless architecture.

To streamline development and packaging, we’ll use uv, a blazing-fast Python package manager and virtual environment tool. Make sure you have it installed before diving in.

By the end of this guide, you’ll have a working FastAPI app deployed to AWS Lambda with a clean, modular directory structure that looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
├── api-lambda
│   ├── pyproject.toml
│   ├── src
│   │   └── api_lambda
│   │       ├── __init__.py
│   │       ├── main.py
│   │       └── v1
│   │           ├── __init__.py
│   │           └── items.py
│   ├── tests
│   │   └── test_api.py
│   └── uv.lock
├── infra
│   ├── infra.sam.yaml
└── layers
    └── mylib
        ├── Makefile
        ├── pyproject.toml
        ├── src
        │   └── mylib
        │       ├── __init__.py
        │       ├── db.py
        │       └── py.typed
        └── uv.lock

Architecture Overview

Our backend is built using:

  • API Gateway – routes HTTP requests
  • Lambda – runs FastAPI with Mangum
  • Lambda Layers – share dependencies
  • DynamoDB – stores data

End-to-End Flow

  1. A client sends an HTTP request.
  2. API Gateway forwards it to the Lambda function.
  3. FastAPI handles the request, optionally interacting with DynamoDB.
  4. The response is sent back to the client.

Defining the Architecture with SAM

To bring our architecture to life, we define all our cloud infrastructure using AWS SAM (Serverless Application Model). This allows us to describe the entire system—API Gateway, Lambda function, Lambda Layer, and DynamoDB table—in a single declarative file: infra/infra.sam.yaml.

Let’s walk through what this file sets up.

API Gateway – Exposing the FastAPI Backend

The first component is API Gateway, which acts as the frontend to our backend:

1
2
3
4
5
6
7
MyApi:
  Type: AWS::Serverless::Api
  Properties:
    Name: MySimpleApi
    StageName: prod
    Auth:
      DefaultAuthorizer: NONE

This sets up a REST API named MySimpleApi, with a deployment stage called prod. It forwards all requests (via the /{proxy+} path) to our Lambda function. We’re not enforcing any authentication here (DefaultAuthorizer: NONE) to keep things simple, but this can be expanded later with Cognito or custom authorizers.

Lambda Function – Running FastAPI

At the core is our Lambda function, where FastAPI is running:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
MyLambdaFunction:
  Type: AWS::Serverless::Function
  Properties:
    CodeUri: ../api-lambda/src/
    Handler: api_lambda.main.handler
    Layers:
      - !Ref MyLib
    Policies:
      - DynamoDBCrudPolicy:
          TableName: MyTable
    Environment:
      Variables:
        TABLE_NAME: MyTable
    Events:
      ApiEvent:
        Type: Api
        Properties:
          Path: "/{proxy+}"  # does not match root path
          Method: ANY
          RestApiId: !Ref MyApi
          Auth:
            Authorizer: NONE

This Lambda function is triggered by API Gateway for any HTTP method on any subpath using the /{proxy+} pattern. This is a catch-all route that matches any path except the root /. For example, /items, /v1/status, or /health would be captured by this route, but / would not. If you want the root path to be handled as well, you’ll need to define a separate event with Path: “/”.

The function uses a Lambda Layer (MyLib) to include shared dependencies and logic, keeping the function code clean and maintainable. Environment variables, such as the table name (TABLE_NAME), are injected to configure the function at runtime.

It also has permission to interact with DynamoDB using the predefined DynamoDBCrudPolicy, scoped to the MyTable resource.

The FastAPI application is made Lambda-compatible using mangum, an adapter that translates API Gateway events into ASGI requests, allowing FastAPI to run seamlessly within the Lambda execution environment.

Lambda Layer – Packaging Dependencies

To avoid bundling third-party dependencies directly in our function code, we use a Lambda Layer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
MyLib:
  Type: AWS::Serverless::LayerVersion
  Properties:
    LayerName: mylib
    ContentUri: ../layers/mylib/
    CompatibleRuntimes:
      - python3.13
    RetentionPolicy: Retain
  Metadata:
    BuildMethod: makefile

This keeps our deployment size lean and improves build speed. The layer is built using a Makefile, making it easy to control and reproduce builds. It’s compatible with Python 3.13 and can be reused across multiple Lambda functions if needed.

DynamoDB – Simple, Scalable Storage

For persistence, we use a DynamoDB table:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
MyDynamoDBTable:
  Type: AWS::DynamoDB::Table
  Properties:
    TableName: MyTable
    AttributeDefinitions:
      - AttributeName: id
        AttributeType: S
    KeySchema:
      - AttributeName: id
        KeyType: HASH
    BillingMode: PAY_PER_REQUEST

This table stores items with an id as the primary key. With PAY_PER_REQUEST billing, there’s no need to provision capacity upfront—it scales automatically and you only pay for what you use.

Outputs – API URL at Your Fingertips

Finally, SAM gives us a convenient output:

1
2
3
Outputs:
  ApiUrl:
    Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/prod/"

Once deployed, this output provides the full URL where your FastAPI backend is publicly accessible.


Creating a Lambda Layer with a Reusable Library

To keep our architecture clean and modular, we’ll use a Lambda Layer to package shared dependencies and reusable code. This layer will contain:

  • Third-party libraries like fastapi, mangum, and boto3
  • A custom Python library called mylib that encapsulates shared logic for accessing DynamoDB

Using a Lambda Layer helps us keep our actual function code lightweight and separates infrastructure concerns from application logic. It also makes shared utilities reusable across multiple Lambda functions.

Layer Directory Structure

Our layer will follow this structure:

1
2
3
4
5
6
7
8
layers/
└── mylib/
    ├── Makefile                # Build script for SAM
    ├── pyproject.toml          # Dependencies
    ├── uv.lock                 # uv-generated lock file
    └── src/
        └── mylib/
            └── db.py           # Reusable DynamoDB helpers

This structure keeps the source code isolated in src/mylib while letting SAM use the Makefile to package everything for deployment.

Step 1: Initialize the Layer Project with uv

We’ll use uv, a fast and modern Python package manager, to create and manage our layer.

Run the following commands from your project root:

1
2
3
mkdir layers
uv init --library layers/mylib
cd layers/mylib

This sets up a new Python library called mylib, including a pyproject.toml and a clean src/mylib directory for our code.

Step 2: Install Shared Dependencies

Install the packages we want to share across Lambda functions:

1
uv add boto3 fastapi mangum pydantic-settings

These will be bundled into the layer so our application Lambda doesn’t need to install them separately.

Step 3: Add DynamoDB Logic to mylib

In layers/mylib/src/mylib/db.py, define reusable functions to interact with DynamoDB:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import boto3
import uuid
from botocore.exceptions import ClientError
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    aws_region: str
    table_name: str

    class Config:
        validate_by_name = True

def get_dynamodb_table(settings: Settings = Settings()):
    dynamodb = boto3.resource("dynamodb", region_name=settings.aws_region)
    return dynamodb.Table(settings.table_name)

def create_item(item: dict, table) -> dict:
    item["id"] = str(uuid.uuid4())
    table.put_item(Item=item)
    return item

def get_item(item_id: str, table) -> dict | None:
    try:
        response = table.get_item(Key={"id": item_id})
        return response.get("Item")
    except ClientError:
        return None

def list_items(table) -> list[dict]:
    response = table.scan()
    return response.get("Items", [])

def delete_item(item_id: str, table) -> bool:
    try:
        table.delete_item(Key={"id": item_id})
        return True
    except ClientError:
        return False

These helper functions allow for clean separation of database logic from your main API code and can be reused across different Lambda functions.

Step 4: Add a Makefile to Build the Layer

AWS SAM supports building Lambda Layers using a Makefile. Add a file named Makefile inside layers/mylib with the following content:

1
2
3
4
build-MyLib:
	mkdir -p "$(ARTIFACTS_DIR)/python"
	cp -r src/mylib "$(ARTIFACTS_DIR)/python"
	uv pip install -r pyproject.toml --target "$(ARTIFACTS_DIR)/python"

This tells SAM how to:

  1. Create the target python/ folder where Lambda expects dependencies
  2. Copy your mylib source code into it
  3. Install all dependencies using uv into that same directory

When you run sam build, SAM will automatically invoke this target to package your layer correctly.


Building the API Lambda

Now that our Lambda Layer is ready with shared dependencies and DynamoDB helpers, it’s time to create the actual Lambda function that runs our FastAPI app. This function will handle HTTP requests routed from API Gateway and interact with DynamoDB using our mylib module.

We’ll start by setting up a Python package for our Lambda function using uv, and then build out the directory structure and code.

Step 1: Initialize the Lambda Project

First, create and initialize the Lambda project using uv. This sets up a modern Python environment with a clean structure and dependency management:

1
2
uv init --package api-lambda
cd api-lambda

Next, add the following dependencies:

1
2
3
4
5
# Add a reference to our shared Lambda layer library
uv add --editable ../layers/mylib

# Add development and runtime dependencies
uv add --dev pytest pytest-xdist requests "fastapi[standard]"

This installs:

  • fastapi[standard]: FastAPI and its optional extras (e.g., pydantic, uvicorn)
  • pytest, pytest-xdist: for running tests efficiently
  • requests: for making HTTP requests in tests
  • mylib: as an editable local dependency pointing to the code in our Lambda layer

Step 2: Create the Directory Structure

Let’s manually create the source structure for our Lambda API:

1
2
3
4
mkdir -p src/api_lambda/v1
touch src/api_lambda/main.py
touch src/api_lambda/v1/__init__.py
touch src/api_lambda/v1/items.py

This structure mirrors the best practices for FastAPI projects—versioning your routes under v1/ while keeping the entry point (main.py) clean and focused.

Here’s what your directory will look like:

1
2
3
4
5
6
7
api-lambda/
├── src/
│   └── api_lambda/
│       ├── main.py         # Entry point for FastAPI + Lambda (via Mangum)
│       └── v1/
│           ├── items.py    # /items API routes
│           └── __init__.py

Step 3: Define the /items API Routes

Let’s implement the FastAPI routes to handle basic item operations. Open src/api_lambda/v1/items.py and add the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import logging
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
import mylib.db as db

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

router = APIRouter(prefix="/items", tags=["items"])

class Item(BaseModel):
    name: str
    price: int
    description: str = ""

class ItemOut(Item):
    id: str

@router.get("/", response_model=list[ItemOut])
def list_items(table=Depends(db.get_dynamodb_table)):
    logger.info("Listing items")
    return db.list_items(table)

@router.get("/{item_id}", response_model=ItemOut)
def get_item(item_id: str, table=Depends(db.get_dynamodb_table)):
    logger.info(f"Retrieving item {item_id=}")
    if (item := db.get_item(item_id, table)) is None:
        raise HTTPException(status_code=404, detail="Item not found")
    return item

@router.post("/", status_code=201, response_model=ItemOut)
def create_item(item: Item, table=Depends(db.get_dynamodb_table)):
    logger.info(f"Creating item {item=}")
    return db.create_item(item.model_dump(), table)

@router.delete("/{item_id}", status_code=204)
def delete_item(item_id: str, table=Depends(db.get_dynamodb_table)):
    logger.info(f"Deleting item {item_id=}")
    if not db.delete_item(item_id, table):
        raise HTTPException(status_code=404, detail="Item not found")

Step 4: Add the FastAPI App Entry Point

Now, connect your router and configure FastAPI to run inside AWS Lambda using Mangum. Open src/api_lambda/main.py and add:

1
2
3
4
5
6
7
8
from fastapi import FastAPI
from mangum import Mangum
from .v1 import items

app = FastAPI()
app.include_router(items.router)

handler = Mangum(app)

This wraps the FastAPI app using Mangum, making it compatible with AWS Lambda and API Gateway events.


Deploying the Infrastructure

With our Lambda Layer and FastAPI code in place, it’s time to deploy the infrastructure to AWS using AWS SAM (Serverless Application Model).

SAM handles packaging your code, building the Lambda Layer with the Makefile, provisioning API Gateway, Lambda, and DynamoDB—all from the definitions in our infra.sam.yaml template.

Step 1: Build and Deploy with SAM

Navigate to the infra directory where the SAM template is located:

1
2
cd infra
pwd  # should output something like /your/project/path/infra

Run the following command to build and deploy the stack:

1
sam build --guided -t infra.sam.yaml && sam deploy

Once confirmed, SAM will:

  • Build the application artifacts
  • Install dependencies using the Lambda Layer’s Makefile
  • Package everything into a CloudFormation stack
  • Deploy your infrastructure to AWS

Step 2: Get Your API Endpoint

After deployment, SAM will print out your API Gateway endpoint. It should look something like this:

1
https://<your-api-id>.execute-api.<region>.amazonaws.com/prod/

This is your live FastAPI-powered API, running serverlessly on AWS Lambda behind API Gateway. You can now start interacting with it using any HTTP client.


API Testing

Now that your FastAPI backend is live, it’s time to verify that the endpoints are working as expected. We’ll write a few simple tests to check the core functionality of the /items API.

These tests will cover:

  • Creating a new item
  • Retrieving an item by ID
  • Deleting an item
  • Listing all items

We’re using the requests library (already added to your development dependencies) to make HTTP calls to the deployed API.

Test File Structure

Inside your api-lambda project, create the following test file:

1
2
3
api-lambda/
└── tests/
    └── test_api.py

Add the API Tests

Paste the following code into api-lambda/tests/test_api.py. Replace the placeholder URL with your actual deployed API Gateway endpoint (it should end with /items/):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import requests

# Replace with your actual deployed API URL
API_URL = "https://your-api-id.execute-api.region.amazonaws.com/prod/items/"

def test__should_create_item_successfully():
    data = {"item": {"name": "test item", "price": 10, "description": "test description"}}
    response = requests.post(API_URL, json=data)

    assert response.status_code == 201
    response_data = response.json()
    assert data["item"].items() <= response_data.items()

def test__should_retrieve_item_successfully():
    data = {"item": {"name": "test item", "price": 10, "description": "test description"}}
    response = requests.post(API_URL, json=data)
    assert response.status_code == 201

    item = response.json()
    response = requests.get(f"{API_URL}{item['id']}")
    assert response.status_code == 200
    assert item == response.json()

def test__should_delete_item_successfully():
    data = {"item": {"name": "test item", "price": 10, "description": "test description"}}
    response = requests.post(API_URL, json=data)
    assert response.status_code == 201

    item = response.json()
    response = requests.delete(f"{API_URL}{item['id']}")
    assert response.status_code == 204

    response = requests.get(f"{API_URL}{item['id']}")
    assert response.status_code == 404

def test__should_list_empty_items_successfully():
    response = requests.get(API_URL)
    assert response.status_code == 200
    assert isinstance(response.json(), list)

Running the Tests

To run your tests, make sure you’re in the api-lambda directory and then execute:

1
uv run pytest -n auto

This will run your tests in parallel using pytest-xdist, and give you a fast and reliable check of your deployed API’s functionality.


Conclusion

In this post, we walked through building a modern, serverless FastAPI backend deployed on AWS Lambda using AWS SAM. Along the way, we:

  • Created a reusable Lambda Layer with FastAPI and custom database utilities
  • Built and deployed a FastAPI application that integrates with API Gateway and DynamoDB
  • Used uv to manage Python environments and streamline development
  • Defined our infrastructure as code with a clean SAM template
  • Wrote end-to-end API tests to validate that everything works as expected

By leveraging serverless technologies and modular code organization, we ended up with a lightweight, scalable, and cost-effective backend architecture—without managing a single server.

This setup provides a strong foundation for building production-ready APIs on AWS with Python.

Happy engineering!