FastAPI for REST APIs

FastAPI is a Python web framework for building HTTP APIs on top of Starlette (ASGI) and Pydantic. Type hints on endpoint parameters drive request parsing, validation, serialisation and the generated OpenAPI schema, so one declaration does the work that other frameworks spread across decorators, schemas and documentation.

1. Overview

2. Example Code Structure


from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional

# Define the application
app = FastAPI()

# Define a request model with Pydantic
class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

# Define a simple POST endpoint
@app.post("/items/")
async def create_item(item: Item):
    return {"name": item.name, "price_with_tax": item.price + (item.tax or 0)}

3. Explanation of Example

FastAPI combines simplicity and power, making it a strong choice for modern REST API development.

4. How a request flows

FastAPI Request Lifecycle Client HTTP request Uvicorn ASGI server Starlette routing Depends auth, DB Pydantic validate body Endpoint your function Response JSON Invalid input never reaches the endpoint: Pydantic returns HTTP 422 with field-level errors. The response_model is validated and filtered on the way out, so internal fields are not leaked.

FastAPI is an ASGI application, so it runs under an ASGI server such as Uvicorn or Hypercorn rather than a WSGI server. The server hands the raw request to Starlette, which runs middleware (CORS, GZip, custom timing) and matches the path to a route. FastAPI then resolves the endpoint's dependencies, parses path, query, header and body parameters according to their type hints, and validates the body with Pydantic. Only if everything validates is your function called. Its return value is checked against the declared response_model, converted to JSON-compatible data and sent back.

Endpoints may be declared with async def or plain def. Async endpoints run on the event loop and must not block it; synchronous endpoints are run in a thread pool automatically. Mixing in a blocking database driver inside an async def endpoint is the most common performance mistake. Under load, run several Uvicorn worker processes (or Gunicorn with the Uvicorn worker class) behind a reverse proxy such as Nginx or Apache, so that CPU-bound work in one request does not delay all others and a crashed worker is restarted automatically.

5. Dependency injection

Dependency Injection Graph GET /items/{id} endpoint get_current_user Depends() get_db yield session also needs db Each dependency runs once per request and is cached; yield-dependencies clean up after the response.

Depends() declares that a parameter is produced by another function. Dependencies can depend on other dependencies, forming a graph that FastAPI resolves per request, caching each result so a shared dependency such as a database session is created only once. A dependency written as a generator with yield can run cleanup code after the response is sent, which is the idiomatic way to close sessions and release locks. Because dependencies are ordinary functions, tests can replace them with app.dependency_overrides.

6. Worked example: CRUD with validation and auth

from fastapi import FastAPI, Depends, HTTPException, Header, status
from pydantic import BaseModel, Field

app = FastAPI(title="Inventory API")
DB: dict[int, dict] = {}

class ItemIn(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    price: float = Field(gt=0)

class ItemOut(ItemIn):
    id: int

def require_token(x_api_key: str = Header()):
    if x_api_key != "secret-demo-key":
        raise HTTPException(status.HTTP_401_UNAUTHORIZED, "bad key")
    return x_api_key

@app.post("/items", response_model=ItemOut, status_code=201)
def create_item(item: ItemIn, _: str = Depends(require_token)):
    new_id = len(DB) + 1
    DB[new_id] = item.model_dump()
    return {"id": new_id, **DB[new_id]}

@app.get("/items/{item_id}", response_model=ItemOut)
def read_item(item_id: int):
    if item_id not in DB:
        raise HTTPException(404, "not found")
    return {"id": item_id, **DB[item_id]}
pip install "fastapi[standard]"
uvicorn main:app --reload
curl -X POST localhost:8000/items -H "X-API-Key: secret-demo-key" \
     -H "Content-Type: application/json" -d '{"name":"pen","price":1.5}'
curl localhost:8000/items/1
# interactive docs: http://localhost:8000/docs

Posting {"price": -1} returns 422 with an error for each failing field, without any validation code in the endpoint. The X-API-Key header is mapped from the parameter name x_api_key. The example uses Pydantic v2 (model_dump); on Pydantic v1 the method is dict().

7. Trade-offs and when to use it

FrameworkStrengthsChoose when
FastAPITyped validation, OpenAPI docs, asyncJSON APIs, ML model serving, microservices
FlaskSmall, mature, synchronous, huge ecosystemSimple services, server-rendered pages
Django + DRFORM, admin, auth, batteries includedLarge apps with a relational data model

FastAPI is a good fit when the API contract matters and clients benefit from a generated schema. It provides less out of the box than Django: no ORM, admin or migrations, so you choose SQLAlchemy or another library yourself, and assemble migrations and admin tooling separately.

8. Pitfalls

Compare with Flask and GraphQL, and see Implementing an API for general design guidance.