FastAPI Architecture: Clean, Scalable & Self-Documenting

Conventional wisdom for Python APIs often leads us to massive main.py files or overly complex Django structures. Like many of you, I’ve struggled with finding the “Goldilocks” zone a setup that is light enough to get code out the door but structured enough not to break when the team grows.

In this post, I’ll show you my preferred setup for a FastAPI microservice. We’ll focus on keeping logic separated, handling errors gracefully, and my favourite part letting Python’s type hints do 90% of the documentation work for us.

The Philosophy: Type Safety is Documentation
In Express, we often use JSDoc to explain our code. In Python, we have Type Hints. By using Pydantic models, FastAPI doesn’t just check your data; it automatically generates a professional Swagger UI without you writing a single line of extra documentation.

1) The Project Structure
For a scalable micro service, I move away from the “all-in-one” file. Here is the blueprint:

.
├── app/
│   ├── main.py          # Entry point
│   ├── routes/          # API Endpoints (equivalent to Express Routes)
│   ├── schemas/         # Pydantic Models (Data validation)
│   └── services/        # Business logic (equivalent to Controllers)
├── requirements.txt
└── .env                 # Environment variables

2) Defining Data (The Schemas)
Instead of manual validation, we define what our data looks like. This replaces the need for complex comments because the code is the spec.

# app/schemas/user.py
from pydantic import BaseModel, EmailStr

class UserBase(BaseModel):
    username: str
    email: EmailStr

class UserCreate(UserBase):
    password: str

class UserResponse(UserBase):
    id: int

    class Config:
        from_attributes = True

3) The “Base Router” Logic
In Express, we often use classes to group logic. In FastAPI, we use APIRouter. It allows us to modularisation the app easily.

# app/routes/user_routes.py
from fastapi import APIRouter, HTTPException
from app.schemas.user import UserResponse, UserCreate

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

@router.get("/", response_model=list[UserResponse])
async def get_users():
    # This acts as your controller logic
    return [{"id": 1, "username": "dev_user", "email": "user@example.com"}]

@router.post("/", response_model=UserResponse)
async def create_user(user: UserCreate):
    # Business logic goes here
    return {**user.model_dump(), "id": 2}

4) Middleware: The Silent Hero
Just like Express middle ware, FastAPI lets us intercept requests. Here’s a simple “Process Time” middle ware to log how fast your API is:

# app/main.py
import time
from fastapi import FastAPI, Request

app = FastAPI(title="My Scalable API")

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    response = await call_next(request)
    process_time = time.time() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response

# Include our routes
from app.routes import user_routes
app.include_router(user_routes.router)

5) Automatic Docs (No More Out-of-Sync Swagger)
One of the biggest pain points in Express is keeping swagger.json updated. With this Python setup, you simply run your server

uvicorn app.main:app --reload

Navigate to /docs, and you’ll find a fully interactive, beautifully themed Swagger UI. Because we used Pydantic models and type hints, the documentation cannot go out of sync. If you change a field in your code, it changes in the docs automatically.

6) Keeping it Clean: Linting & Formatting
To keep the code quality high (and catch errors early), I always include these two in my Python workflow:

  • Ruff: An incredibly fast Python linter and formatter (replaces Flake8 and Black).

  • MyPy: For static type checking to ensure your type hints are actually correct.

This setup isn’t “the only way,” but it provides a foundation that handles 99% of micro service needs. It’s simple enough for a prototype but strict enough for production.

Similar Posts