Deploy Your AI Email Summarizer to Lambda

Deploy a FastAPI email summarizer to AWS Lambda with API key auth.

Introduction

30 Second Summary

You built an AI email summarizer that runs on your laptop. That is great for testing, but nobody else can use it unless your computer is on and your terminal is running.

In this project, you will deploy your FastAPI email summarizer to AWS Lambda so it runs serverlessly in the cloud. You will add API key authentication, manage secrets through environment configuration, and ship everything with two terminal commands.

What You'll Build

A production-ready AI email summarizer API deployed to AWS Lambda, secured with an API key and accessible via a public HTTPS endpoint.

By the end of this project, you'll have:

  • A FastAPI application with API key authentication via the X-API-Key header.
  • A pydantic-settings configuration module that loads secrets from a .env file.
  • A SAM template defining your Lambda function and API Gateway as Infrastructure as Code.
  • A deployed Lambda function accessible via a public HTTPS URL that summarizes emails using Claude.
  • Secret Mission: Add a second endpoint that classifies email priority using Claude Haiku 4.5.

Are there any prerequisites?

This project picks up where Project 1 (Build an AI Email Summarizer API) left off. You need a working FastAPI + Claude email summarizer, an Anthropic API key, and an AWS account.

Install AWS CLI and SAM CLI

Before you can deploy your email summarizer to AWS, you need two command-line tools installed on your machine.

The AWS CLI lets you interact with AWS services from your terminal. The AWS SAM CLI builds on top of it, giving you commands to package and deploy serverless applications. In this step, you will install both tools and connect them to your AWS account.

In this step, get ready to:
  • Install the AWS CLI and verify it with aws --version.
  • Install the AWS SAM CLI and verify it with sam --version.
  • Configure your AWS credentials so both tools can access your account.
Install the AWS CLI

The AWS CLI is the foundation for all AWS command-line tooling. The SAM CLI depends on it for credential management, so you need to install the AWS CLI first.

  • Download the AWS CLI Windows 64-bit MSI installer by visiting this link:
  • https://awscli.amazonaws.com/AWSCLIV2.msi
  • Run the downloaded AWSCLIV2.msi file and follow the on-screen prompts to complete installation.
  • After installation completes, open a new PowerShell window. Press the Windows key, type PowerShell, and press Enter.
  • Verify the installation by running this command:
aws --version

What should I see?

You should see output showing the AWS CLI version, something like aws-cli/2.x.x Python/3.x.x Windows/10 exe/AMD64. The exact version numbers will vary depending on when you download the installer.

✔️ I see a version number

The AWS CLI is installed and ready. Move on to the next substep.

ⓧ Command not recognized

Windows cannot find the aws command yet.

  • Close the PowerShell window and open a brand new one. The installer updates your PATH, but existing windows do not pick it up automatically.
  • If it still does not work after reopening, restart your computer and try again.
Install the AWS SAM CLI

The SAM CLI gives you commands like sam build and sam deploy to package and deploy serverless applications. It also uses Docker to build your code inside a container matching the Lambda runtime.

  • Download the AWS SAM CLI 64-bit MSI installer from the official docs page:
  • Install the AWS SAM CLI
  • On the page, scroll to the Windows section and click the 64-bit download link.
  • Run the downloaded MSI file and follow the on-screen prompts.
  • After installation, verify by running this command in your PowerShell window:
sam --version

What should I see?

You should see output like SAM CLI, 1.x.x. The exact version number depends on when you download the installer.

✔️ I see a version number

The SAM CLI is installed and ready. Move on to configuring your credentials.

ⓧ Command not recognized

  • Close your PowerShell window and open a new one so the updated PATH takes effect.
  • If you still see an error, restart your computer and try again.

Why enable long paths?

The SAM CLI installer may prompt you to enable long paths on Windows 10 and newer. This prevents errors caused by file paths exceeding the Windows 260-character limit. If you see this prompt during installation, accept it.

If you were not prompted, you can enable it later through the Windows Group Policy Editor if you encounter FileNotFoundError issues.

Configure AWS credentials

Both the AWS CLI and SAM CLI need credentials to access your AWS account. You will use the Access Key ID and Secret Access Key from your AWS IAM user.

  • Run the configuration command in PowerShell:
aws configure

What does this command do?

The aws configure command walks you through an interactive prompt to set your credentials. It stores them locally so every AWS CLI and SAM CLI command can authenticate on your behalf.

  • When prompted, enter the following values one at a time:
  • AWS Access Key ID: Paste your IAM user's Access Key ID.
  • AWS Secret Access Key: Paste your IAM user's Secret Access Key.
  • Default region name: Enter us-east-1.
  • Default output format: Enter json.

