# ADR 0003: Canonical paginated response envelope - **Status:** Accepted - **Date:** 2026-04-09 - **Deciders:** Platform team, Frontend team ## Context Different Odoo controllers returned paginated data in at least three shapes: - `{ data: [...], total: N, page, limit }` - `{ results: [...], count: N }` - `[...]` (bare array, no totals) The frontend grew defensive code paths to handle all three, and every new endpoint risked inventing a fourth shape. ## Decision Every list endpoint MUST return the canonical envelope produced by `encoach_api.controllers.base.paginated_envelope`: ```json { "items": [ … ], "data": [ … ], "total": 123, "page": 1, "size": 20 } ``` - `items` is the canonical field name. New code reads from `items`. - `data` mirrors `items` for backwards compatibility with older callers and can be removed once every consumer migrates. - `total` is the total number of matching records across all pages. - `page` is 1-indexed. - `size` is the requested page size (capped server-side). On the frontend, `PaginatedResponse` in `frontend/src/types/common.ts` exposes both `items` and an optional `data`, and service methods (`users.service.ts`, `lms.service.ts`, etc.) construct a clean `PaginatedResponse` object from whatever the server returns so that UI code never sees the legacy fields. ## Consequences - Positive: frontend code is simpler and typesafe — read `items`, done. - Positive: OpenAPI spec advertises one consistent shape across endpoints. - Negative: one additional key (`data`) is duplicated in responses. Cheap (same reference, no JSON bloat) and easy to delete later. ## Alternatives considered - **Follow JSON:API's `{ data, meta, links }` convention.** Rejected as overkill: we don't use HATEOAS, and the extra indirection would force a rewrite of every existing consumer. - **Return a bare array + pagination headers.** Rejected because Odoo's controller helpers make setting custom headers awkward and because it hides totals from curl/Postman users.