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.
SSOT Boundary — What Lives Where
Sección titulada «SSOT Boundary — What Lives Where»| Document | Owns | Never contains |
|---|---|---|
| [[API]] | Per-endpoint contracts: ### METHOD /path/ blocks, request/response JSON, field tables, status codes, enum value lists tied to a field | General 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 workflow | Individual 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.
Keeping API.md in Sync With Models
Sección titulada «Keeping API.md in Sync With Models»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 + serviceAdding or changing an endpoint:
- Edit [[API]] first — add the
### METHOD /api/v1/path/block (or an FSM table row under a### ... FSM Transitionsheader). Use canonical names from [[adr-001-a-naming-glossary|ADR-001-a]] and the path/naming rules below. - The contract coverage guard now fails (see Contract-Test Workflow). Write the contract test, or allowlist with justification.
- 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]] withSource: computed. New routes go in the app’surls.py; custom actions use@actionwithurl_pathmatching the kebab-case path in [[API]]. - Frontend: the Angular interface/type mirrors the response field table; the service method targets the documented path. JSON fields stay
snake_caseend-to-end (no client-side camelCase remap) — see Naming below.
Field-table ↔ serializer correspondence:
| API.md column | Backend equivalent | Frontend equivalent |
|---|---|---|
| Field name | serializer field / source= | interface property (same snake_case) |
Type (UUID, string, decimal(6,2), date | null) | model field / serializers.X | TS type (string, number, string | null) |
| Enum value list | TextChoices / choices= | TS string-literal union |
Source: computed | SerializerMethodField / annotation | read-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 Transitionsheader; 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:
pytest tests/test_contract_coverage.py -v # coverage guardpytest tests/contract/ -v # modular contract testspytest -m api # all API testsAdding 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.
URL Structure
Sección titulada «URL Structure»| Rule | Correct | Wrong |
|---|---|---|
| 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/ |
HTTP Methods
Sección titulada «HTTP Methods»| Method | Use | Idempotent | Body |
|---|---|---|---|
GET | Read (list, detail, queries) | Yes | Query params only |
POST | Create resource, trigger action/FSM transition | No | JSON body |
PATCH | Partial update | Yes | JSON body (changed fields only) |
DELETE | Hard delete | Yes | No 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.
Nested Resources
Sección titulada «Nested Resources»Nest only when the child cannot exist without the parent context:
GET /api/v1/employees/{id}/transitions/ ← audit log scoped to employeeGET /api/v1/employees/{id}/available-transitions/ ← state scoped to employeePOST /api/v1/employees/{id}/contract/adjustments/ ← adjustment scoped to contractFor 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/Action Endpoints (Non-CRUD)
Sección titulada «Action Endpoints (Non-CRUD)»FSM transitions and operations use POST on a verb sub-path of the resource:
POST /api/v1/employees/{id}/activate/ ← FSM transitionPOST /api/v1/employees/{id}/terminate/ ← FSM transitionPOST /api/v1/assignments/preview/ ← dry-run / validationPOST /api/v1/demand/{id}/duplicate/ ← cloneNaming Conventions
Sección titulada «Naming Conventions»| Layer | Convention | Example |
|---|---|---|
| URL paths | kebab-case | /org-units/, /accept-proposal/ |
| JSON fields (request & response) | snake_case | employee_number, org_unit_id |
| Query parameters | snake_case | page_size, coverage_state |
| Enum values | UPPER_SNAKE_CASE | ONBOARDING, ON_LEAVE |
| Error codes | UPPER_SNAKE_CASE | CONFLICT_HAS_DEPENDENCIES, FSM_INVALID_TRANSITION |
Pagination (DRF PageNumberPagination)
Sección titulada «Pagination (DRF PageNumberPagination)»All list endpoints are paginated unless explicitly marked otherwise.
| Param | Default | Max | Notes |
|---|---|---|---|
page | 1 | — | 1-based |
page_size | 25 | 100 | Override per-request. Globally supported on all list endpoints. |
ordering | model default | — | Prefix - for descending |
search | — | — | Full-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/).
Date & Time Formats
Sección titulada «Date & Time Formats»| Type | Format | Example |
|---|---|---|
| Date | ISO 8601 YYYY-MM-DD | 2026-03-17 |
| DateTime | ISO 8601 UTC | 2026-03-17T14:30:45.123456Z |
All datetimes are UTC. Frontend converts to local time for display.
ID Format
Sección titulada «ID Format»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}.
Cognito Compatibility
Sección titulada «Cognito Compatibility»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.
Content Type
Sección titulada «Content Type»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).
Throttle Rates
Sección titulada «Throttle Rates»| Scope | Rate | Applies to |
|---|---|---|
anon | 100/hour | Unauthenticated requests |
user | 1000/hour | Authenticated requests |
login | 5/hour | POST /api/v1/auth/login/ |
token_refresh | 20/hour | POST /api/v1/auth/token/refresh/ |
logout | 20/hour | POST /api/v1/auth/logout/ |
CSRF Protection
Sección titulada «CSRF Protection»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.
Authentication Mechanism
Sección titulada «Authentication Mechanism»All authenticated requests use httpOnly cookies (not Authorization: Bearer headers).
| Environment | SameSite | Secure | Notes |
|---|---|---|---|
LOCAL=True (localhost) | Lax | False | Same-origin, no HTTPS needed |
LOCAL=False (production) | None | True | Cross-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.
Cookies
Sección titulada «Cookies»| Cookie | Lifetime | Path | Visible to JS |
|---|---|---|---|
access_token | 60 minutes | / | No (httpOnly) |
refresh_token | 7 days | /api/v1/auth/token/refresh/ | No (httpOnly) |
cookie_consent | 1 year | / | Yes (regular cookie, GDPR banner state) |
Auth Flow
Sección titulada «Auth Flow»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]]).
Error Response Format
Sección titulada «Error Response Format»DRF’s native exception handling — no custom error renderer.
- Field-level validation (400):
{ "field_name": ["msg", ...], ... }— DRF’sserializer.errors. - Non-field validation (400):
{ "non_field_errors": ["msg", ...] }. - Detail errors (401/403/404/409/422/429):
{ "detail": "..." }— DRFAPIExceptionsubclasses. Conflicts that carry a machine code add"code": "UPPER_SNAKE_CASE"(e.g.CONFLICT_HAS_DEPENDENCIES).
Status codes:
| Code | Meaning | Body |
|---|---|---|
200 | Read or update | Resource JSON |
201 | Creation | Created resource JSON |
204 | Deletion / logout | No body |
400 | Validation error | {field: [msgs]} or {non_field_errors: [msgs]} |
401 | Missing/invalid auth | {detail} |
403 | Insufficient permissions / CSRF failure | {detail} |
404 | Not found | {detail: "Not found."} |
405 | Method not allowed (e.g. hard DELETE) | {detail} |
409 | Conflict (FSM invalid, dependencies, duplicate) | {detail} (+ optional code) |
422 | Business rule violation | {detail} |
429 | Throttle exceeded | {detail} |