Where do I find my Access Key?

If you do not have an Access Key yet, sign in to the AWS Console, go to IAM > Users, select your user, open the Security credentials tab, and click Create access key. Copy both values immediately because the Secret Access Key is only shown once.

  • Verify your credentials are configured correctly by running:
aws sts get-caller-identity

What should I see?

You should see a JSON response containing your UserId, Account number, and Arn. This confirms your credentials are valid and connected to your AWS account.

Seeing an error?

  • If you see Unable to locate credentials, re-run aws configure and check that you pasted your Access Key ID and Secret Access Key correctly with no extra spaces.
  • If you see InvalidClientTokenId, your Access Key ID may be incorrect or the key was deleted. Generate a new key pair from the IAM console.

Help me troubleshoot my AWS credentials configuration.

Your AWS tools are installed and authenticated. Next up, you will restructure your FastAPI app with proper environment configuration so it is ready for deployment.

Restructure Your App with Environment Configuration

Your AWS tools are installed and your credentials are configured. Now it's time to prepare your FastAPI email summarizer for deployment.

A local Python script works fine on your machine, but deploying to AWS Lambda requires a specific project structure. You also need a secure way to manage secrets like API keys without hardcoding them.

In this step, get ready to:
  • Create a structured project directory with an app/ package folder.
  • Build an environment configuration module using pydantic-settings to load secrets from a .env file.
  • Write the email summarizer API and verify it runs locally.
Create the project directory structure

Your project needs a Python package (the app/ folder) to organize your source code, plus root-level files for configuration and dependencies.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir email-summarizer-api
cd email-summarizer-api
  • Create the app/ directory by running:
mkdir app
  • Create all the files your project needs by running:
New-Item -ItemType File -Path "app/__init__.py", "app/main.py", "app/config.py", "app/auth.py", "requirements.txt", ".env", ".gitignore"
  • Confirm the files exist by running:
ls app
ls

You should see __init__.py, main.py, config.py, and auth.py inside app/. In the root folder you should see requirements.txt, .env, and .gitignore.

Not seeing all the files?

If .env or .gitignore don't appear in ls output, they may be hidden. Run ls -Force to show hidden files in PowerShell.

If you see an error about the path, make sure you're inside the email-summarizer-api folder before running the New-Item command.

Help me troubleshoot my missing files

Now fill in the dependency and ignore files. These stay relatively static throughout the project.

  • Open the email-summarizer-api folder in your code editor. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code (or Cursor) and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Select the email-summarizer-api folder on your Desktop, then click Select Folder.
  • Open requirements.txt from the Explorer sidebar and paste the following content:
fastapi==0.136.3
mangum==0.21.0
anthropic==0.109.1
pydantic-settings==2.14.1
uvicorn[standard]>=0.30.0

