Deploy a FastAPI App

Su Suriya Ravichandran Updated 10 Nov 2026
6 min read ·Lesson 189 of 200

A Web Service in Front of the Model

Lesson 68 introduced FastAPI as a way to put Claude behind a web API. This lesson takes that service to deployment. A deployed FastAPI application has to create its client once, hand each request its own answer or a clear failure, translate internal errors into responses that are safe to show, and report whether it is healthy. Each of those needs a deliberate design, and the deployment itself adds the server that runs the application and the platform that routes traffic to it.

The example is a small question-answering service with two endpoints: one that asks the model, and one that reports health. The lesson covers the lifecycle that creates and closes the client, the request model that validates input, the mapping from SDK errors to HTTP responses, and the way the service is started. The mapping is the part most often done carelessly, because a raw error message can reveal configuration to anyone who receives it.


One Client for the Whole Process

A web application handles many requests, and creating a client for each one wastes connections and configuration. FastAPI's lifespan mechanism creates the client when the application starts, makes it available to every request, and closes it when the application shuts down. The asynchronous client from Lesson 72 is the natural choice, because a web server handles many requests concurrently:

from contextlib import asynccontextmanager

from anthropic import AsyncAnthropic
from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.client = AsyncAnthropic(timeout=30.0, max_retries=2)
    yield
    await app.state.client.close()


app = FastAPI(lifespan=lifespan)

The client is stored on the application's state, so a request handler reaches it through the request. The timeout and retry settings from Lessons 143 and 144 are set here, once, for every request the service makes.


Validating the Request

The question arrives as JSON, and the request model checks it before the handler runs. A length limit bounds the cost of each request, and the validation rules from Lesson 174 are applied by declaring them on the model:

from pydantic import BaseModel, Field


class AskRequest(BaseModel):
    question: str = Field(min_length=1, max_length=2000)

A request that breaks the rules receives a validation error from FastAPI before any model call is made, so invalid input costs nothing.


Mapping Errors to Responses

A failed model call must become an HTTP response, and the response must tell the caller what happened without revealing what the service is. The status code tells the caller whether to retry, and the message should be short and fixed. A raw SDK message can include the key's identity, the account's configuration, or a region name, so it is logged and not returned. The mapping below applies that rule:

"""error_responses.py: map SDK errors to HTTP status codes and messages that are safe to return."""

from anthropic import (
    APIConnectionError, APIStatusError, APITimeoutError, AuthenticationError,
    BadRequestError, InternalServerError, PermissionDeniedError, RateLimitError,
)


def http_status_for(error: Exception) -> tuple[int, str]:
    if isinstance(error, APITimeoutError):
        return 504, "The model took too long to answer. Please try again."
    if isinstance(error, APIConnectionError):
        return 503, "The service is temporarily unavailable. Please try again."
    if isinstance(error, RateLimitError):
        return 429, "The service is busy. Please wait a moment and try again."
    if isinstance(error, (AuthenticationError, PermissionDeniedError)):
        return 500, "The service is misconfigured. The operators have been notified."
    if isinstance(error, BadRequestError):
        return 400, "The request could not be processed."
    if isinstance(error, InternalServerError):
        return 503, "The service is temporarily unavailable. Please try again."
    if isinstance(error, APIStatusError):
        return 500, "Something went wrong. Please try again later."
    return 500, "Something went wrong. Please try again later."


if __name__ == "__main__":
    from api_errors import fake_status_error
    import httpx

    request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
    samples = [
        APITimeoutError(request=request),
        fake_status_error(429, "rate_limit_error", "slow", "req_1"),
        fake_status_error(401, "authentication_error", "bad key sk-demo", "req_2"),
        fake_status_error(400, "invalid_request_error", "max_tokens too large", "req_3"),
    ]
    for error in samples:
        status, message = http_status_for(error)
        print(status, message)

Output:

504 The model took too long to answer. Please try again.
429 The service is busy. Please wait a moment and try again.
500 The service is misconfigured. The operators have been notified.
400 The request could not be processed.

How It Works

  1. Timeouts map to 504, a gateway timeout, which tells the caller the upstream service did not answer in time.
  2. Connection failures and server errors map to 503, meaning the service is temporarily unavailable, which a caller may retry.
  3. Rate limits map to 429, and the message asks the caller to wait, which matches the backoff advice from Lesson 146.
  4. Authentication and permission errors map to 500, with a message that hides the cause. The third sample's error message contains a key-like string, and it is not returned. The caller learns only that the service is misconfigured, while the operators receive the details through logging.
  5. A bad request maps to 400 without echoing the model's complaint, since the caller's input is the only thing it could correct, and the full message is logged for the developers.
  6. The final branch is a catch-all, so an unforeseen error still produces a safe response.

The message returned for the third sample never includes the words of the original error, and that is the property the function exists to guarantee.


The Endpoints

The request handler calls the model through the shared client, maps any failure to a response, and returns the answer otherwise. The health endpoint answers without calling the model, so a platform can check that the process is running without spending tokens:

from fastapi import HTTPException
from anthropic import APIError

from error_responses import http_status_for


@app.post("/ask")
async def ask(body: AskRequest):
    try:
        response = await app.state.client.messages.create(
            model="claude-sonnet-5-5",
            max_tokens=512,
            messages=[{"role": "user", "content": body.question}],
        )
    except APIError as error:
        status, message = http_status_for(error)
        raise HTTPException(status_code=status, detail=message)
    answer = "".join(block.text for block in response.content if block.type == "text")
    return {"answer": answer}


@app.get("/health")
async def health():
    return {"status": "ok"}

The handler catches APIError, the base of the SDK's error hierarchy (Lesson 141), so every failure from the client is covered, and the more specific mapping decides the response. The health check depends on nothing external, so a failing model service does not make the process look dead to the platform, which would restart it for no benefit.


Starting the Service

The application is started with an ASGI server, which runs the FastAPI application and handles connections:

uvicorn app.main:app --host 0.0.0.0 --port 8000

The server runs in the container from Lesson 188, and the platform routes traffic to its port. Several server processes can run side by side for capacity. Each one creates its own client during startup, and the rate-limit planning from Lesson 170 must count every process, since they share the account's quota.


Summary

A FastAPI service creates one asynchronous client at startup and closes it at shutdown, with the timeout and retry settings applied once. Requests are validated by a model before any call is made. SDK errors are mapped to status codes and fixed messages that never reveal configuration, while the details are logged. The health endpoint answers without the model, so the platform's checks do not depend on an external service. The next lesson covers how to keep such a service observable once it is running in production.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Deploy a FastAPI App and get answers drawn from it.

Signed-in readers only.