Python Framework

FastAPI

FastAPI is the async Python framework built on type hints. Pydantic validates your data, Starlette handles the HTTP, and the OpenAPI docs write themselves.

intermediate15 min readUpdated Sep 16, 2026
main.py
python
# main.py
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float


@app.post("/items", status_code=201)
async def create_item(item: Item) -> Item:
    return item
Released
2018
Language
Python
Built on
Starlette + Pydantic
Core idea
Type hints as the contract
Docs
OpenAPI and Swagger UI
Current line
0.11x

Why it matters

Why FastAPI feels like magic (and isn't)

Types are the API

Declare a parameter or a model with type hints and FastAPI validates requests, serialises responses and documents the endpoint from the same declaration.

Async when you need it

Built on Starlette's ASGI toolkit, so async endpoints handle concurrent I/O without blocking the event loop.

Docs for free

Every endpoint appears in interactive Swagger UI and ReDoc, generated from the OpenAPI schema FastAPI builds at startup.

The big picture

Types, Pydantic, dependencies

Type hints describe the contract, Pydantic enforces it, and dependencies inject the shared pieces a request needs.

Type hints

Describe

Annotating parameters tells FastAPI where a value comes from and what shape it has, so validation happens before your function runs.

Pydantic models

Validate

BaseModel classes parse, coerce and reject data, and double as the response contract and the generated schema.

Dependencies

Inject

Depends() composes shared logic such as sessions, auth and settings, resolving the graph per request and reusing cached results.

At a glance

The FastAPI toolbox

Path operations

@app.get and @app.post bind a URL and a function, with typed path and query parameters.

Pydantic models

BaseModel classes validate request bodies and shape responses.

Automatic docs

Swagger UI at /docs and ReDoc at /redoc, generated from your code.

Dependencies

Depends() injects sessions, users, pagination and configuration.

Async support

async def endpoints run on ASGI and await I/O concurrently.

Background tasks

Run work after the response is sent without adding a queue.

The complete guide

FastAPI: Everything you need to know

What is FastAPI?

FastAPI is a modern Python web framework for building APIs. Its defining idea is that your type hints are the contract. Declare a parameter as int, a body as a Pydantic model, or a return type as ItemOut, and the framework validates the request, serialises the response and writes the documentation — all from that one declaration.

It is built on two libraries you should know by name. Starlette provides the ASGI toolkit: routing, middleware and the request/response cycle. Pydantic provides the validation and serialisation. FastAPI is the layer that combines them, adds dependency injection, and generates an OpenAPI schema at startup.

The result is that a small, readable function gives you a validated endpoint with interactive docs. The magic is real, but it is mechanical, and understanding the mechanics is what this guide is about.

Path operations

A path operation is a function bound to an HTTP method and a URL. The decorator names the method, and the function signature tells FastAPI what to expect.

# main.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello, world"}


@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
    return {"item_id": item_id, "q": q}

The {item_id} segment is a path parameter, and its int annotation means FastAPI converts the string and returns a 422 automatically if it cannot. Anything not in the path is treated as a query parameter, and a default of None makes it optional. There is no manual parsing and no request.args.

Group related operations with an APIRouter, which can carry a prefix and tags for the docs.

from fastapi import APIRouter

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


@router.get("")
async def list_items():
    return []


@router.post("", status_code=201)
async def create_item():
    return {}

Then include the router in the app: app.include_router(router). That is how a FastAPI project stays modular.

Pydantic models

A Pydantic model is a class that inherits from BaseModel. It parses input, coerces compatible types, applies constraints and raises a clear validation error when something is wrong.

# app/schemas.py
from datetime import datetime

from pydantic import BaseModel, Field


class ItemBase(BaseModel):
    name: str = Field(min_length=1, max_length=120)
    price: float = Field(gt=0)
    tags: list[str] = []


class ItemCreate(ItemBase):
    pass


class ItemOut(ItemBase):
    id: int
    created_at: datetime

    model_config = {"from_attributes": True}

Field adds constraints and metadata. model_config = {"from_attributes": True} lets Pydantic read attributes from an ORM object, so you can return a database row directly and have it serialised. Separating ItemCreate from ItemOut is deliberate: the input model has no id, and the output model can expose fields the client may not set.

Use a Pydantic model as the body type and FastAPI parses, validates and hands you a typed object.

@app.post("/items", response_model=ItemOut, status_code=201)
async def create_item(item: ItemCreate):
    return await repo.create(item)

If the body is invalid, FastAPI returns a 422 with a list of the exact fields that failed. You did not write a single validation line.

Request bodies and validation

FastAPI distinguishes the sources of data by position and type:

  • Path parameters come from the URL.
  • Query parameters are everything else in the signature.
  • A Pydantic model is the request body.
  • Header, Cookie, Form and File mark those special inputs.
from fastapi import Header, Query


@app.get("/search")
async def search(
    q: str = Query(min_length=1, max_length=50),
    limit: int = Query(default=20, ge=1, le=100),
    user_agent: str | None = Header(default=None),
):
    return {"q": q, "limit": limit, "user_agent": user_agent}

Constraints on query parameters are enforced the same way as on models, and the generated docs show the limits. This consistency is the reason FastAPI code reads so compactly.

Automatic documentation

Because the schema is generated from the code, documentation is never out of date. Start the app and open /docs for Swagger UI, where you can call endpoints directly, or /redoc for a cleaner reference.

app = FastAPI(
    title="Inventory API",
    version="1.0.0",
    description="Items, stock levels and orders.",
)

You can add summaries, descriptions and examples to path operations and fields, and they appear in the docs. For frontend teams, the same schema can generate typed clients, so the API and its consumers stay in sync. This is the payoff of treating types as the contract.

Dependencies

A dependency is a function that FastAPI calls before your endpoint and whose return value is injected. Depends() is the mechanism, and it composes.

from fastapi import Depends, HTTPException


async def get_session() -> AsyncSession:
    async with SessionLocal() as session:
        yield session


async def get_current_user(
    token: str = Depends(get_token),
    session: AsyncSession = Depends(get_session),
) -> User:
    user = await authenticate(session, token)
    if user is None:
        raise HTTPException(status_code=401, detail="Not authenticated")
    return user
@app.get("/me", response_model=UserOut)
async def read_me(user: User = Depends(get_current_user)):
    return user

Dependencies can be sync or async, can use yield to run cleanup after the response, and are cached within a request, so get_session is called once even if several dependencies need it. They are the idiomatic home for database sessions, authentication, pagination and feature flags.

Async versus sync

FastAPI supports both def and async def endpoints, and the choice matters.

  • An async def endpoint runs on the event loop. Every I/O call inside it must be awaited from an async library, or it blocks the loop for every other request.
  • A plain def endpoint is run in a thread pool, so blocking calls are safe but each request holds a thread.
# Async: use an async driver and await it
@app.get("/items")
async def list_items(session: AsyncSession = Depends(get_session)):
    result = await session.execute(select(Item))
    return result.scalars().all()


# Sync: blocking code is fine; FastAPI offloads it
@app.get("/report")
def build_report():
    return generate_report()  # CPU or blocking I/O

The rule is simple: async all the way, or sync all the way. Mixing a blocking call into an async def endpoint is the most common performance bug in FastAPI, and it is invisible until traffic arrives.

Errors and status codes

Raise HTTPException to return an error with a status code and a JSON detail. FastAPI turns it into a response and documents the status code.

from fastapi import HTTPException, status


@app.get("/items/{item_id}", response_model=ItemOut)
async def read_item(item_id: int):
    item = await repo.get(item_id)
    if item is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="Item not found",
        )
    return item