What are these packages?

  • fastapi is your web framework for building APIs.
  • mangum is an adapter that lets FastAPI run inside AWS Lambda (you'll use this in a later step).
  • anthropic is the official Python SDK for calling Claude.
  • pydantic-settings loads environment variables into typed Python objects.
  • uvicorn is the ASGI server that runs your FastAPI app locally.
  • Open .gitignore and paste the following content:
.env
.aws-sam/
__pycache__/
.venv/

Why add .env to .gitignore?

Your .env file will contain your Anthropic API key and your app's secret key. Adding it to .gitignore prevents you from accidentally pushing these secrets to a public repository.

  • Save both files.
Write the environment configuration module

Hardcoding API keys directly in your source code is a security risk. The pydantic-settings library lets you define a typed settings class that automatically reads values from a .env file or OS environment variables.

  • Open app/config.py and paste the following code:
from functools import lru_cache

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    anthropic_api_key: str
    app_api_key: str
    app_name: str = "Email Summarizer API"

    model_config = SettingsConfigDict(env_file=".env")


@lru_cache
def get_settings():
    return Settings()

What does this code do?

  • Settings extends BaseSettings to create a typed configuration class. Each field (anthropic_api_key, app_api_key, app_name) maps directly to a variable in your .env file.
  • SettingsConfigDict(env_file=".env") tells pydantic-settings to look for a file called .env in your project root and load values from it.
  • @lru_cache on get_settings() means the settings object is only created once, then reused on every subsequent call. This avoids reading the .env file on every API request.

Why does this also work with OS environment variables?

pydantic-settings automatically checks OS environment variables in addition to the .env file. OS environment variables take priority.

This means locally your app reads from .env, but on Lambda it reads from environment variables set in the SAM template.

  • Save app/config.py.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

from functools import lru_cache

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    anthropic_api_key: str
    app_api_key: str
    app_name: str = "Email Summarizer API"

    model_config = SettingsConfigDict(env_file=".env")


@lru_cache
def get_settings():
    return Settings()
Write the main application and test locally

With your configuration module ready, it's time to write the actual API. You'll start with a health-check endpoint, then add the email summarization endpoint that uses Claude.

  • Open .env and add your secrets:
ANTHROPIC_API_KEY=your-anthropic-api-key-here
APP_API_KEY=your-random-secret-key-here
APP_NAME=Email Summarizer API

What values do I put here?

  • ANTHROPIC_API_KEY: Paste your Anthropic API key from Project 1.
  • APP_API_KEY: Make up any random string (e.g. my-secret-key-123). This is the key clients will send to access your API.
  • APP_NAME: Leave this as Email Summarizer API.
  • Save .env.

Now write the FastAPI application. You'll add it in two parts: the app scaffold with a health check, then the summarize endpoint.

  • Open app/main.py and paste the following code:
from fastapi import FastAPI, Depends
from pydantic import BaseModel
import anthropic

from app.config import get_settings, Settings


app = FastAPI(title="Email Summarizer API")


class EmailRequest(BaseModel):
    email_body: str


class SummaryResponse(BaseModel):
    summary: str


@app.get("/")
def health_check():
    return {"status": "healthy", "app": "Email Summarizer API"}

What does this code do?

  • The imports bring in FastAPI, Pydantic for request/response models, the Anthropic SDK, and your settings module.
  • EmailRequest and SummaryResponse define the shape of data your API accepts and returns. FastAPI uses these for automatic validation and documentation.
  • The health_check() endpoint at / gives you a quick way to verify the app is running without calling Claude.

Now add the summarize endpoint. This is the core functionality that calls Claude to summarize an email.

  • Add the following code below the health_check function in app/main.py:
@app.post("/summarize", response_model=SummaryResponse)
def summarize_email(
    request: EmailRequest,
    settings: Settings = Depends(get_settings),
):
    client = anthropic.Anthropic(api_key=settings.anthropic_api_key)
    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=300,
        messages=[
            {
                "role": "user",
                "content": f"Summarize this email in 2-3 concise sentences:\n\n{request.email_body}",
            }
        ],
    )
    return SummaryResponse(summary=message.content[0].text)

What does this code do?

  • Depends(get_settings) tells FastAPI to inject your settings object automatically. The Anthropic API key comes from your .env file through the settings, never hardcoded.
  • The function creates an Anthropic client, sends the email body to claude-sonnet-4-6 with a summarization prompt, and returns the response wrapped in a SummaryResponse.
  • response_model=SummaryResponse ensures the API always returns the documented shape, which also powers the auto-generated docs.
  • Save app/main.py.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on to testing.

ⓧ I'd like to double check the full code

from fastapi import FastAPI, Depends
from pydantic import BaseModel
import anthropic

from app.config import get_settings, Settings


app = FastAPI(title="Email Summarizer API")


class EmailRequest(BaseModel):
    email_body: str


class SummaryResponse(BaseModel):
    summary: str


@app.get("/")
def health_check():
    return {"status": "healthy", "app": "Email Summarizer API"}


@app.post("/summarize", response_model=SummaryResponse)
def summarize_email(
    request: EmailRequest,
    settings: Settings = Depends(get_settings),
):
    client = anthropic.Anthropic(api_key=settings.anthropic_api_key)
    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=300,
        messages=[
            {
                "role": "user",
                "content": f"Summarize this email in 2-3 concise sentences:\n\n{request.email_body}",
            }
        ],
    )
    return SummaryResponse(summary=message.content[0].text)

