← Engineering

Organising FastAPI projects for data products

Domain-driven project structure that keeps data APIs maintainable as they grow beyond a few endpoints.

FastAPIArchitectureAPI DesignProduction

Context

FastAPI documentation promotes a flat structure where routes, Pydantic models, and business logic coexist in the same file. This is acceptable for a two-endpoint prototype, but every data product I have built has grown beyond that. When a system reaches 10+ endpoints with multiple data sources, a flat structure becomes a maintenance bottleneck.

Structure by domain, not by layer

The approach I use is organising by business domain rather than technical layer:

src/
  api/
    routes/
      ingestion.py
      analytics.py
      alerts.py
    middleware/
      auth.py
      validation.py
  domain/
    weather/
      models.py
      service.py
      validators.py
    alerts/
      models.py
      service.py
  infrastructure/
    database.py
    cache.py
    config.py

Each domain owns its Pydantic models, business logic, and validation rules. The API layer is deliberately thin: it parses the HTTP request, delegates to the domain service, and formats the response. No business logic lives in route handlers.

Why this works for data products

Data products differ from standard web applications in one important way: the same data model is frequently consumed by multiple interfaces. The weather domain models in this structure are used by the API, the background ingestion job, and the dashboard. When the model changes, the change is made in one file and all consumers pick it up.

Testing also improves. With domain-services separated from HTTP concerns, I can unit-test transformation logic without spinning up a test client. Integration tests verify the API layer separately.

The trade-off

Domain-driven structure introduces more files than a flat layout. A new domain requires creating a directory with at least three files (models, service, validators). For very small projects with 2-3 endpoints, this overhead may not be justified. But in my experience, every data product eventually grows — and the cost of refactoring flat code into domain structure is higher than starting with it.