Designing Thin FastAPI Routes
HTTP routes handle requests and responses while screen composition moves into application use cases
The Hangangjari backend is based on FastAPI. But the FastAPI process does not do everything.
The API returns screen-ready responses that the app and widgets can read immediately. External data collection, forecast calculation, push candidate selection, and delivery are handled by workers.
The reason for this separation is simple. When users open the app, they need a fast response. Public-data collection, however, can be slow, can fail, and can update on different schedules by source.
The tempting way to build an API is to read and compose everything inside route functions. In a small service, that feels faster. Hangangjari also seemed fine that way early on. But as park first screens, widgets, forecasts, and notification settings were added, routes began needing to know external sources, Redis, DB schema, and DTO shapes at the same time. Then even small changes spread in several directions.
So FastAPI was left with HTTP handling, and the user’s questions moved into application queries and use cases. In effect, “build this park’s first screen” was separated from “select these rows from this table.”
Routes only assemble HTTP boundaries
The backend is split into something close to clean architecture.
domain: entities and policies.application: queries, commands, use cases, and ports.infrastructure: SQLAlchemy, Redis, APNs, and external source adapters.interfaces: HTTP routes and job entrypoints.
flowchart TB Route["FastAPI route"] --> Dependency["Dependency layer"] Dependency --> UseCase["Application query / command"] UseCase --> Port["Repository / cache port"] Port --> Infra["SQLAlchemy / Redis / APNs / source client"] Infra --> Store["Postgres / Redis / external service"] UseCase --> DTO["Pydantic DTO serialization"] DTO --> Client["iOS app / widget"]
FastAPI routes stay thin. Once a route starts knowing SQL queries or source adapters directly, screen needs and infrastructure details become mixed.
This does not mean the project is an ideal clean architecture example. In a personal project, splitting files and roles too finely can make reading harder. In Hangangjari, I mostly tried to keep these boundaries.
Routes handle HTTP parameters and response shape. Queries and use cases handle what the screen asks. Repository and cache adapters handle storage details. Keeping even this much separation made testing and incident analysis much easier.
API groups called by the app
The v2 app API area read by the app includes:
- Parks/Parking: parks, parking lots, parking overview, and lot status.
- Home Summary: screen-ready first-screen response that bundles parking, outing, forecasts, and refresh state.
- Outing: per-park general overview and signal detail.
- Forecast: parking forecast, park congestion forecast, and horizon timeline.
- Telemetry: client performance events and product event batches.
- Push: APNs subscription registration and removal.
- App access bootstrap: app access validation and request proof issuance.
Admin routes are separated from user-facing APIs. Admin details do not leak into screen-ready responses.
Data read for a parking screen
The parking overview uses Redis and Postgres together. Master data is read from Postgres, and latest status checks Redis first.
sequenceDiagram
autonumber
participant Client as iOS app/widget
participant Route as FastAPI route
participant Query as Parking query
participant Cache as Status cache
participant Repo as Parking repository
participant DB as Postgres
Client->>Route: Request parking screen
Route->>Query: Build parking screen values
Query->>Repo: Read park and parking-lot reference data
Repo->>DB: Query reference data
Query->>Cache: Check latest parking status
alt Cache miss or malformed entry
Query->>Repo: Check last stored status
Repo->>DB: Query status history
end
Query-->>Route: Return screen-ready read model
Route-->>Client: Return response with freshness
The first screen is handled by home-summary, an endpoint that bundles several data types into one payload.
sequenceDiagram
autonumber
participant Client as App/widget
participant Route as home-summary route
participant Cache as Hot/stale cache
participant Parking as Parking query
participant Outing as Outing query
participant Forecast as Forecast query
participant DB as Postgres
Client->>Route: Request first screen
Route->>Cache: Check ready first-screen cache
alt Hot cache hit
Cache-->>Route: Cached first-screen response
else Cache miss
Route->>Parking: Build parking summary
Route->>Outing: Gather outing signals and source status
Route->>Forecast: Check forecast summary
Parking->>DB: Read parking reference data
Outing->>DB: Read outing reference data
Forecast->>Cache: Read forecast response
Route->>Cache: Store hot response and last-success value
end
Route-->>Client: Return first-screen response
This API simplifies the app first screen and general park widget. The client does not need to compose several APIs in sequence.
The benefit of home-summary was not only performance. It made the app and widget see the same values.
If the client combines several APIs, parking may be fresh while event information is old, or the widget may receive only part of the values and change the meaning of the screen. The server response aligns those differences at once.
Responses carry values and refresh state together
iOS apps are sensitive to API shape. Swift decoding feels server response changes immediately. So the backend does not expose internal DB schema directly; it uses DTOs.
DTOs handle these jobs.
- Make optional fields and default policy explicit.
- Keep datetime serialization consistent.
- Distinguish
fresh,stale, andunavailable. - Include source refresh state and generated/observed/fetched timestamps.
- Hide raw payloads and internal policy values.
- Make contracts harder to break while mixed app versions are in use.
Read requests also check app access
Current v2 app routes pass app access validation. The concrete platform validation implementation and request proof issuance are separated into the bootstrap side.
The choices ahead of platform-specific details were:
- Read APIs can also be abused.
- Write APIs validate request proof more strictly.
- Authentication failure metrics are kept, but user-identifying information is not written directly to logs.
- Operational metric endpoints stay invisible to normal user traffic.
What changed after keeping routes thin
Keeping FastAPI routes thin separated screen needs from storage details. App APIs were designed around user-facing screen units, not DB schema, and work that belongs to workers was not pulled into user requests.
In a public-data app, one value is not enough. Source, time, and refresh state have to travel with it. As the first screen became more complex, letting the server bundle screen-ready responses also helped client stability.
Once routes became thin, reasons for change became visible. Instead of routes knowing everything, use cases own the screen’s question. Then screen needs and storage details can be handled separately.
Share
No comments yet. You can leave the first one.
Pending review