Time to test your app. You need a virtual environment and your dependencies installed first.

  • Create a virtual environment and activate it by running these commands in your PowerShell terminal (make sure you're inside email-summarizer-api):
python -m venv .venv
.venv\Scripts\Activate.ps1

Seeing a script execution error?

If PowerShell blocks the activate script, you need to allow script execution for your user. Run this once:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

Then try .venv\Scripts\Activate.ps1 again.

Help me fix my PowerShell execution policy error

You should see (.venv) appear at the start of your terminal prompt, confirming the virtual environment is active.

  • Install your project dependencies by running:
pip install -r requirements.txt

Installation failed?

Make sure your virtual environment is active (you should see (.venv) in your prompt). If pip isn't found, try python -m pip install -r requirements.txt instead.

Help me troubleshoot my pip install errors

  • Start the development server by running:
uvicorn app.main:app --reload
  • Open a new PowerShell tab or window and test the health check by running:
Invoke-RestMethod http://127.0.0.1:8000/

You should see a response like {status: healthy, app: Email Summarizer API} confirming your app is running.

Not getting a response?

Check that uvicorn is still running in the other terminal window. Look for the line Uvicorn running on http://127.0.0.1:8000 in its output.

If you see a ModuleNotFoundError, make sure you activated the virtual environment before running uvicorn.

Help me debug my FastAPI server

  • Stop the uvicorn server by pressing Ctrl+C in the terminal where it's running.

Your app is structured, configured, and running locally. Next up, you'll add API key authentication so only authorized clients can access your summarize endpoint.

Add API Key Authentication

Your API is working locally, but right now anyone who knows the URL can call it. In production, you need to control who has access to your endpoints.

In this step, you'll add API key authentication to your /summarize endpoint. Every request will need to include a valid key in a special header, or the API will reject it.

In this step, get ready to:
  • Write an authentication module that checks API keys.
  • Protect the /summarize endpoint so only authorized requests get through.
  • Verify authentication works in the interactive docs.
Create the authentication module

FastAPI has built-in security utilities that handle API key validation and integrate with the auto-generated docs. You'll use APIKeyHeader to define which header carries the key, and a dependency function to verify it.

  • Create a new file called auth.py inside your app/ folder.
  • Add the following code:
from fastapi import Depends, HTTPException, status
from fastapi.security import APIKeyHeader

from app.config import Settings, get_settings

# Define which header carries the API key
api_key_header = APIKeyHeader(name="X-API-Key")


def verify_api_key(
    api_key: str = Depends(api_key_header),
    settings: Settings = Depends(get_settings),
):
    # Compare the incoming key against the one in settings
    if api_key != settings.app_api_key:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid API key",
        )
    return api_key

What does this code do?

  • APIKeyHeader(name="X-API-Key") tells FastAPI to look for a header called X-API-Key on incoming requests.
  • verify_api_key is a FastAPI dependency. It extracts the key from the header and compares it against the app_api_key value loaded from your .env file.
  • If the key doesn't match, the function raises a 401 Unauthorized error and the request never reaches your endpoint logic.
  • Save app/auth.py.

Seeing a red underline on the import?

If your editor shows an error on from app.config import Settings, get_settings, make sure you saved config.py in the previous step. The file must exist at app/config.py for the import to resolve.

help me fix import errors in my auth.py file

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

from fastapi import Depends, HTTPException, status
from fastapi.security import APIKeyHeader

from app.config import Settings, get_settings

api_key_header = APIKeyHeader(name="X-API-Key")


def verify_api_key(
    api_key: str = Depends(api_key_header),
    settings: Settings = Depends(get_settings),
):
    if api_key != settings.app_api_key:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid API key",
        )
    return api_key
Protect the /summarize endpoint

Now you need to wire the authentication into your existing endpoint. FastAPI's Depends system makes this a two-line change in main.py.

  • In app/main.py, find the import section at the top of the file. Find this line:
from app.config import get_settings, Settings
  • Add a new import line directly below it, so your imports look like this:
from app.config import get_settings, Settings
from app.auth import verify_api_key
  • Next, scroll down to the summarize_email function. Find this function signature:
def summarize_email(
    request: EmailRequest,
    settings: Settings = Depends(get_settings),
):
  • Add api_key: str = Depends(verify_api_key), as a new parameter between request and settings, so the signature looks like this:
def summarize_email(
    request: EmailRequest,
    api_key: str = Depends(verify_api_key),
    settings: Settings = Depends(get_settings),
):

What does adding this dependency do?

By adding Depends(verify_api_key) to the function parameters, FastAPI will automatically run your verification function before the endpoint logic executes. If the key check fails, the request is rejected and summarize_email never runs.

  • Save app/main.py. Your server will auto-reload with the changes.

Now test that the authentication is working. You'll send two requests to confirm that unauthenticated calls are blocked and authenticated calls succeed.

  • Open your browser and go to http://127.0.0.1:8000/docs.
  • Expand the POST /summarize endpoint and click Try it out.
  • Enter a test email body like Meeting moved to 3pm tomorrow. and click Execute.

You should see an error response because you haven't provided an API key yet. The request is rejected before it reaches your summarization logic.

  • Scroll back to the top of the page and click the Authorize button (green lock icon).
  • In the Value field, enter the APP_API_KEY value from your .env file.
  • Click Authorize, then click Close.
  • Try the POST /summarize endpoint again with the same test email body.

This time you should see a successful response with a Claude-generated summary of your test email.

Still getting an error after authorizing?

