SuperAPI AI Directory

fastapi-ai-patterns

Coding & development

View repository

Updated 10-15 20:33

About this skill

FastAPI AI Patterns

Opinionated, production-calibrated guidance for FastAPI services — with the focus on what makes AI/ML/LLM serving different from an ordinary CRUD API. It is a knowledge skill: it teaches the decisions and the traps, then routes you to a per-topic reference for depth.

Stock agents already write basic FastAPI well (routes, Pydantic models, simple CRUD). This skill spends its tokens on the parts they get wrong by default: the concurrency model, the lifespan/singleton lifecycle, the security boundaries, and the discipline that turns a stochastic LLM into a typed component.

Inspired by FastAPI for AI Engineers: From First Endpoint to Production-Scale AI Systems (AI Engineering Insider, 2026). All content here is re-expressed in original wording; consult the book for its worked examples and case studies.

When to use

  • Building a FastAPI service that serves ML models, embeddings, RAG, or wraps an LLM
  • Reviewing or debugging a FastAPI app for production-readiness (latency, leaks, auth)
  • Deciding def vs async def for a handler, or diagnosing a "p99 exploded under load" incident
  • Designing inference, streaming-chat, embedding, or document-Q&A endpoints
  • Hardening an endpoint: response shape, authz, rate limiting, input/output guardrails

When NOT to use

  • You want a full project skeleton generated → use fastapi-ai-scaffold
  • You are drilling interview questions on these topics → use fastapi-ai-interview-prep
  • Pure frontend/Next.js work, or a non-FastAPI Python web stack (Flask/Django) → different skills

Core mental model: the endpoint is a typed contract

A FastAPI endpoint signature is the API contract. Parameter annotations declare what the request must contain; the return annotation or response_model declares what the response will contain. The framework enforces both at runtime and publishes them as OpenAPI. Everything else — validation, docs, the 422 error body, SDK generation — is derived from that one declaration.

Two consequences worth internalizing:

  1. The annotation is the single source of truth. Don't duplicate validation logic that a Pydantic field constraint already expresses.
  2. FastAPI is a thin layer over Starlette + Pydantic. When you debug, you are often reading Starlette (routing, middleware, BackgroundTasks) or Pydantic (validation, coercion) source. Knowing which layer owns a behavior is the difference between guessing and fixing.

The single most important decision: def vs async def

FastAPI runs async def handlers on one event loop; it dispatches plain def handlers to a threadpool (~40 threads by default). Choose by the workload, not by habit:

Workload Correct handler Why
Async-capable I/O (httpx, asyncpg, async redis) async def + await Loop interleaves thousands of in-flight waits
Blocking-only library (requests, classic ORM, boto3) plain def Threadpool absorbs the block; loop stays free
Light CPU (< a few ms) either Negligible either way
Heavy CPU (model inference, parsing, crypto) plain def, or offload (to_thread/process pool/queue) Never run multi-ms CPU on the loop

The deadliest FastAPI production bug: an async def handler that makes a blocking call inside it (requests.get, time.sleep, a sync model .predict(), a CPU-heavy parse). It passes every single-request test, then under concurrency it freezes the entire loop — and p99 explodes for every endpoint at once, including /health. See references/async-and-external.md.

Top gotchas (read before you write the handler)

