Small FastAPI examples often start with global folders named routers, models, services, and repositories. As the application grows, understanding one feature means jumping between every folder.
I prefer vertical slices:
app/
workouts/
router.py
schemas.py
service.py
repository.py
domain.py
subscriptions/
router.py
schemas.py
service.py
repository.py
core/
db/
Everything needed to understand workouts lives inside workouts. Cross-cutting infrastructure such as logging, errors, and configuration lives in core because it genuinely applies to every feature.
Give each file one clear role#
The dependency direction is simple:
router → service → repository
↓
domain
- Router: HTTP input, dependencies, and response.
- Service: one application use case and its orchestration.
- Repository: SQL and persistence mapping.
- Schemas: Pydantic wire contracts.
- Domain: optional pure business rules without I/O.
The domain.py file is optional. A CRUD-style feature may not need it. Creating empty layers everywhere produces ceremony rather than architecture.
Keep cross-feature calls public#
Suppose recommendations need a user's recent workout load. The recommendation service should call a public workout service function, not import a workout repository query.
# workouts/__init__.py
from app.workouts.service import get_recent_training_load
__all__ = ['get_recent_training_load']
This makes the owned capability explicit and lets workouts change its storage without breaking consumers.
Avoid the shared-folder trap#
A utils directory often becomes a place for code with unclear ownership. Before moving something into shared code, ask whether it represents stable technical policy or feature behavior.
Request IDs belong in core. A function that decides workout intensity belongs to workouts, even if recommendations also uses it.
Let complexity earn structure#
A simple feature can start with router, schemas, service, and repository. Extract pure rules when they become meaningful. Add internal dataclasses when wire types stop fitting domain work. Add a background-job module when the feature actually has scheduled processing.
Vertical slices reduce the distance between related decisions. They also make deletion easier: removing a feature has a visible boundary. FastAPI provides routers and dependencies; the application still needs an architecture that tells developers where business behavior belongs.