Make sure the value you entered in the Authorize dialog matches your APP_API_KEY in .env exactly. Even one extra space will cause a mismatch.

Check your terminal for error messages. If you see an Anthropic API error, your ANTHROPIC_API_KEY in .env may be invalid or expired.

help me debug my API key authentication in FastAPI

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before continuing.

ⓧ I'd like to double check the full code

from fastapi import FastAPI, Depends
from pydantic import BaseModel
import anthropic

from app.config import get_settings, Settings
from app.auth import verify_api_key


app = FastAPI(title="Email Summarizer API")


class EmailRequest(BaseModel):
    email_body: str


class SummaryResponse(BaseModel):
    summary: str


@app.get("/")
def health_check():
    return {"status": "healthy", "app": "Email Summarizer API"}


@app.post("/summarize", response_model=SummaryResponse)
def summarize_email(
    request: EmailRequest,
    api_key: str = Depends(verify_api_key),
    settings: Settings = Depends(get_settings),
):
    client = anthropic.Anthropic(api_key=settings.anthropic_api_key)
    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=300,
        messages=[
            {
                "role": "user",
                "content": f"Summarize this email in 2-3 concise sentences:\n\n{request.email_body}",
            }
        ],
    )
    return SummaryResponse(summary=message.content[0].text)
Confirm the security scheme in the interactive docs

FastAPI's APIKeyHeader automatically registers an OpenAPI security scheme. This means the interactive docs know your endpoint requires a key.

  • In the docs page at http://127.0.0.1:8000/docs, look at the POST /summarize endpoint row.
  • Confirm you see a lock icon next to the endpoint name.

The lock icon confirms that the OpenAPI security scheme is registered. Anyone reading your API docs will know this endpoint requires authentication.

Why does the lock icon matter?

The lock icon is generated from your code automatically. It tells API consumers (other developers, tools, clients) that this endpoint requires a key. No separate documentation needed.

Your API is now secured. Only requests carrying the correct key in the X-API-Key header can reach your summarization logic. Next up, you'll make this app deployable to AWS Lambda.

Add Mangum and the SAM Template

Your API works locally, but AWS Lambda does not receive HTTP requests the way your local server does. Lambda receives events as JSON dictionaries, so you need an adapter that translates between Lambda's event format and FastAPI's ASGI interface.

In this step, you'll add Mangum as that adapter and write a SAM template that tells AWS exactly how to deploy your function as Infrastructure as Code.

In this step, get ready to:
  • Add the Mangum handler to make your app Lambda-compatible.
  • Create a SAM template defining your serverless infrastructure.
  • Confirm your config works with both local .env files and Lambda environment variables.
Add the Mangum handler to your app

Mangum is a lightweight adapter (about 17KB) that sits between Lambda and your FastAPI app. It translates Lambda's JSON event into an ASGI request, passes it to FastAPI, and converts the response back into Lambda's expected format.

  • Open requirements.txt and confirm it already includes mangum==0.21.0. You added this in an earlier step.
  • Switch to app/main.py in your editor.
  • Find this line near the top of the file:
from pydantic import BaseModel
  • Add this new import directly below it:
from mangum import Mangum
  • Now scroll to the very bottom of main.py. After the summarize_email function, add this line:
handler = Mangum(app, lifespan="off")

What does this line do?

This single line is what makes your entire FastAPI app work on Lambda. handler is the name Lambda looks for when it receives an event.

The lifespan="off" parameter tells Mangum to skip ASGI startup and shutdown events. Lambda is stateless, so these lifecycle events do not apply.

  • Save app/main.py.
  • Your app still works locally too. Confirm by running this in your terminal:
uvicorn app.main:app --reload
  • Visit http://127.0.0.1:8000 in your browser. You should see the same healthy status JSON response as before.
  • Press Ctrl+C in your terminal to stop the server.

Seeing an ImportError for mangum?

  • Make sure you installed your dependencies with pip install -r requirements.txt in an earlier step. Run it again if needed.
  • Check that your virtual environment is activated. You should see (.venv) at the start of your terminal prompt.

Still stuck? Help me fix this ImportError for mangum in my FastAPI project.

Why Mangum over Lambda Web Adapter?

AWS offers its own Lambda Web Adapter, but it requires an additional shell script and adds cold start overhead for the uvicorn server startup.

Mangum is a single import and one line of code. For learning how FastAPI connects to Lambda, Mangum makes the adapter pattern completely transparent.

✔️ Awesome, I've got everything!

Great. Double check you've saved app/main.py before moving on.

ⓧ I'd like to double check the full code

from fastapi import FastAPI, Depends
from pydantic import BaseModel
from mangum import Mangum
import anthropic

