Files
Dokumentace/API/DESIGN_DOCUMENT_API.md

9.2 KiB

API Design Guidelines

Document Version: v2

Note on Versioning:

  • This document version is independent — reused across projects
  • Project version source of truth: the project manifest (pyproject.toml, Cargo.toml)
  • The URL version (/v0, /v1) is a separate axis and moves independently — see section 1
  • CHANGELOG.md uses the project version, never the URL version
  • README.md — Project overview, tool descriptions, build instructions
  • AGENTS.md — Rules for AI assistants
  • PROJECT.md — Project goals and current state
  • CHANGELOG.md — Version history
  • DESIGN_DOCUMENT_LANGUAGE.md — everything language-specific (DESIGN_DOCUMENT_PYTHON.md, …)

This document applies to any project that exposes an HTTP API, in addition to the design document for the language it is written in. Where the two disagree, the language document wins on language matters and this one wins on API surface.


1. Two Version Axes

An API carries two versions that must never be conflated.

Axis Where it lives What it means How often it moves
URL version the path — /v0/... the contract with the consumer almost never
Product version project manifest, job manifest the release of the software every release

The product version follows semantic versioning and the rules in AGENTS.md. It is what the OpenAPI/Swagger version field reports, and what CHANGELOG.md is organised by.

The URL version is not semantic. It is a single integer identifying a frozen contract. A consumer pinned to /v0 keeps working across every product release until /v0 is formally retired.

1.1 When the URL version is bumped

Bump /v0 to /v1 only on a breaking change to an existing endpoint:

  • a field is removed or renamed
  • a field changes type or meaning
  • a default changes in a way that alters the response
  • an endpoint is removed or its path changes
  • a previously optional parameter becomes required

The following are not breaking and never justify a bump:

  • adding a new endpoint
  • adding a new optional query parameter
  • adding a new field to a response
  • performance, caching or refresh-strategy changes that leave the payload identical

1.2 Coexistence and retirement

When the version is bumped, the old version keeps serving until every consumer has moved:

  • both versions are routed at the same time; the old one is not silently redirected
  • the old version is marked deprecated in the OpenAPI schema
  • retirement is announced in CHANGELOG.md at least one release before removal
  • removal is a MAJOR bump of the product version

A new URL version is a cost paid by every consumer. Prefer an additive change, a new endpoint, or an opt-in query parameter over a bump.


2. Path Naming

/v<n>/<CONSUMER>/<resource>
Segment Rule Example
v<n> lowercase v + integer, no dots /v0, /v1
<CONSUMER> the consuming system, as its own abbreviation WEXT, PDH, SALESOFFER
<resource> lowercase kebab-case insert-relation, attribute-defs
  • Collections are plural, single lookups append the identifier: /v0/WEXT/products and /v0/WEXT/products/{productnr}
  • Sub-resources nest, they are not hyphenated into the parent: /v0/WEXT/assets/general, not /v0/WEXT/assets-general
  • Never PascalCase or SCREAMING_CASE a resource. The consumer segment is the only place an uppercase abbreviation is allowed, because it is a proper name rather than a word.
  • Spell resources out. number-of-inserts over noi — the URL is read by people who do not know the internal acronym.

2.1 Endpoints outside the versioned space

Two different kinds of endpoint carry no version prefix, and the same rule does not cover both.

Operational endpoints — /health, /cache/stats, /cache/reset — are called by operators and monitoring, never by a consumer. They change with the product version, no ceremony.

Unversioned consumer endpoints — the token endpoints (/subscribe) and any feed that predates the versioning scheme — carry no prefix but are still contracts, and the token endpoints are the widest of them: every delta consumer holds a token one of them issued. A breaking change to any of these follows section 1.1 exactly. Having no version to bump is not permission to break. The endpoint is either extended additively, or it enters the versioned space as a new path while the old one keeps serving and is marked deprecated.

Unversioned is not unprotected. What decides is who calls the endpoint, not whether the path has a number in it.


3. Parameter Naming

Kind Convention Example
Path parameter snake_case /products/{productnr}
Query parameter snake_case ?web_only=true, ?dict_id=42
Header X- prefix, Kebab-Case X-Token

Reserved query parameter names, to be used for nothing else:

Name Meaning
limit rows per page
offset row offset
since ISO-8601 low-water mark for a delta pull
format response format override, where an endpoint serves more than one

4. Code Naming

The path is the contract; the code that serves it mirrors the path mechanically, so the two cannot drift apart.

Element Convention Example
Router variable <consumer>_router wext_router
Handler function <consumer>_<resource>, snake_case wext_attribute_defs
Single-item handler <consumer>_<resource>, singular wext_product
OpenAPI tag <Consumer> feed "WEXT feed"

One router per consumer, declared with its own prefix and tag and included on the application in a single place. A handler never hardcodes its own prefix.


5. Responses

5.1 Envelope

A list endpoint returns its rows inside an envelope carrying the paging state, never a bare array:

{"table": "wext-products", "since": null, "count": 3,
 "limit": 5000, "offset": 0, "has_more": false, "rows": []}

For XML, the same metadata goes on the root element as attributes (limit, offset, returned, hasMore).

Where the response body is a message defined by the consumer rather than by us, the paging metadata stays outside that message — the body must remain exactly what the consumer schema expects.

5.2 Paging

Every list endpoint is paged. An endpoint with no cap is a denial of service waiting for the first client that omits a filter.

  • limit has a documented default and a hard maximum, both configurable by environment variable
  • the maximum is chosen by measurement — response size, and the time a request holds any shared lock — not by guess
  • has_more is reported so the client knows to continue
  • deep offsets over a very large table are not the intended access path; document the alternative (walking since windows) instead

5.3 Delta feeds

An endpoint whose source has a reliable change column may support incremental pulls:

  • ?since=<ISO-8601> returns only rows changed strictly after that instant
  • a subscription token in X-Token supplies, and then advances, a server-tracked watermark
  • an explicit ?since always wins over the token value
  • a malformed ?since is rejected with 422 before it reaches the query

A snapshot message is never a delta. If a response states a complete set as of a timestamp, answering with only the changed rows tells the consumer that the set shrank to those rows. Such an endpoint is a full snapshot and says so in its description.

5.4 Status codes

Code When
200 success, including a legitimately empty collection
302 redirect to an external asset
400 the request is missing something it needs
403 unknown or revoked token
404 a named single entity does not exist
422 a parameter is present but malformed
500 internal failure

An empty collection is 200 with an empty list, not 404. 404 means this identifier does not exist, which only a single-entity lookup can determine.

Error bodies carry a short, generic detail. Internal detail — driver strings, connection parameters, SQL, stack traces — goes to the log only, never to the client.


6. Documentation

  • Every endpoint declares a summary and a description. The description states where the data comes from, whether the endpoint is a delta or a snapshot, and any non-obvious default.
  • The OpenAPI version field is read from the project manifest, never hand-maintained.
  • The interactive schema (/docs, /openapi.json) is the reference for consumers. Prose that only restates it belongs nowhere.
  • A new consumer feed is announced in CHANGELOG.md under the product version that ships it.

7. Testing

In addition to the testing rules in the language design document:

  • every endpoint has at least one test asserting the shape of its response
  • the query an endpoint builds is asserted, so a changed filter cannot pass unnoticed
  • tests stub the data-access layer; they never require a live database
  • the empty-result path is tested explicitly — it is the one most often wrong