Add API and web application design documents
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
# 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
|
||||
|
||||
## Related Documents
|
||||
|
||||
- **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:
|
||||
|
||||
```json
|
||||
{"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
|
||||
Reference in New Issue
Block a user