from app.config import get_settings, Settings
from app.auth import verify_api_key


app = FastAPI(title="Email Summarizer API")


class EmailRequest(BaseModel):
    email_body: str


class SummaryResponse(BaseModel):
    summary: str


@app.get("/")
def health_check():
    return {"status": "healthy", "app": "Email Summarizer API"}


@app.post("/summarize", response_model=SummaryResponse)
def summarize_email(
    request: EmailRequest,
    api_key: str = Depends(verify_api_key),
    settings: Settings = Depends(get_settings),
):
    client = anthropic.Anthropic(api_key=settings.anthropic_api_key)
    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=300,
        messages=[
            {
                "role": "user",
                "content": f"Summarize this email in 2-3 concise sentences:\n\n{request.email_body}",
            }
        ],
    )
    return SummaryResponse(summary=message.content[0].text)


handler = Mangum(app, lifespan="off")
Create the SAM template

A SAM template is a YAML file that describes your entire serverless architecture. Instead of clicking through the AWS console to create resources, you define everything in code. SAM reads this file and creates your Lambda function, API Gateway, and permissions automatically.

  • Create a new file called template.yaml at your project root (the same level as requirements.txt).
  • Paste the following into template.yaml:
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Email Summarizer API deployed on AWS Lambda

Globals:
  Function:
    Timeout: 30
    MemorySize: 512

Resources:
  EmailSummarizerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./
      Handler: app.main.handler
      Runtime: python3.12
      Architectures:
        - x86_64
      Environment:
        Variables:
          ANTHROPIC_API_KEY: your-anthropic-api-key-here
          APP_API_KEY: your-random-secret-key-here
          APP_NAME: Email Summarizer API
      Events:
        Api:
          Type: HttpApi
          Properties:
            Path: /{proxy+}
            Method: ANY
        Root:
          Type: HttpApi
          Properties:
            Path: /
            Method: ANY

Outputs:
  ApiUrl:
    Description: API Gateway endpoint URL
    Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"

What does this template define?

  • Handler: app.main.handler tells Lambda to call the handler variable you just created in app/main.py.
  • Runtime: python3.12 sets the Lambda execution environment. Your local Python is 3.14, but Lambda currently supports up to 3.12. The SAM build step handles this mismatch for you.
  • The /{proxy+} path captures all API routes (like /summarize) and forwards them to your function. A separate / event is needed because the proxy pattern does not match the root path.
  • The Environment Variables section passes your API keys to the Lambda function at runtime.
  • Replace your-anthropic-api-key-here and your-random-secret-key-here with the same values from your .env file.
  • Save template.yaml.
  • Confirm the file exists by running:
ls template.yaml
  • You should see template.yaml listed in the output.

File not found?

  • Make sure you created template.yaml at the project root, not inside the app/ folder.
  • Check that your terminal is in the project root by running ls. You should see both app/ and requirements.txt listed.

Need more help? Help me figure out where to place my template.yaml file.

Why HttpApi instead of REST API?

SAM supports two API Gateway types. HttpApi (HTTP API) costs $1.00 per million requests. The older REST API type costs $3.50 per million requests. For a straightforward proxy to Lambda, HTTP API is the better fit.

✔️ Awesome, I've got everything!

Great. Make sure you've replaced the placeholder API key values with your real keys before moving on.

ⓧ I'd like to double check the full code

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Email Summarizer API deployed on AWS Lambda

Globals:
  Function:
    Timeout: 30
    MemorySize: 512

Resources:
  EmailSummarizerFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./
      Handler: app.main.handler
      Runtime: python3.12
      Architectures:
        - x86_64
      Environment:
        Variables:
          ANTHROPIC_API_KEY: your-anthropic-api-key-here
          APP_API_KEY: your-random-secret-key-here
          APP_NAME: Email Summarizer API
      Events:
        Api:
          Type: HttpApi
          Properties:
            Path: /{proxy+}
            Method: ANY
        Root:
          Type: HttpApi
          Properties:
            Path: /
            Method: ANY

Outputs:
  ApiUrl:
    Description: API Gateway endpoint URL
    Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
Confirm your config handles Lambda environment variables

Your SAM template passes secrets as environment variables to the Lambda function. Locally, your app reads secrets from the .env file. You need both paths to work without changing any code.

The good news is that pydantic-settings already handles this for you. No changes to config.py are needed.

  • Open app/config.py and review the Settings class:
class Settings(BaseSettings):
    anthropic_api_key: str
    app_api_key: str
    app_name: str = "Email Summarizer API"

    model_config = SettingsConfigDict(env_file=".env")

How does pydantic-settings resolve values?

