← Blog

Claude API FastAPI Backend Example in Python

2026-11-09 · 4 min read · SubToAPI Team

If you're building a Python backend that talks to Claude, FastAPI is a natural fit: async support, Pydantic validation, and minimal boilerplate. This article walks through a working Claude API FastAPI backend example — a single service that accepts chat requests from a frontend, forwards them to Claude, and returns both plain and streamed responses.

The core pattern is always the same: FastAPI handles HTTP routing and validation, an async HTTP client calls the Claude-compatible endpoint, and you shape the response (JSON or Server-Sent Events) for your frontend. Everything below works whether you call Anthropic directly or route through a proxy like SubToAPI — the only thing that changes is the base URL and key.

Project setup

Install the dependencies:

pip install fastapi uvicorn httpx python-dotenv

Project structure:

app/
  main.py
  claude_client.py
  models.py
.env

Store your credentials in .env:

CLAUDE_API_KEY=sk-ant-your-key-here
CLAUDE_BASE_URL=https://api.anthropic.com/v1

Defining request models

Use Pydantic to validate incoming requests before they ever reach Claude. This catches malformed payloads early and gives you clean type hints throughout the app.

# app/models.py
from pydantic import BaseModel
from typing import List, Optional

class Message(BaseModel):
    role: str
    content: str

class ChatRequest(BaseModel):
    messages: List[Message]
    model: str = "claude-3-5-sonnet-20241022"
    max_tokens: int = 1024
    system: Optional[str] = None

A thin Claude client

Wrap the HTTP call in its own module so routes stay small and the client can be reused or mocked in tests.

# app/claude_client.py
import os
import httpx

CLAUDE_BASE_URL = os.getenv("CLAUDE_BASE_URL", "https://api.anthropic.com/v1")
CLAUDE_API_KEY = os.getenv("CLAUDE_API_KEY")

async def send_message(payload: dict) -> dict:
    headers = {
        "x-api-key": CLAUDE_API_KEY,
        "anthropic-version": "2023-06-01",
        "content-type": "application/json",
    }
    async with httpx.AsyncClient(timeout=60) as client:
        response = await client.post(
            f"{CLAUDE_BASE_URL}/messages",
            headers=headers,
            json=payload,
        )
        response.raise_for_status()
        return response.json()

The FastAPI route

# app/main.py
from fastapi import FastAPI, HTTPException
from app.models import ChatRequest
from app.claude_client import send_message
import httpx

app = FastAPI()

@app.post("/chat")
async def chat(request: ChatRequest):
    payload = {
        "model": request.model,
        "max_tokens": request.max_tokens,
        "messages": [m.dict() for m in request.messages],
    }
    if request.system:
        payload["system"] = request.system

    try:
        result = await send_message(payload)
    except httpx.HTTPStatusError as exc:
        raise HTTPException(status_code=exc.response.status_code, detail=exc.response.text)
    except httpx.TimeoutException:
        raise HTTPException(status_code=504, detail="Claude request timed out")

    return result

Run it with:

uvicorn app.main:app --reload

Test it:

curl -X POST http://localhost:8000/chat \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"Explain FastAPI in one sentence"}]}'

Adding streaming

For chat UIs, users expect tokens as they're generated instead of waiting for the full response. FastAPI supports this with StreamingResponse, forwarding Claude's SSE stream directly to the client.

from fastapi.responses import StreamingResponse
import httpx, json, os

CLAUDE_BASE_URL = os.getenv("CLAUDE_BASE_URL")
CLAUDE_API_KEY = os.getenv("CLAUDE_API_KEY")

async def stream_claude(payload: dict):
    headers = {
        "x-api-key": CLAUDE_API_KEY,
        "anthropic-version": "2023-06-01",
        "content-type": "application/json",
    }
    payload["stream"] = True

    async with httpx.AsyncClient(timeout=None) as client:
        async with client.stream(
            "POST", f"{CLAUDE_BASE_URL}/messages", headers=headers, json=payload
        ) as response:
            async for line in response.aiter_lines():
                if line:
                    yield f"{line}\n\n"

@app.post("/chat/stream")
async def chat_stream(request: ChatRequest):
    payload = {
        "model": request.model,
        "max_tokens": request.max_tokens,
        "messages": [m.dict() for m in request.messages],
    }
    return StreamingResponse(stream_claude(payload), media_type="text/event-stream")

The frontend can then consume this with EventSource or a fetch reader, same pattern you'd use for any SSE endpoint.

Error handling and timeouts

Three things break production Claude backends more than anything else:

Using SubToAPI instead of calling Anthropic directly

If you don't want to manage raw Anthropic keys, rate limit handling, and per-team usage tracking yourself, you can point the exact same FastAPI code at SubToAPI instead. It turns your Claude access into a standard HTTPS API with sub_live_... keys, so the client module above barely changes:

CLAUDE_BASE_URL = "https://api.subtoapi.app/v1"
CLAUDE_API_KEY = os.getenv("SUBTOAPI_KEY")

Everything else — routes, Pydantic models, streaming logic — stays identical, because the request/response shape follows the same Messages format. See the quickstart, messages docs, and streaming docs for the exact payloads. SubToAPI also adds team seats and usage metadata per key, which is useful once more than one service or developer is calling Claude through your backend. Plans start at €9/month with a free trial at signup; full pricing is on the pricing page.

Wrapping up

A Claude API FastAPI backend doesn't need much: a Pydantic model for validation, a thin async client for the HTTP call, one route for plain responses, and one for streaming. From there, add retries, logging, and timeouts as your traffic grows. Whether you call Anthropic directly or route through a managed layer like SubToAPI, the FastAPI code itself stays the same — only the base URL and key change.

Questions

Does FastAPI require any special SDK to call Claude? No. A standard async HTTP client like httpx is enough — you don't need Anthropic's Python SDK unless you want its convenience methods and built-in retries.

How do I handle long-running Claude requests without blocking FastAPI? Use async def routes with an async HTTP client (httpx.AsyncClient) so requests don't block the event loop, and set explicit timeouts so stuck connections don't tie up workers.

Can I use the same FastAPI code with SubToAPI instead of Anthropic directly? Yes. Change the base URL to https://api.subtoapi.app/v1 and use a sub_live_... key — the request and response format follows the Messages API, so routes and models don't need rewrites. See /docs for details.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →