API contract workflow
Contract first: how an operation goes from a Zod schema in packages/contracts to the OpenAPI document, a Dart client, a mock server and an implementation — and how the contract changes without breaking the apps.
The API contract is the interface between Anis's track and Imad's track. One rule holds it together: no operation is implemented or consumed before it exists in packages/contracts on dev. The mock server lets the apps be built against the contract while the server implementation is still being written (ADR-002).
- SRV-03 writes contract v1 — every V1 operation, the OpenAPI export and the mock server — and reaches the milestone API contract v1 published (OpenAPI + mock server). Imad reviews it as the first consumer.
- MOB-03 generates
hoco_apifrom that document, with a regeneration script, the Prism workflow and a pinned contract version. - SRV-29 repeats the loop for the V2 operations, so V2 mobile work starts against the mock too.
After contract v1, every change follows the loop below, however small.
Where the contract lives
| Artefact | Location | Produced by | Read by |
|---|---|---|---|
| The contract — oRPC routes, Zod schemas, declared errors, metadata (permission, scope, idempotency, release) | hoco → packages/contracts/src/ (contract.ts, routers/<module>.ts, schemas/) | Anis, in contract PRs | apps/server (implements it), packages/api-client (Next apps) |
| The OpenAPI 3.1 document | apps/docs/public/openapi/hoco-v1.json, served by this site at /openapi/hoco-v1.json | pnpm openapi:export | Imad's generator, Prism, reviewers |
| The live document | /openapi.json on each environment — what that environment runs | The server | Debugging; Scalar at /reference outside production |
| Contract tags | contracts-vX.Y.Z on the merge commit in hoco | Anis | The pin in hoco-mobile |
| The Dart client | hoco-mobile → packages/hoco_api, with the pinned copy of the document | Imad's generator | Both apps |
| Catalogue and reference pages | apps/docs/content/spec/endpoints.json, /docs/srs/api | Anis, in the contract PR | Everyone |
| Error registry | Errors | The contract PR | Every app |
The loop
Propose (Anis). Branch feature/<TASK-ID>-contract-<topic>, for example feature/SRV-21-contract-visits. For each operation, declare in packages/contracts:
- the op id (the contract key path, e.g.
delegate.visits.arrive) and.route({ method, path })relative to/v1; - the Zod input and output, with the field conventions of the API reference (camelCase, centimes, UTC, E.164, UUIDv7);
- the declared errors with their status and
detailsshape; - the metadata: permission key, scope, idempotency requirement, release (V1 or V2);
- a summary, and an example for every response (Prism serves the examples).
Then run pnpm openapi:export, update endpoints.json and the operation's section on its API page, add the changelog entry and bump the contract version. Open the PR with the label contract and a title like feat(contracts): delegate visit check-in operations [SRV-21].
Review (Imad, within 4 working hours). Imad checks that the shapes generate clean Dart (no anonymous unions, explicit nullability, enums as UPPER_SNAKE_CASE strings), that every field a screen needs is there and nothing it does not, the pagination style, the Idempotency-Key and occurredAt requirements on offline mutations, error codes the UI can map, and realistic examples (French copy, centimes, E.164 phones). He approves, or leaves blocking: comments. Akram is consulted when a shape decides what a screen shows.
Merge and publish. Squash-merge into dev; Anis tags the merge commit contracts-vX.Y.Z. CI checks that the committed document equals a fresh export and that oasdiff finds no undeclared breaking change. The docs deploy publishes /openapi/hoco-v1.json; staging serves /openapi.json once the server foundation (SRV-04) is live. Anis posts in the team group: "Contract 1.N.0 — operations … — PR #…".
Regenerate (Imad, same or next working day). Copy the tagged document into hoco-mobile as the pinned copy, run the generator, commit chore(api): regenerate client from contract 1.N.0 [MOB-03]. The pinned version is the document's info.version.
Build in parallel. Imad builds the screens against Prism. Anis implements behind the same contract; until he does, a contracted operation answers 501 NOT_IMPLEMENTED with the standard error envelope on staging.
Implement (Anis). feat(server): implement the visit check-in operations [SRV-21]. This PR must not change the contract. If implementing reveals a contract problem, stop and open a contract PR — back to step 1.
Integrate. When the operation is live on staging, Imad switches that feature from the mock to staging (the full switch is MOB-12). A mismatch is a bug against whichever side differs from the contract; if the contract itself is wrong, a contract PR fixes it.
The description of a contract PR:
**Operations:** delegate.visits.arrive (changed: response adds `distanceMeters`), delegate.visits.start (changed: accepts an optional `note`)
**Class:** additive — contract 1.4.0 → 1.5.0
**Consumers:** DA-10 (MDA-03, MDA-05), BO-23 (DSH-08)
**New error codes:** none
**Examples:** updated for both responsesMock server
Prism serves the OpenAPI document as a mock API, on the developer's machine:
# in hoco — the latest contract on your branch (regenerate it with pnpm openapi:export)
pnpm mock # prism mock … --port 4010 --dynamic --cors
# in hoco-mobile — the pinned copy (the path is set in MOB-03)
npx @stoplight/prism-cli mock packages/hoco_api/openapi/hoco-v1.json --port 4010
# Android emulator: http://10.0.2.2:4010 · a phone on the same Wi-Fi: http://<laptop-ip>:4010- Dynamic mode (
--dynamic, whatpnpm mockruns) generates data from the schemas. Response objects are closed (additionalProperties: false), so the generated data has exactly the documented fields. Contract v1.0.0 has no response examples yet; until they land, use dynamic mode. - Static mode (without
--dynamic) returns the response example; contract PRs add examples operation by operation. - Error states: send
Prefer: code=422(orPrefer: example=<name>) to get the declared error example and build the error screens. - Prism also validates the app's requests against the document: a request Prism rejects is a client bug to fix before integration. Prism's own validation answers are not HOCO's error envelope — build error UIs from the declared error examples, not from Prism's validation messages.
The Next apps do not need the mock: they live in the same repository as the server and run against it locally.
Additive and breaking changes
| Change | Class | What to do |
|---|---|---|
| New operation | Additive — minor | Loop as above |
| New optional request field, new query parameter with a default | Additive — minor | |
| New response field | Additive — minor | Clients ignore unknown fields |
| New error code on an existing operation, same status family | Additive — minor | Registry row, translations, app mapping |
| New enum value in a response | Additive — minor, announced first | Changelog entry at least one mobile release before the server emits it; apps render unknown values with a neutral label |
| Relaxed validation (longer limits, more accepted values) | Additive — minor | |
| Descriptions and examples only | Patch | |
| Removing or renaming a field, an operation, a parameter or an enum value | Breaking | Add the new one alongside, deprecate the old one |
| Making an optional request field required, adding a required field | Breaking | New field or new operation |
| Changing a type, unit or format (centimes to decimals, date to date-time) | Breaking | New field |
| Changing the pagination style, the default sort, or lowering a page limit | Breaking | New operation |
| Tightening validation | Breaking | Only with the deprecation window |
| Changing the meaning of a status or error code, reusing a code | Breaking — never done | Create a new code |
Requiring a new header (for example Idempotency-Key) or a new permission that rejects existing callers | Breaking | Deprecation window; check the app versions in use |
A breaking need is shipped as an addition plus a deprecation, never in place. /v2 is reserved for a wholesale redesign and is not planned: the V2 product scope is additive and stays in /v1.
Deprecation
- The route or field is marked
deprecatedin the contract. It appears asdeprecatedin the OpenAPI document, and the generated Dart code flags it. - The server answers deprecated operations with
Deprecation(RFC 9745) andSunset(RFC 8594) headers and a link to the page:
Deprecation: @1798761600
Sunset: Fri, 31 Dec 2027 23:59:59 GMT
Link: <https://{docs-site}/docs/srs/api/orders#orderslist>; rel="deprecation"- Removal needs all three conditions:
- at least two releases of each affected app have reached the track the users are on (closed track during the pilot, production after) without using the deprecated item;
- the minimum supported version of each affected app (BO-66) has been raised above the last version that uses it;
- the server logs (which carry
X-App-Version) show no call to it for 14 days.
- The back-office and the reseller web are deployed with the server, so they migrate in the same release; the window exists for the mobile apps.
Versioning
| Version | Where | Rule |
|---|---|---|
| Path version | /v1 | For the whole life of the V1 and V2 product scope |
| Contract version | info.version of the document, tag contracts-vX.Y.Z | 1.0.0 at API contract v1 published; minor for additive changes; patch for descriptions and examples; 0.x before that milestone |
| App version | X-App-Version header from the apps | Below the minimum for the app and platform: 426 UPGRADE_REQUIRED |
| Platform version | vX.Y.Z tags in hoco | Independent of the contract version (versions) |
Assumption — the export currently takes info.version from apps/server/package.json. SRV-03 switches it to the version of @hoco/contracts, bumped in each contract PR, so the contract version moves only when the contract does.
Error codes
- Every error an operation can return is declared in the contract with its HTTP status and
detailsshape, and listed in the error registry. - Codes are
UPPER_SNAKE_CASEand stable forever: never renamed, never reused for another meaning. A retired code stays documented as retired. - A new code arrives in one PR with: the contract declaration, the registry row (status, meaning,
details, operations), the server message in French witharandenkeys, and an example. Imad adds the app mapping when he regenerates. - Clients branch on
code— never onmessage, never on the status alone. NOT_IMPLEMENTED(501) exists on staging only. The release checklist verifies that no contracted operation of the release still answers it.
Keeping the reference pages in sync
- Once SRV-03 lands, the OpenAPI document is authoritative.
endpoints.jsonand the API reference pages describe it for people. - The contract PR updates, in the same PR: the operation in
endpoints.json(op id, method, path, auth, permission, apps, release, requirements); its section on the API page (request, response example, errors, rules), under the anchor of its op id (cart.checkout→#cartcheckout); the error registry. - CI (added with SRV-03) compares the op ids of the exported document with
endpoints.jsonand fails when an operation is missing on either side. - When a page and the document disagree, the document wins. The page is fixed in a
docs(contracts): …commit. - Scalar (
/reference, local and staging) renders the same document, so it is always in sync.
Contract changelog
Newest first. One entry per merged contract PR, in the same PR. Imad reads this table before each regeneration.
| Version | Task · PR | Change | Class | App action |
|---|---|---|---|---|
1.2.0 | MOB-03 · Imad's requests | Generation only: every tagged union declares discriminator with its mapping (BannerTarget and ReportHistoryItem by type, LiveEvent by event, OrderHistoryEntry by kind, ResellerMap by mode); the enums of query and path parameters reference their named models (CatalogSort, ResellerListSort, ObjectivePeriod, PerformancePeriod, PromotionType…), and parameters that share a name with different values get one model per operation (AdminOrdersListSort, AdminOrdersListDateField…). Earlier, OpenAPI pass 2 (#45) named every nested object. Changed: ProductPresented of delegate.visits.report carries `kind: "CATEGORY" | "PRODUCT"(required, the discriminator); the report view's presented products carry the samekind`. | Breaking for ProductPresented only — accepted in place because no released app sends it (asked by Imad); the rest is additive |
1.1.0 | SRV-41 | New operations: admin.users.get, admin.staff.assignable, admin.credit.list, admin.variants.prices, admin.variants.bulkPrices, admin.resellers.paymentMethods, admin.banners.reorder, admin.imports.list, admin.imports.template, creditNotes.list, announcements.get. New fields and filters: admin.resellers.list/map/export/bulk ids; map pins delegateName; DeliveryCard version, warehouse and deliveries filters warehouseId, cod, validatedFrom/validatedTo; admin.orders.create response issues; Promotion fixedPrices (per customer type, value still valid) and preview for a customerType or a guest (resellerId optional, response viewer, priceLocked; reseller absent without a reseller); AttributeDefinition minValue/maxValue/showOnProductSheet, VariantAttribute onProductSheet; Announcement bodyAr, image (imageFileId), audience.customerTypes, targetNames, file; FilePurpose ANNOUNCEMENT_IMAGE; orders.confirmAssisted paymentMethod/addressId/note; orders.rejectAssisted reasonCode + comment (reason now optional); returns.create reasonCode (reason optional with a code), ReturnView reasonCode; reference lists ASSISTED_REJECT_REASON, RETURN_REASON; FinanceSummary paymentInstructions; payments.declare reference optional; promotions.list brandId; OrderSummary invoiceStatus; ProductDetail complements; ProductCard/VariantPromotion isFlashSale (+ card endsAt); notifications.markAllRead group. New stream event: return.status_changed (admin stream; return.requested and it also reach support.view holders). Fix: catalog.products.similar lists the configured complements first. | Additive (new enum values announced here: AdminEventType.return.status_changed, FilePurpose.ANNOUNCEMENT_IMAGE, ReferenceListKey × 2) | Regenerate; RA-23 confirm/reject with the new fields, RA-31 optional reference and payment details, RA-40 return reasons from the list, flash badge from isFlashSale, inbox « tout lire » per tab |
1.0.0 | SRV-03 — done 18 Oct 2026 | Contract v1: all 269 V1 operations of the catalogue as 341 routes (multi-method operations split per verb, e.g. admin.segments.crud.list), with request and response schemas and pagination; enum alignment (BannerPlacement, AttributeFilterWidget, InventorySessionStatus); Prism mock in dynamic mode. Response examples follow. | — | Generate hoco_api (MOB-03) |
0.1.0 | PRJ-01 — done | Scaffold: system.health, system.version, the error envelope, money and pagination schemas | — | None — not consumed |
New entries follow this template:
| `1.5.0` | SRV-21 · #48 | delegate.visits.arrive returns `distanceMeters`; delegate.visits.start accepts an optional `note` | Additive | Regenerate; show the distance on DA-10 |Pull requests and reviews
What a pull request must contain, how big it may be, the CI checks that gate it in each repository, who reviews what in a team of three, how fast, and how a PR is merged and cleaned up.
Design workflow
How Akram's designs flow into development: Claude Design organisation and statuses, the 21-day checkpoints, the daily feasibility check, the handoff package, design QA, changes after handoff and HOCO's validation of the designs.