FastAPI does not impose a folder layout. A single file is a fine place to start, but as endpoints pile up you will want to split them by feature. Based on the "Bigger Applications" example from the official docs, this chapter covers a common layout, routers, configuration files and conventions.
A recommended layout
hello-fastapi/
├── app/
│ ├── __init__.py
│ ├── main.py # creates FastAPI(), registers routers
│ ├── dependencies.py # shared dependencies
│ ├── config.py # settings (pydantic-settings)
│ ├── models.py # Pydantic and SQLModel models
│ └── routers/
│ ├── __init__.py
│ ├── items.py
│ └── users.py
├── tests/
│ └── test_items.py
├── .env # local environment variables (not committed)
├── pyproject.toml # or requirements.txt
└── Dockerfile
The app folder is a Python package. Every folder gets an __init__.py so imports like from app.routers import items work. Run the dev server with fastapi dev app/main.py; the fastapi command works out the package structure from the file path and finds the app object for you.
Splitting routes with APIRouter
An APIRouter behaves like a small FastAPI app. Create one per feature and give it a shared path prefix and documentation tags in one place.
# app/routers/items.py
from fastapi import APIRouter, HTTPException
router = APIRouter(prefix="/items", tags=["items"])
fake_items = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}
@router.get("/")
def list_items():
return fake_items
@router.get("/{item_id}")
def read_item(item_id: str):
if item_id not in fake_items:
raise HTTPException(status_code=404, detail="Item not found")
return fake_items[item_id]
main.py then only creates the app and registers routers. The entry point stays short, and each feature's code lives in its own file.
# app/main.py
from fastapi import FastAPI
from app.routers import items, users
app = FastAPI(title="Hello FastAPI", version="0.1.0")
app.include_router(items.router)
app.include_router(users.router)
@app.get("/health")
def health():
return {"status": "ok"}
include_router also accepts prefix, tags and dependencies, so you can mount the same router under /api/v1, or require authentication for an entire admin router.
A dependencies module
Dependencies shared by several routers belong in dependencies.py. Here is a simple check of a token in a request header:
# app/dependencies.py
from typing import Annotated
from fastapi import Header, HTTPException
async def verify_token(x_token: Annotated[str, Header()]):
if x_token != "secret-token":
raise HTTPException(status_code=400, detail="X-Token header invalid")
Pass it when creating a router, as in APIRouter(dependencies=[Depends(verify_token)]), and it applies to every route on that router. The parameter name x_token is automatically mapped to the x-token header.
Dependencies and configuration files
Project dependencies usually go in pyproject.toml, which tools such as uv, Poetry and Hatch read to manage the virtual environment. If you only use pip, a requirements.txt is enough.
[project]
name = "hello-fastapi"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = [
"fastapi[standard]",
"sqlmodel",
"pydantic-settings",
]
[dependency-groups]
dev = ["pytest", "ruff"]
Keep values such as database URLs and secret keys out of the code and pass them in through environment variables or a .env file. Add .env to .gitignore and commit a .env.example listing just the variable names. Chapter 4 shows how to read these settings.
Startup and shutdown
Resources that should be prepared once when the app starts and cleaned up when it stops, such as a connection pool or a machine learning model, are handled with lifespan:
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
print("starting up") # open connections, load models
yield
print("shutting down") # release resources
app = FastAPI(lifespan=lifespan)
Code before yield runs before the first request is served; code after it runs when the server stops.
Conventions
Summary
- Start with a single
main.py, then move to an app package with a routers folder as it grows. - Group routes per feature with
APIRouter and register them with app.include_router. - Put shared dependencies in
dependencies.py and configuration in environment variables or .env. - Manage startup and shutdown resources with
lifespan.
0 comments
Sign in · Sign in to leave a comment.