Python for JS Devs · FastAPI
One main.py doesn't scale. Here's the router + settings layout real teams use — and how config beats process.env.
APIRouter — Express's RouterGroup related endpoints in their own module with an APIRouter, then mount them on the app. Same idea as express.Router().
# app/routers/users.py
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/{uid}")
def get_user(uid: int): ...
# app/main.py
from fastapi import FastAPI
from app.routers import users
app = FastAPI()
app.include_router(users.router) # mount the group
app/
├── __init__.py
├── main.py # create app, include routers
├── core/
│ └── config.py # Settings (env)
├── routers/ # HTTP layer (thin)
│ ├── users.py
│ └── auth.py
├── schemas/ # Pydantic in/out models
│ └── user.py
├── services/ # business logic (HTTP-agnostic)
│ └── user_service.py
└── db/ # SQLAlchemy models + session
└── models.py
process.envNo scattered process.env.FOO with undefined surprises. pydantic-settings loads env vars into a validated, typed object and fails fast at startup if something's missing.
const url = process.env.DATABASE_URL;
// string | undefined, no validation
const port = +process.env.PORT;
// NaN if unset 🙃from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
port: int = 8000
model_config = {"env_file": ".env"}
settings = Settings() # validates at import; missing → errorAdd it with uv add pydantic-settings. Now settings.database_url is typed and guaranteed present. Inject settings anywhere (often via a Depends).
Refactor a two-route app: move the routes into app/routers/users.py behind an APIRouter(prefix="/users"), mount it in main.py with include_router, and add a Settings class reading APP_NAME from a .env. Run it and confirm /docs shows the grouped, tagged routes. A structured, config-driven app is the win.
APIRouter per module and mount it via app.include_router(...).lru_cache go for settings, and do I need it? (Your old code used it — ask what problem it solves.)