When your app starts, pydantic-settings checks multiple sources in this priority order:

  • OS environment variables (highest priority).
  • Values from the .env file.
  • Default values defined in the class (lowest priority).

On Lambda, the environment variables from your SAM template are OS-level variables. They take priority over the .env file. Locally, there are no OS variables set, so .env wins. One class, two environments, zero code changes.

Your app is now fully configured for Lambda. Next up, you'll build and deploy it to AWS with just two commands.

Deploy to AWS Lambda

Your application is fully configured with authentication, environment management, and a Lambda-ready handler. Now it is time to ship it to the cloud.

In this step, you will use two SAM commands to package your code with Docker and deploy it to AWS Lambda. By the end, you will have a live HTTPS endpoint that anyone with your API key can call to summarize emails.

In this step, get ready to:
  • Build your Lambda deployment package using Docker.
  • Deploy your app to AWS with SAM.
  • Test the live API endpoint with a real email.
Build your Lambda package with Docker

Your local machine runs Python 3.14, but Lambda runs Python 3.12. If you build your dependencies locally, they might not work on Lambda because of architecture differences.

The --use-container flag tells SAM to build inside a Docker container that matches the Lambda runtime exactly. This guarantees your packages are compiled for the right environment.

  • Make sure Docker Desktop is running (check for the whale icon in your system tray).
  • Build your Lambda package by running this command in your project's PowerShell terminal:
sam build --use-container

What does this command do?

SAM downloads a Docker image matching the python3.12 Lambda runtime. It installs all your requirements.txt packages inside that container, then copies the compiled output to a .aws-sam/ folder in your project.

The build takes 1-2 minutes the first time because Docker needs to pull the base image.

  • Wait for the build to finish. You should see Build Succeeded in the terminal output.

Build failing or timing out?

  • Make sure Docker Desktop is running. SAM cannot build with --use-container unless Docker is active.
  • If you see a permissions error, try running your PowerShell terminal as Administrator.
  • If the build hangs on pulling the Docker image, check your internet connection. The first pull downloads approximately 500MB.

Help me fix my sam build error.

Deploy your stack to AWS

With the build ready, SAM can now package your code, upload it to S3, and create all the AWS resources defined in your template.yaml. The guided deploy walks you through configuration prompts and saves your answers for future deployments.

  • Start the guided deployment by running this command:
sam deploy --guided

What does this command do?

SAM uploads your built code to an S3 bucket, creates a CloudFormation stack, and provisions all the resources in your template (Lambda function, API Gateway, IAM role).

The --guided flag triggers an interactive prompt sequence so you can configure the deployment step by step.

SAM will ask you a series of questions. Answer each prompt as follows:

  • For Stack Name, type email-summarizer-api and press Enter.
  • For AWS Region, type us-east-1 and press Enter.
  • For Confirm changes before deploy, press Enter to accept the default (Y).
  • For Allow SAM CLI IAM role creation, press Enter to accept the default (Y).
  • For Disable rollback, press Enter to accept the default (N).
  • For the authorization prompt (EmailSummarizerFunction may not have authorization defined, Is this okay?), type y and press Enter.
  • For Save arguments to configuration file, press Enter to accept the default (Y).
  • For SAM configuration file and SAM configuration environment, press Enter to accept the defaults.

Why say yes to the authorization prompt?

SAM detects that your API Gateway endpoint is publicly accessible without AWS-level authorization (like IAM auth or a Cognito authorizer). This is expected because your app handles authentication itself via the X-API-Key header check in auth.py.

Typing y tells SAM this is intentional.

  • If SAM asks you to confirm the changeset, type y and press Enter.
  • Wait for the deployment to complete. This takes 2-3 minutes while CloudFormation creates your resources.
  • Look for the Outputs section at the bottom of the terminal. Find the ApiUrl value.
  • Copy this URL. It looks like https://abc123xyz.execute-api.us-east-1.amazonaws.com and you will need it to test your deployed API.

Deploy failed or stuck?

  • If you see an error about credentials, run aws sts get-caller-identity to verify your AWS credentials are still configured.
  • If the deploy aborts with a security constraints error, re-run sam deploy --guided and make sure you type y at the authorization prompt.
  • If CloudFormation rolls back, check that the API key values in your template.yaml environment variables are your actual keys, not placeholders.

Help me debug my SAM deployment failure.

Test your live API endpoint

Your API is now live on the internet. Time to prove it works by sending a real email summarization request to your deployed Lambda function.

  • Test your deployed API by running this command in PowerShell (replace the URL with your actual API Gateway endpoint and the API key with your actual APP_API_KEY value from template.yaml):
