Ir al contenido

API Conventions — Django DRF ↔ Angular 21

API Conventions — Django DRF ↔ Angular 21

Sección titulada «API Conventions — Django DRF ↔ Angular 21»

The maintenance guide for [[API]]. [[API]] holds the per-endpoint contracts (method, path, schema, field tables, status codes); this document holds every cross-cutting rule that applies to all of them. A rule lives in exactly one place: if it is general, it belongs here and [[API]] must not restate it.

These rules codify DRF’s defaults plus project-specific decisions ([[adr-005-api-contract-ssot|ADR-005]]). Violating them is a review blocker.


DocumentOwnsNever contains
[[API]]Per-endpoint contracts: ### METHOD /path/ blocks, request/response JSON, field tables, status codes, enum value lists tied to a fieldGeneral conventions, restated pagination/auth/error prose
[[api-conventions|this file]]Cross-cutting rules: URL structure, methods, pagination, naming, dates, IDs, auth, throttling, CSRF, error format, sync workflowIndividual endpoint schemas
ADRs (docs/adr/)The why (decisions, legal/legacy rationale)The what of the wire contract

[[API]] is the single source of truth for HTTP contracts ([[adr-005-api-contract-ssot|ADR-005]] rule 4: if code and [[API]] disagree, [[API]] wins — fix the code). Fields documented in [[API]] but missing from a serializer are an implementation gap, not a spec error ([[adr-005-b-api-md-scope-implemented-and-planned|ADR-005-b]]) — no NOT IMPLEMENTED markers. To check whether an endpoint is live, read backend/config/urls.py: if the app is routed, the endpoint exists and must match the spec.


The contract spans three layers that must agree. The flow is one-directional: spec first, code follows.

API.md (contract, SSOT) → backend serializer/viewset → Angular type + service

Adding or changing an endpoint:

  1. Edit [[API]] first — add the ### METHOD /api/v1/path/ block (or an FSM table row under a ### ... FSM Transitions header). Use canonical names from [[adr-001-a-naming-glossary|ADR-001-a]] and the path/naming rules below.
  2. The contract coverage guard now fails (see Contract-Test Workflow). Write the contract test, or allowlist with justification.
  3. Backend: the serializer field names, types, and enum choices must match the field table exactly. Computed/read-only fields (e.g. coverage_state, employee_name) are declared in [[API]] with Source: computed. New routes go in the app’s urls.py; custom actions use @action with url_path matching the kebab-case path in [[API]].
  4. Frontend: the Angular interface/type mirrors the response field table; the service method targets the documented path. JSON fields stay snake_case end-to-end (no client-side camelCase remap) — see Naming below.

Field-table ↔ serializer correspondence:

API.md columnBackend equivalentFrontend equivalent
Field nameserializer field / source=interface property (same snake_case)
Type (UUID, string, decimal(6,2), date | null)model field / serializers.XTS type (string, number, string | null)
Enum value listTextChoices / choices=TS string-literal union
Source: computedSerializerMethodField / annotationread-only, never sent on write

When a serializer gains a field not in [[API]], either add it to [[API]] (if it is part of the contract) or keep it out of the response — do not let undocumented fields reach the client.


Contract-Test Workflow ([[adr-005-a-contract-testing-pipeline|ADR-005-a]])

Sección titulada «Contract-Test Workflow ([[adr-005-a-contract-testing-pipeline|ADR-005-a]])»

[[API]] is machine-parsed. The heading format is a machine contract — do not change it:

  • Every endpoint is a level-3 heading: ### METHOD /api/v1/path/ (METHOD ∈ GET/POST/PATCH/PUT/DELETE).
  • FSM transitions may instead be table rows | `POST /{id}/action/` | ... | under a ### <Resource> FSM Transitions header; the parser prefixes them with the section base path.
  • Any endpoint not matching one of these two patterns is invisible to the coverage guard.

The guard (backend/tests/test_contract_coverage.py) enforces a 1:1 mapping between documented endpoints and contract tests, rejects duplicate (METHOD, path) definitions, and keeps the KNOWN_UNTESTED allowlist free of stale entries. Run:

Ventana de terminal
pytest tests/test_contract_coverage.py -v # coverage guard
pytest tests/contract/ -v # modular contract tests
pytest -m api # all API tests

Adding a ### METHOD /api/... line with no matching test fails the guard immediately — write the test first, or add the path to KNOWN_UNTESTED with a justification comment.


Schema Governance — Endpoint Path Conventions

Sección titulada «Schema Governance — Endpoint Path Conventions»

These rules apply to every endpoint in [[API]] and all future additions. They codify what DRF’s SimpleRouter enforces plus project-specific decisions from [[adr-005-api-contract-ssot|ADR-005]]. Violating them is a review blocker.

RuleCorrectWrong
Plural nouns for collections/employees/, /org-units//employee/, /org-unit/
Singular only for singletons/auth/me/, /settings//auth/mes/
Kebab-case paths/org-units/, /employee-tags//org_units/, /orgUnits/
{id} for path params/employees/{id}//employees/:id/, /employees/<id>/
Trailing slash always/employees//employees
Version prefix /api/v1/ for all domain + auth endpoints/api/v1/employees/, /api/v1/auth/login//api/employees/
No version only for infra/api/health//api/v1/health/
MethodUseIdempotentBody
GETRead (list, detail, queries)YesQuery params only
POSTCreate resource, trigger action/FSM transitionNoJSON body
PATCHPartial updateYesJSON body (changed fields only)
DELETEHard deleteYesNo body → 204

PUT is technically generated by DRF’s ModelViewSet but not recommended — use PATCH for all updates (partial=True). Future viewsets may disable PUT via http_method_names.

