codingstairs
NotesEDULifeContact
⌕Search⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

Get in touch

Send without signing in. Add your email if you'd like a reply.

  • Leave a message anonymously →
  • ✉ warragon112@gmail.com
  • KakaoTalk Open Chat ↗

© 2026 codingstairs

  • Notes
  • EDU
  • Search
  • Life
  • Contact
  • Legal
  • RSS
  • GitHub
Notes›backend

FastAPI design philosophy

Published 2026-04-28· Updated 2026-05-18·0 views

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. In practice — validation, errors, middleware

When the design philosophy comes down to code, four concerns repeat on every request: validation, errors, rate limiting, and CORS.

Delegate validation to Pydantic

Constraints that types alone cannot express belong inside the model via Field and @field_validator. Checking if not name: ... inside the handler scatters the rules.

from pydantic import BaseModel, Field, field_validator

class ArticleIn(BaseModel):
    title: str = Field(..., max_length=120, description='Article title')
    slug: str = Field(..., pattern=r'^[a-z0-9-]+$', description='URL slug')

    @field_validator('title')
    @classmethod
    def not_blank(cls, v: str) -> str:
        if not v.strip():
            raise ValueError('Title cannot be blank')
        return v

On validation failure FastAPI automatically returns a 422 stating which field is wrong and why.

Gather models in schemas.py

Leaving request/response models as inline dicts inside handlers, or redefining BaseModel per router, splits the same schema. Gather them in one per-domain schemas.py and have routers import it (folder placement is covered in the next note).

Normalize errors with HTTPException

A plain return {"error": "..."} response still has status code 200 and an ad-hoc schema. Raise failures as exceptions.

from fastapi import HTTPException

if article is None:
    raise HTTPException(status_code=404, detail='Article not found')

To respond uniformly even to unexpected exceptions, add a global handler. Keep the stack trace in the logs only — never expose it in the body.

from fastapi import Request
from fastapi.responses import JSONResponse

@app.exception_handler(Exception)
async def on_unhandled(request: Request, exc: Exception):
    logger.exception('Unhandled exception')
    return JSONResponse(status_code=500, content={'detail': 'Server error'})

Rate limiting and CORS are the middleware layer

A rate limit that curbs endpoint abuse is better declared with a tool's decorator, such as slowapi, than hand-written counters per handler.

from slowapi import Limiter
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)

@router.get('/search')
@limiter.limit('10/minute')
async def search(request: Request): ...

An API called from a browser on another origin declares its allowed origins via CORSMiddleware. allow_origins=["*"] cannot be combined with credentialed requests and is far too broad — list the domains.

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=['https://app.example.com'],
    allow_methods=['*'],
    allow_headers=['*'],
)

12. 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.

More in backend

All in this category →
  • Wrap public OpenAPIs with your own BFF
  • Email Delivery and OTP — SMTP
  • Audit Log — logAdminAction pattern
  • WebSocket and SSE — real-time communication
  • REST API introduction
  • OpenAPI Specification