Handbooks / FastAPI / Chapter 4
APIRouter and Project Structure
7 pages · ~5 min✓ Reviewed
Builds on Async and Databases. Next up: Middleware and Exception Handlers.
APIRouter Mental Model
1. What a Router Is
- APIRouter = a mini FastAPI app that holds path operations
- It uses
@router.get/@router.postlike the app does - It cannot run alone: attach it with
app.include_router(router) - One router per resource (users, items, orders), one module each
main.pyonly wires routers together
- users.pyrouter
- items.pyrouter
- main.pyinclude_router
- FastAPI appserves
2. Constructor Options
router = APIRouter(
prefix='/users', tags=['users'],
dependencies=[Depends(get_current_user)],
responses={404: {'description': 'Not found'}},
)all four options apply to every route in the router
3. Prefix Rules
- Prefix starts with
/ - Prefix never ends with
/ - Route paths are relative to the prefix
| Prefix | Route path | Final URL |
|---|---|---|
/users | /{user_id} | /users/{user_id} |
/items | / | /items/ |
router = APIRouter(prefix='/items', tags=['items']) @router.get('/') async def list_items(): ...
minimal router
A router does nothing until include_router mounts it on the app.
Prefix, Tags, Dependencies and include_router
1. Mounting a Router
app.include_router(router)registers all its routes; call it once per router- Call it in
main.pyor anapi/__init__aggregator - Extra
prefix,tags,dependencies,responsesare added on top of the router's own - So one router mounts under
/v1and/v2without edits
app.include_router(users.router, prefix="/v1") app.include_router(users.router, prefix="/v2", tags=["v2"])
same router, two mounts
2. What Stacks
| Option | Effect |
|---|---|
prefix | Concatenated into the final path |
tags | Docs grouping only; router + include + route tags combine |
dependencies | Run for every route, before the route's own |
Organizing a Larger App
1. Layout and layers
app/main.py app/api/routers/ users.py items.py app/schemas/ Pydantic app/models/ ORM app/services/ logic app/core/ config.py security.py app/db/ session.py base.py tests/
one folder per layer
| Layer | Does | Never |
|---|---|---|
| Router | parse, Depends, call service | logic, raw SQL |
| Service | session + schemas in, models out | HTTP objects |
2. Data flow
- BodyUserCreate
- RouterDepends
- Servicelogic
- ModelORM to DB
- UserReadJSON
- Schemas = API contract; models = DB tables
- Services are plain functions: testable without HTTP, reusable from CLI or jobs
from_attributes=TrueletsUserReadread ORM objects
Settings Module with pydantic-settings
1. Defining Settings
pip install pydantic-settings: a separate package in Pydantic v2class Settings(BaseSettings)with typed fields- Types are validated and coerced:
"true"becomesTrue - Env vars override
.env, which overrides field defaults; names are case-insensitive
from pydantic import SecretStr from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file='.env') database_url: str debug: bool = False secret_key: SecretStr
no default on required fields and secrets
2. One Instance, Injected
- Env varshighest
- .env filenext
- Defaultslowest
- Settings()validated
| Style | How | Use when |
|---|---|---|
| Module global | settings = Settings() in app/core/config.py |
Avoiding Circular Imports and Common Mistakes
1. Why cycles happen
- One module sees a partially initialized module
- Raises ImportError or AttributeError
2. One-way imports
- 1routers
- 2services
- 3models / schemas
- 4core / db
- Lower layers never import higher ones; routers never import each other
get_db,get_current_userlive in app/api/deps.py;settingsinapp/core
| Unavoidable cycle | Technique |
|---|---|
| Runtime use | Import inside the function |
| Type hints only |
Cheat Sheet: Structuring a FastAPI App
1. Router and Mount
router = APIRouter(prefix='/users', tags=['users'], dependencies=[Depends(auth)]) app.include_router(users.router, prefix='/api/v1')
final path: /api/v1/users/...
- Extra prefix, tags and dependencies in
include_routerstack on top - Prefixes concatenate; tags combine; router dependencies run first
2. Layers and Imports
- RouterHTTP
- Servicelogic
- ModelDB
- Schema = the contract at the boundaries
- Imports flow one way: routers → services → models → core
- Shared deps live in
deps.py TYPE_CHECKINGimports break type-only cycles
3. Settings
class Settings(BaseSettings): model_config = SettingsConfigDict(env_file='.env') settings = Settings() # or @lru_cache get_settings
Check yourself
app.include_router(router) runs first, then @router.get('/ping') is declared below it. What does GET /ping return?
- 404:
include_routercopies routes at call time. - Define every route before including.
child = APIRouter(prefix='/items') has @child.get('/{id}'). Then parent.include_router(child, prefix='/admin') and app.include_router(parent, prefix='/api/v1'). Final path?
/api/v1/admin/items/{id}- Prefixes concatenate, outermost first.
.env has DEBUG=false, the shell exports DEBUG=true, and the field is debug: bool = False. What is settings.debug?
- True: env vars beat
.env, which beats defaults.