For a consistent error shape across the API, register exception handlers that convert framework and domain errors into one envelope. Keep the detail field stable; clients will parse it.

Background tasks

BackgroundTasks runs work after the response is sent, on the same process. It is the lightweight option for sending an email or writing an audit row.

from fastapi import BackgroundTasks


def send_receipt(email: str) -> None:
    ...


@app.post("/orders", status_code=201)
async def create_order(
    order: OrderCreate,
    tasks: BackgroundTasks,
):
    saved = await repo.create(order)
    tasks.add_task(send_receipt, saved.email)
    return saved

Background tasks are not a job queue. They run in-process, so they are lost if the worker restarts and they compete with request handling. For anything durable, hand off to Celery, RQ or a managed queue instead.

Databases

FastAPI has no database layer, so you bring your own. The common choices are SQLAlchemy with an async engine or SQLModel, which combines SQLAlchemy with Pydantic. The dependency pattern supplies the session and closes it after the request.

# app/db.py
from collections.abc import AsyncIterator

from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

engine = create_async_engine(settings.database_url, pool_pre_ping=True)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)


async def get_session() -> AsyncIterator[AsyncSession]:
    async with SessionLocal() as session:
        yield session
@router.post("", response_model=ItemOut, status_code=201)
async def create_item(
    payload: ItemCreate,
    session: AsyncSession = Depends(get_session),
):
    item = Item(**payload.model_dump())
    session.add(item)
    await session.commit()
    await session.refresh(item)
    return item

Use expire_on_commit=False so objects remain usable after the commit, and keep transactions inside the request. If you prefer a synchronous driver, use plain def endpoints so FastAPI can offload them to the thread pool.

Security

FastAPI ships the building blocks for OAuth2 and JWT without prescribing a full auth system. OAuth2PasswordBearer extracts the token, and python-jose or pyjwt verifies it.

from fastapi.security import OAuth2PasswordBearer

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.get("/users/me", response_model=UserOut)
async def read_me(
    token: str = Depends(oauth2_scheme),
    session: AsyncSession = Depends(get_session),
):
    user = await get_user_from_token(session, token)
    if user is None:
        raise HTTPException(status_code=401, detail="Invalid token")
    return user

Store password hashes with passlib or argon2-cffi, never plaintext. Hash and verify outside the endpoint, keep secrets in settings, and add rate limiting and CORS deliberately. FastAPI gives you the tools; the policy is still yours to define.

