Conventions¶
Code style¶
- Choose simplicity. The shortest readable implementation. Three similar lines beat a premature abstraction.
- Small, focused functions and files. A function past one clear responsibility gets split.
- Obvious names over clever ones.
- No defensive code for impossible states. Validate at the boundary — request bodies, external APIs. Trust internal callers.
- Default to no comments. Comment when the why is non-obvious: a hidden constraint, a surprising invariant, a workaround. Never narrate what the code does.
That last rule is the one worth internalising. Most comments in this codebase explain a decision that
looks wrong until you know something — why consent records the granted set rather than the requested
one, why sid is a hash. Those earn their space. # increment the counter does not.
Feature packages¶
A resource is a directory with four files, each with one job:
admin/users/
├── routes.py FastAPI handlers. Translate domain errors → HTTP. No database calls.
├── schemas.py Pydantic DTOs. Validation lives here.
├── service.py Database logic. FastAPI-free. Raises domain errors.
└── errors.py Domain exceptions.
The split is not ceremony. service.py knowing nothing about HTTP is what lets the same code back
/entity/profile and /admin/users/{id}/profile with opposite permission rules. routes.py making
no database calls is what keeps the handlers readable as a description of the endpoint.
Cross-cutting code lives in core/ (config, database, auth, audit, rate limiting) or shared/
(models, enums, the permission catalogue).
Every endpoint documents itself¶
/docs is the integration surface. An endpoint that does not explain itself there is not finished.
@router.post(
"",
response_model=ApiResponse, # explicit, never inferred
status_code=201, # explicit for non-200 successes
summary="Register a resource API", # one line
description=( # richer, markdown, states the permission
"...\n\n**Required scope:** `admin:apis:write`"
),
responses={409: {"model": ErrorResponse, "description": "Name already in use"}},
dependencies=[WRITE],
)
Use Literal[...] for enums so allowed values appear in the schema, and Field(description=...) on
anything non-obvious.
Query parameters are camelCase too¶
FastAPI does not apply Pydantic's alias generator to query parameters. A parameter named
group_id is group_id on the wire unless you say otherwise, which silently breaks a camelCase API:
This was a real bug — filters were accepted and ignored, returning unfiltered lists.
Loading relationships on new objects¶
SQLAlchemy's async mode cannot lazy-load. A freshly created object has unloaded relationships, and
reading one raises MissingGreenlet — an error message that names nothing useful.
session.add(user)
await session.commit()
await session.refresh(user, ["groups", "roles"]) # then they are safe to read
Routes rarely hit this because they load users with a query, which triggers the eager selectin
strategy. Fixtures and creation paths hit it constantly.