FastAPI design philosophy
FastAPI design philosophy
FastAPI moved into the Python web framework mainstream in a relatively short time. Behind that quick rise is a design that ties type hints, Pydantic, and async into a single coherent sentence.
1. About FastAPI
FastAPI is a Python web framework created by Sebastián Ramírez (tiangolo), reportedly first released in December 2018. It stands on top of two libraries.
| Foundation | Role | Released |
|---|---|---|
| Starlette | ASGI routing, middleware, test client | encode team, 2018 |
| Pydantic | Type-driven data validation and serialization | Samuel Colvin, 2017 |
FastAPI adds routing, DI, security, and automatic OpenAPI generation, creating the experience of "type hints alone bring along validation and documentation."
2. Type hints as the schema
from fastapi import FastAPI
from pydantic import BaseModel
class Item(BaseModel):
name: str
price: float
app = FastAPI()
@app.post('/items')
def create(item: Item) -> Item:
return item
At runtime FastAPI sees item: Item and validates the request body with Pydantic. The same information is exposed as an OpenAPI schema, and we can try it directly from /docs (Swagger UI) and /redoc.
3. Pydantic v1 → v2
Pydantic v2 (2023-06) rewrote the core validation in Rust (pydantic-core) and announced a large performance boost over v1. The v1 and v2 APIs differ in places, and a separate migration guide is provided.
class Config became model_config in v2, and several decorators and methods changed. Verify version alignment between libraries first.
4. Dependency injection
FastAPI's DI is essentially "write Depends(...) in the function signature."
from fastapi import Depends
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get('/users/{id}')
def read(id: int, db = Depends(get_db)):
return db.query(User).get(id)
A dependency function can declare its own dependencies via type hints, so they compose. Wrapping setup/teardown in a single function via yield is similar to a context manager.
Within the same request, the same dependency is evaluated only once (cached). Dependencies that need side effects may behave differently from intent.
5. async first
When endpoints are declared with async def, they run concurrently on the ASGI event loop. Synchronous functions (def) are also accepted — FastAPI runs them on a separate thread pool so the event loop is not blocked. If the DB driver or HTTP client does not support async, leaving the route as a synchronous function is the safer choice.
6. Comparison with other frameworks
| Framework | First release | Character |
|---|---|---|
| Django | 2005 | "Batteries included." Integrated ORM, admin, auth. Synchronous-centric, ASGI also supported. |
| Flask | 2010, Armin Ronacher | Microframework. Grown through extensions. |
| FastAPI | 2018 | Type hints + auto docs. ASGI first. |
| Starlette | 2018 | ASGI foundation. FastAPI sits on top. |
| Litestar | 2022 (formerly Starlite) | Another ASGI framework built on type hints. |
| Sanic | 2016 | One of the earlier async-first attempts. |
Django's strengths are full-stack and admin tooling. Flask is small and freely extensible. FastAPI is the marriage of types and OpenAPI. Rather than one being superior, the choice depends on the work.
7. Splitting routers
# routers/users.py
from fastapi import APIRouter
router = APIRouter(prefix='/users', tags=['users'])
@router.get('/')
def list_users(): ...
# main.py
app.include_router(users.router)
As the size grows, splitting routers per domain comes naturally.
8. Response model, status code, background
@app.post('/items', response_model=ItemOut, status_code=201)
def create(item: ItemIn) -> ItemOut: ...
Specifying response_model separates the response serialization schema from the input and acts as a guard against leaking secret fields.
BackgroundTasks is a tool for light tasks that should run after the response. Long or stability-critical tasks belong in a separate queue (Celery · RQ · Arq).
9. Lifecycle events
Resource initialization that runs once at app start and end is expressed as a lifespan context.
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.db = await create_pool()
yield
await app.state.db.close()
app = FastAPI(lifespan=lifespan)
The earlier @app.on_event("startup") · @app.on_event("shutdown") decorators have been deprecated in favor of lifespan.
10. OpenAPI and /docs
/openapi.json is the spec itself. /docs serves Swagger UI, /redoc serves Redoc. To avoid exposing the spec externally in production, use FastAPI(docs_url=None, redoc_url=None, openapi_url=None) or block at the gateway.
11. Common pitfalls
Blocking calls inside async def — requests.get(...) or synchronous ORM calls. They block the event loop. Use async replacements like httpx.AsyncClient, asyncpg, or SQLAlchemy 2.x async, or leave the route as a synchronous function and let it run on a thread pool.
Limits of automatic OpenAPI generation — when the response shape is dynamic or supports multiple content types, manual supplementation is needed.
Closing thoughts
FastAPI's biggest differentiator is the experience of validation, docs, and DI naturally following from a single line of type hints. It is the place where Python type hints live as runtime tooling rather than as mere comments. Even small APIs can start without much cost.
Next
- python-folder-philosophy
See FastAPI · FastAPI GitHub · Starlette · Pydantic · ASGI spec · Flask · Django REST framework.