These are the corrections an agent most often needs. They are environment facts that defy reasonable assumptions, so they live here, not in a reference.

  • async def + blocking call = whole-process stall. One non-cooperating coroutine starves every other request on the loop. If a call isn't truly async, use plain def or offload it. Detect with a loop-lag watchdog.
  • Load ML models once, in the lifespan handler — never per request, never at import time. Per-request loading re-reads weights on every call (and OOMs under concurrency); import-time loading makes tests, linters, and CLI tools pay the cost (or crash without a GPU). Lifespan loading runs once per worker before traffic and integrates with readiness probes. With N workers you hold N copies — size worker count by memory, or front a dedicated inference server.
  • Every response needs a response_model (or explicit return type). Returning an ORM object directly serializes all its columns — that is how hashed_password ends up in a JSON response. Separate request / storage / response schemas; the response schema is a structural leak-prevention boundary.
  • Object-level authorization must be enforced in the query, not by review. BOLA (broken object-level authorization) tops the OWASP API list because GET /orders/{id} that trusts the path id leaks other users' data. Filter by owner in the data layer (WHERE owner_id = current_user.id), so an unauthorized fetch returns 404 by construction — don't rely on a reviewer remembering.
  • Echo model_version (and embedding model version) in every AI response. When quality regresses the first question is "which model produced this?", and canary rollouts serve multiple versions at once. Vectors from different embedding models are incomparable — a model upgrade means re-embedding the corpus.
  • Treat LLM output as untrusted input: generate → validate with Pydantic → retry with the error appended → fall back. This loop is the single most useful LLM-engineering pattern: it converts a stochastic text generator into a typed component with an explicit failure mode. One retry fixes most cases; on the second failure raise a typed error to a defined fallback (human review, cheaper deterministic path, or 502) — never pass malformed data downstream.
  • In RAG, enforce access control in retrieval (SQL/filter), never in the prompt. Never let the model see chunks the user can't access and "ask it" to respect permissions — prompt injection defeats that. The authorization boundary lives outside the model.
  • Per-process singletons (httpx.AsyncClient, DB engine, Redis client, model handle) belong in lifespan, injected via a dependency — not constructed per request. A per-request httpx client pays TCP+TLS handshakes every call and leaks connections; a lifespan-scoped client reuses keepalive connections and enforces global limits.
  • Wrap streaming generators in try/finally. Clients disconnect mid-stream constantly; token metering, logging, and upstream stream cancellation must run even then, or you keep paying for abandoned generations and lose usage data.
  • Keep /health trivially cheap; use a separate /ready for dependency readiness. A health check that does real work (or shares the event loop with heavy handlers) will fail under load and trigger restart storms. Liveness and readiness are different questions and need different probes.
  • Retry only idempotent operations on transient errors, with backoff + jitter and a total deadline below your caller's timeout. Naive retries amplify load exactly when an upstream is weakest (retry storm); synchronized retries are a self-inflicted thundering herd. Add a circuit breaker for repeated failure.
  • Validate JWTs with a pinned algorithm; hash passwords with bcrypt/argon2, not SHA-256. Accepting the token's own alg header enables the alg=none forgery; fast hashes make password cracking cheap. See references/security.md.

How to use the references

Load the one reference that matches the decision in front of you — don't preload them all. Each is self-contained with patterns, gotchas, and capacity/cost rules restated in original wording.

  • references/api-design.md — Read when designing routes, status codes, pagination/versioning, or Pydantic v2 request/response schemas (Ch 1–3 scope: ASGI vs WSGI, REST semantics, idempotency, validation, leak prevention).
  • references/architecture-di.md — Read when structuring a project or wiring Depends() (Ch 4: DI resolution + caching, yield deps for DB sessions, router/service/repository layering, composition root, dependency_overrides).
  • references/database.md — Read when adding persistence (Ch 5: session-per- request, N+1 + selectinload/joinedload, SQLModel vs SQLAlchemy+Pydantic, Alembic, pool sizing, PgBouncer, flush vs commit).
  • references/security.md — Read when adding auth or hardening (Ch 6: password hashing, JWT + revocation, API keys, CORS, rate limiting, OWASP API risks, BOLA).
  • references/testing.md — Read when writing tests (Ch 7: TestClient, dependency_overrides, SQLite-in-memory tiers, authz matrix, time injection, coverage honesty, 422-body assertions).
  • references/async-and-external.md — Read when doing concurrency or external calls (Ch 8: event loop model, the full def/async table, httpx client + retries + circuit breaker, BackgroundTasks vs Celery, webhooks, uploads, streaming, WebSockets, backpressure).
  • references/ai-ml-serving.md — Read when serving models or LLMs (Ch 9: model loading, batch + dynamic micro-batching, LLM gateway + SSE streaming, embeddings + vector DB + RAG, guardrails, validation loop, versioning/monitoring/ HITL, LLM cost model).
  • references/deployment-observability.md — Read when deploying or operating (Ch 10: Uvicorn/Gunicorn workers, Docker, K8s probes, logs/metrics/traces, caching + stampede, load testing, graceful degradation, production checklist).

Authoritative sources (link, don't paraphrase from memory)

FastAPI's API surface and the surrounding ecosystem change between minor versions. When unsure of exact syntax or current behavior, fetch the docs:

Gotchas

The top, cross-cutting gotchas are in the section above (they apply before you even pick a topic). Topic-specific traps live in each reference's own ## Gotchas section — for example, flush() vs commit() timing in database.md, the alg=none attack in security.md, and SSE proxy-buffering in ai-ml-serving.md. Load the reference for the area you're working in so you read its gotchas before hitting the situation, not after.