Skip to content
AB

How I Structure a Large FastAPI Backend with Vertical Slices

Organize FastAPI around business capabilities so routes, services, schemas, persistence, and rules evolve together.

Ashish BhakhandPublished

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:

Code
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:

Code
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.

Code
# 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.

← Back to all writing