Nest only when the child cannot exist without the parent context:

GET /api/v1/employees/{id}/transitions/ ← audit log scoped to employee
GET /api/v1/employees/{id}/available-transitions/ ← state scoped to employee
POST /api/v1/employees/{id}/contract/adjustments/ ← adjustment scoped to contract

For resources queryable independently, use flat paths with query filters:

GET /api/v1/assignments/?position_id={id} ← not /demand/{id}/assignments/
GET /api/v1/demand/?org_unit_id={id} ← not /org-units/{id}/positions/

FSM transitions and operations use POST on a verb sub-path of the resource:

POST /api/v1/employees/{id}/activate/ ← FSM transition
POST /api/v1/employees/{id}/terminate/ ← FSM transition
POST /api/v1/assignments/preview/ ← dry-run / validation
POST /api/v1/demand/{id}/duplicate/ ← clone
LayerConventionExample
URL pathskebab-case/org-units/, /accept-proposal/
JSON fields (request & response)snake_caseemployee_number, org_unit_id
Query parameterssnake_casepage_size, coverage_state
Enum valuesUPPER_SNAKE_CASEONBOARDING, ON_LEAVE
Error codesUPPER_SNAKE_CASECONFLICT_HAS_DEPENDENCIES, FSM_INVALID_TRANSITION

All list endpoints are paginated unless explicitly marked otherwise.

ParamDefaultMaxNotes
page11-based
page_size25100Override per-request. Globally supported on all list endpoints.
orderingmodel defaultPrefix - for descending
searchFull-text where supported

Response envelope:

{
"count": 150,
"next": "/api/v1/employees/?page=2",
"previous": null,
"results": [...]
}

Non-paginated exceptions: /api/health/, /api/v1/auth/*, singleton resources (/settings/), reference lists (/unit-types/, /business-rules/).

TypeFormatExample
DateISO 8601 YYYY-MM-DD2026-03-17
DateTimeISO 8601 UTC2026-03-17T14:30:45.123456Z

All datetimes are UTC. Frontend converts to local time for display.

All primary keys are UUID v4 (xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx). No exceptions — every model in the project uses UUID PKs. DRF lookup field is pk, URL param is {id}.

The /api/v1/auth/me/ response uses OIDC claim names (sub, given_name, family_name, email_verified) so the Angular AuthService works identically against local SimpleJWT and production Cognito without field mapping.

All requests with a body must include Content-Type: application/json. DRF is configured with JSONParser only — no form-data, no multipart (except future file uploads).

ScopeRateApplies to
anon100/hourUnauthenticated requests
user1000/hourAuthenticated requests
login5/hourPOST /api/v1/auth/login/
token_refresh20/hourPOST /api/v1/auth/token/refresh/
logout20/hourPOST /api/v1/auth/logout/

The CookieJWTAuthentication class enforces CSRF checks on all mutation methods (POST, PUT, PATCH, DELETE). The Angular auth interceptor reads the csrftoken cookie and sends it as X-CSRFToken header.


All authenticated requests use httpOnly cookies (not Authorization: Bearer headers).

EnvironmentSameSiteSecureNotes
LOCAL=True (localhost)LaxFalseSame-origin, no HTTPS needed
LOCAL=False (production)NoneTrueCross-origin (Amplify ↔ AppRunner)

The Angular interceptor adds withCredentials: true to every request so the browser automatically sends cookies cross-origin.

Exception: /api/v1/auth/login/ and /api/v1/auth/logout/ set withCredentials: false. Login must not send stale session cookies that could conflict with fresh authentication. Logout clears cookies server-side and does not need them in the request. All other endpoints — including /api/v1/auth/token/refresh/ (which needs the httpOnly refresh_token cookie) — use withCredentials: true.

CookieLifetimePathVisible to JS
access_token60 minutes/No (httpOnly)
refresh_token7 days/api/v1/auth/token/refresh/No (httpOnly)
cookie_consent1 year/Yes (regular cookie, GDPR banner state)

Local (PostgreSQL + SimpleJWT + httpOnly cookies): login sets access_token + refresh_token cookies and returns {user: {...}}; the browser sends them automatically (withCredentials: true, X-CSRFToken on mutations); refresh issues a new access cookie (+ rotated refresh, ROTATE_REFRESH_TOKENS=True); the Angular APP_INITIALIZER calls GET /auth/me/ on bootstrap (on 401, tries refresh then retries); logout blacklists the refresh token and clears cookies. Production (AWS Cognito): tokens issued by Cognito and validated against its JWKS; cookie strategy SameSite=None; Secure=True (see [[adr-008-authentication|ADR-008]]).


DRF’s native exception handling — no custom error renderer.

  • Field-level validation (400): { "field_name": ["msg", ...], ... } — DRF’s serializer.errors.
  • Non-field validation (400): { "non_field_errors": ["msg", ...] }.
  • Detail errors (401/403/404/409/422/429): { "detail": "..." } — DRF APIException subclasses. Conflicts that carry a machine code add "code": "UPPER_SNAKE_CASE" (e.g. CONFLICT_HAS_DEPENDENCIES).

Status codes:

CodeMeaningBody
200Read or updateResource JSON
201CreationCreated resource JSON
204Deletion / logoutNo body
400Validation error{field: [msgs]} or {non_field_errors: [msgs]}
401Missing/invalid auth{detail}
403Insufficient permissions / CSRF failure{detail}
404Not found{detail: "Not found."}
405Method not allowed (e.g. hard DELETE){detail}
409Conflict (FSM invalid, dependencies, duplicate){detail} (+ optional code)
422Business rule violation{detail}
429Throttle exceeded{detail}