API Service Template
Small FastAPI services are easy to start, but route functions can accumulate validation, transactions, logging, ORM access, and external API calls surprisingly quickly. This chapter proposes a service skeleton that is not "perfect architecture," but one that tends to age with less regret.
Quick takeaway: the goal is not to maximize layers. The goal is to keep boundaries explicit. Let routes behave like transport adapters, services orchestrate use cases, repositories own persistence details, schemas define contracts, and settings/logging/lifespan define infrastructure boundaries.
One-Page Structural View
Suggested Folder Layout
app/
api/
routes/
users.py
health.py
schemas/
user.py
services/
user_service.py
repositories/
user_repository.py
db/
models.py
session.py
domain/
errors.py
core/
config.py
logging.py
main.py
tests/
integration/
unit/What Each Layer Should Know
| Layer | Responsibility | Should not know |
|---|---|---|
| route | HTTP input/output, auth, status codes, response model | ORM details, commit policy |
| schema | request/response contract | DB sessions, external API calls |
| service | business action orchestration, transaction boundary | framework-specific HTTP details |
| repository | query shape, persistence details | HTTP status and serialization contract |
| core/infrastructure | settings, logging, lifespan, clients | business branching |
Let the App Entrypoint Handle Wiring
from fastapi import FastAPI
from app.api.routes import health, users
from app.core.config import Settings, get_settings
from app.db.session import lifespan
def create_app() -> FastAPI:
settings: Settings = get_settings()
app = FastAPI(
title=settings.app_name,
lifespan=lifespan,
)
app.include_router(health.router)
app.include_router(users.router)
return app
app = create_app()`main.py` should be a wiring entrypoint, not a business layer. It usually owns settings, router registration, and lifespan configuration, while domain rules stay elsewhere.
Settings and Logging Are Boundaries Too
- read settings once and inject them, rather than scattering environment lookups
- define logging strategy around request IDs, domain events, and error boundaries early
- pass settings through factories or dependencies so tests can replace them cleanly
For deeper guidance on settings.py and pydantic-settings, see Settings and Pydantic Settings. For hosting tradeoffs, see Lambda vs Kubernetes.
Split the Test Layout Early
unit tests
- pure business rules
- service branching
- typing boundaries and serialization behavior
integration tests
- HTTP contracts
- dependency overrides
- DB session lifecycle
- real serialization output
Rules for a Service That Ages Well
- Keep routes thin.
- Let services or use cases own commits.
- Treat ORM entities as internal details and finish with DTOs.
- Separate background work from request paths.
- Add observability and timeouts early.
Practical Checklist
Entrypoint is for wiring
The app factory should compose routers and infrastructure, not implement domain behavior directly.
Service owns transaction completion
Repositories should not commit autonomously if you want multi-step business actions to remain atomic.
Tests follow the same boundaries
Lifespan, dependency overrides, and rollback strategies should mirror the service's real structure.
Settings and logging are designed
Early structure around configuration and observability reduces later operational pain.
Good Supporting Examples
examples/fastapi_service_template_example.pyexamples/sqlalchemy_loading_strategies.pyexamples/sqlalchemy_class_based_uow.pytests/test_fastapi_fixtures_and_teardown.py