$headers = @{"X-API-Key" = "your-app-api-key-here"}
$body = '{"email_body": "Hi team, just a reminder that the quarterly review meeting is scheduled for Friday at 2pm in Conference Room B. Please bring your department reports and be prepared to discuss Q2 targets. Thanks, Sarah"}'
Invoke-RestMethod -Uri "[[API_URL="your API Gateway endpoint URL"]]/summarize" -Method Post -Headers $headers -Body $body -ContentType "application/json"

What should I see?

You should see a JSON response with a summary field containing a 2-3 sentence summary of the email written by Claude. The response comes from your Lambda function running in the cloud, not your local machine.

  • Verify that the response contains a summary field with a concise summary of the meeting reminder email.

Getting a 401 or 403 error?

  • A 401 means your X-API-Key header value does not match the APP_API_KEY environment variable in your template.yaml. Double-check they are the same string.
  • A 403 usually means your URL path is wrong. Make sure you appended /summarize to the end of your API Gateway URL.
  • If you get a 500 error, your Anthropic API key in template.yaml might be incorrect. Update it, then redeploy with sam build --use-container followed by sam deploy.

Help me fix my deployed API error.

Your email summarizer is now live and accessible from anywhere on the internet. Next up, you will tackle a secret mission to extend your API with a second Claude-powered endpoint.

Secret mission

Add an Email Priority Classifier Endpoint

Your API can summarize emails, but can it tell you which ones are urgent? Add a second endpoint that uses a faster, cheaper Claude model to classify email priority in real time.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Your Lambda function scales to zero when idle, so it costs nothing unless you exceed AWS Free Tier limits (1 million Lambda requests and 1 million API Gateway requests per month).

Resources you used:

  • AWS CloudFormation stack (email-summarizer-api) containing: 1 Lambda function, 1 API Gateway HTTP API, 1 IAM role.
  • Local project files (app/, template.yaml, requirements.txt, samconfig.toml, .env, .gitignore).
  • AWS CLI and AWS SAM CLI (installed via MSI in Step 1).
  • AWS credentials stored in the .aws folder in your user profile.

Keep everything running

No action needed. Choose this if you plan to keep building on this API or want it available for testing.

  • Your Lambda function costs nothing when idle. It only incurs charges if you exceed the AWS Free Tier.
  • Monitor usage in the AWS Lambda console under the Monitoring tab.
  • The AWS CLI and SAM CLI remain installed for future projects.

Pause - I'll come back to this later

Lambda already scales to zero automatically. There is no separate pause action needed for serverless resources.

  • Your API stays live but incurs zero cost while no one calls it.
  • When you return, you can redeploy changes with sam build --use-container and sam deploy.

Delete - I don't want to use this again

Remove all AWS resources, local files, and optionally the CLI tools you installed.

  • Delete the CloudFormation stack (Lambda, API Gateway, IAM role) by running this command in PowerShell from your project folder:
sam delete --stack-name email-summarizer-api
  • Confirm the deletion when prompted. SAM removes all AWS resources cleanly.
  • Delete your local project files. In PowerShell, navigate to the parent folder that contains your project directory, then run:
Remove-Item -Recurse -Force .\app, .\template.yaml, .\requirements.txt, .\samconfig.toml, .\.env, .\.gitignore, .\.aws-sam
  • (Optional) Uninstall the AWS SAM CLI. From the Start menu, search for Add or remove programs. Find AWS SAM Command Line Interface and select Uninstall.
  • (Optional) Uninstall the AWS CLI. From the Start menu, search for Add or remove programs. Find AWS Command Line Interface and select Uninstall.
  • (Optional) Remove your stored AWS credentials by deleting the .aws folder in your user profile:
Remove-Item -Recurse -Force $HOME\.aws

Removing credentials is permanent

Deleting the .aws folder removes your stored access keys and configuration. If you use the AWS CLI for other projects, skip this step.

That's a Wrap!

That's a Wrap!

Nice work! You just took a local Python API and deployed it as a live, serverless endpoint on AWS. Anyone with your API key can now send an email and get back an AI-powered summary from anywhere on the internet.

You've learned how to:

  • Secure a FastAPI application with API key authentication and manage secrets through pydantic-settings with a .env file.
  • Connect a FastAPI app to AWS Lambda using the Mangum ASGI adapter and define your serverless infrastructure with a SAM template.
  • Deploy a live API to Lambda and API Gateway with just two commands: sam build --use-container and sam deploy.
  • Secret Mission: Added a second endpoint using Claude Haiku 4.5 to classify email priority, demonstrating how to route different tasks to different models.

Ready to quiz yourself?