Configuration and settings

Keep configuration in one typed object instead of reading environment variables throughout the code. pydantic-settings validates the environment at startup and gives you the same type safety as your models.

# app/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_prefix="APP_")

    database_url: str
    secret_key: str
    access_token_minutes: int = 30


settings = Settings()
from functools import lru_cache

from app.config import Settings


@lru_cache
def get_settings() -> Settings:
    return Settings()
@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
    return {"token_minutes": settings.access_token_minutes}

Because settings are injected like any other dependency, tests can override them with app.dependency_overrides[get_settings]. Missing required variables fail at startup rather than at the first request, which is exactly when you want to find out.

Testing

TestClient wraps the app in a WSGI-like client and makes real requests in-process, including validation and dependency overrides.

# tests/test_items.py
from fastapi.testclient import TestClient

from app.main import app

client = TestClient(app)


def test_create_item():
    response = client.post("/items", json={"name": "Cup", "price": 9.5})
    assert response.status_code == 201
    assert response.json()["name"] == "Cup"


def test_invalid_price():
    response = client.post("/items", json={"name": "Cup", "price": -1})
    assert response.status_code == 422

Use app.dependency_overrides to swap the database session for a test one, and write async tests with httpx.AsyncClient when you need to exercise async endpoints directly. Because dependencies are explicit, replacing them in tests is straightforward.

Best practices

  • Declare request and response models explicitly; never accept or return raw dictionaries.
  • Keep async def endpoints fully async, or use def so FastAPI offloads blocking work.
  • Put shared logic in Depends() dependencies instead of repeating it per route.
  • Split the app with APIRouter and give each router a prefix and tags.
  • Add constraints with Field and Query rather than validating by hand.
  • Raise HTTPException with the right status code and a stable detail.
  • Override dependencies in tests instead of patching internals.
  • Keep secrets in settings objects read from the environment.

Common mistakes

  • Calling blocking code inside an async def endpoint and stalling the event loop.
  • Returning ORM objects without a response_model, leaking internal columns.
  • Forgetting from_attributes = True and wondering why serialisation fails.
  • Writing one giant main.py instead of routers and modules.
  • Treating BackgroundTasks as a durable job queue.
  • Sharing a single session across requests or across async tasks.
  • Validating the same input twice because the Pydantic model was ignored.
  • Catching every exception and returning 200 with an error body.

Where to go next

FastAPI turns type hints into a validated, documented API, which is why it has become the default for new Python services. To compare the trade-off, read the Flask guide for the minimal synchronous approach and Django for a full framework with an ORM and admin. Then go deeper on the API contract with OpenAPI and the design rules in REST APIs, and make sure your endpoints are worth documenting by getting the resource model right.

In practice

One endpoint, four layers

The model declares the shape, the path operation exposes it, dependencies supply context, and the database call does the work.

app/main.py
from fastapi import FastAPI, HTTPException

from app.schemas import ItemOut

app = FastAPI(title="Inventory API")


@app.get("/items/{item_id}", response_model=ItemOut)
async def read_item(item_id: int):
    item = await db.items.get(item_id)
    if item is None:
        raise HTTPException(status_code=404, detail="Item not found")
    return item

Async and blocking I/O

An async endpoint runs on the event loop, so a blocking call stalls every other request. Use an async driver with async def, or a plain def endpoint that FastAPI runs in a thread pool.

Prefer
@app.get("/items")
async def list_items(session: AsyncSession = Depends(get_session)):
    result = await session.execute(select(Item))
    return result.scalars().all()
Avoid
@app.get("/items")
async def list_items():
    # blocks the event loop for the whole query
    return session.query(Item).all()

Shaping the response

A response_model filters the output to the declared fields and documents it, so internal columns never leak to clients.

Prefer
@app.get("/users/{user_id}", response_model=UserOut)
async def get_user(user_id: int):
    return await repo.get(user_id)
Avoid
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    # returns password_hash and every other column
    return await repo.get(user_id)

Trade-offs

Is FastAPI the right default?

FastAPI optimises for typed APIs and async throughput. That is excellent for services and awkward when the app is mostly server-rendered pages.

Strengths

  • Less code, fewer bugs

    Validation, serialisation and documentation all come from the same type hints, so there is no second schema to keep in sync.

  • Documentation is automatic

    A live OpenAPI schema and interactive docs make the API explorable for frontend and client teams from day one.

  • Built for modern Python

    Async endpoints, dependency injection and Pydantic v2 make it fast and pleasant without a heavy framework.

Trade-offs

  • Async has sharp edges

    One blocking call inside an async endpoint can stall the whole worker. You have to know which libraries are truly async.

  • It is an API framework

    FastAPI does not ship templates, an admin or an ORM. Server-rendered apps need another layer or a different framework.

  • The ecosystem is younger

    Pydantic and SQLAlchemy versions move quickly, and patterns change. Expect to keep dependencies current.

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning FastAPI?

Our interactive tutorial walks you through FastAPI step by step — with quizzes and real code you can run in the browser.