REST API Principles
Resources
- Every core domain concept is its own resource:
/api/v{version}/{entities}/{id}. Resource names are plural kebab-case, e.g. /api/v1/flow-categories/{id}.
- When the domain language treats a multi-word concept as a single word, write it as one word. Document such exceptions to the naming rule explicitly in the project applying it.
Versioning
- The
v{version} segment is the major version only: v1, never v1.2.
- It is required for public APIs, where we do not control all clients; optional for internal APIs, where all clients can easily be moved along with API changes.
Methods and status codes
GET is safe; PUT and DELETE are idempotent; POST creates or executes.
400 Bad Request for validation failures; 409 Conflict for requests that conflict with the resource's current state.
Collections
- The resource root
/api/v{version}/{entities} is a collection listing all resources of that type.
- If an unfiltered listing is inappropriate, the collection takes query parameters to filter; an unfiltered request returns
400 Bad Request, and the error names the required filters.
- There is no pagination convention — deliberately, not as an oversight. Introduce one only when scale demands it.
Ownership and nesting
- If an entity owns a collection of sub-resources (deleting the parent deletes them), represent it as a sub-collection under the parent:
/api/v{version}/{parents}/{id}/{children}.
- Cap owned sub-collections at one level under the parent unless there is a documented reason to go deeper.
- If an entity holds a collection of entities it does not own (independent lifecycle), those entities are their own root collection. In the referencing entity's representation they appear only as refs — the minimal representation of a resource: its id, plus a display name where appropriate.
Resource shape
Related data appears in a representation in one of two forms: embedded (the full representation, inline) or as a ref (id plus display name). Owned sub-resources may be embedded; non-owned resources appear only as refs. Example — GET /api/v1/flow-instances/42:
{
"id": 42,
"name": "Purchased aluminium scrap",
"flowCategory": { "id": "ALU07", "name": "Aluminium" },
"processes": [
{ "id": 17, "name": "Remelting" },
{ "id": 23, "name": "Casting" }
],
"flowDetails": [
{ "id": 901, "year": 2025, "inputAmount": 120.5, "unit": "tonne" },
{ "id": 902, "year": 2026, "inputAmount": 98.0, "unit": "tonne" }
]
}
flowCategory and processes are refs — non-owned resources living in their own root collections. processes shows refs in a collection-valued property.
flowDetails is the owned sub-collection, embedded in full — the same resources addressable at /api/v1/flow-instances/42/flow-details.
- A ref may optionally carry a self link where that helps clients discover resources:
{ "id": "ALU07", "name": "Aluminium", "links": [{ "rel": "self", "href": "/api/v1/flow-categories/ALU07" }] }.
- Complete self-discoverability and navigability (full HATEOAS) is a non-goal; individual HATEOAS concepts — like self links — are adopted where they are useful.
Operations
- An operation on the domain model is its own resource, suffixed
-operation, e.g. /api/v1/migrate-flow-category-operations.
- Operation names are verb-first and keep the
-operation suffix even where a noun name (e.g. /flow-category-migrations) would read more naturally — self-declaring and greppable wins.
- Operations may likewise be organized under an
/operations sub-resource, and deeper, when it makes sense.
- An operation may span several domain objects, of the same or of different types, when appropriate.
- An operation has its own id and is referenceable by URL when its execution must be referenced afterwards (async status, audit/history); otherwise it executes without one.
POST to the operation resource executes it, creating an execution at /{id} when the operation has ids.
- A long-running operation with an id returns
202 Accepted with a Location header pointing at the execution; its status is read from that URL.
- An operation has its own response, with the relevant resources embedded or linked.
Reports
- A request that returns data from multiple entities/resources is its own top-level resource, suffixed
-report, e.g. /api/v1/flow-instance-data-report.
- Report names are singular, unlike collections: a report is a parameterized singleton view — nothing is enumerated and there is no
/{id}. If a report needs persisted, addressable runs, model it as an operation with ids instead.
- Reports may be organized under a
/reports sub-resource, and deeper when needed, e.g. /api/v1/reports/flow-details/process-report?…. The grouping prefix stays plural; the leaf names one report.
- A report is read-only:
GET, with query parameters selecting its content. A request missing required parameters returns 400 Bad Request naming them.
- A report resource may contain redundant data — values copied from other resources or entities — to be concise and efficient.
- That does not displace embedding or referencing related entities where it makes sense: if nearly all of a resource's properties would be copied into the report, consider embedding the resource instead.
1---2name: rest-api-principles3description: REST API design principles — resource modeling, URL structure, versioning, collections, sub-collections, ownership, operations, reports, methods and status codes. Use when designing, adding, or reviewing REST API endpoints, or deciding how a domain concept maps to resources and URLs.4---56# REST API Principles78## Resources910- Every core domain concept is its own resource: `/api/v{version}/{entities}/{id}`. Resource names are plural kebab-case, e.g. `/api/v1/flow-categories/{id}`.11- When the domain language treats a multi-word concept as a single word, write it as one word. Document such exceptions to the naming rule explicitly in the project applying it.1213## Versioning1415- The `v{version}` segment is the major version only: `v1`, never `v1.2`.16- It is required for public APIs, where we do not control all clients; optional for internal APIs, where all clients can easily be moved along with API changes.1718## Methods and status codes1920- `GET` is safe; `PUT` and `DELETE` are idempotent; `POST` creates or executes.21- `400 Bad Request` for validation failures; `409 Conflict` for requests that conflict with the resource's current state.2223## Collections2425- The resource root `/api/v{version}/{entities}` is a collection listing all resources of that type.26- If an unfiltered listing is inappropriate, the collection takes query parameters to filter; an unfiltered request returns `400 Bad Request`, and the error names the required filters.27- There is no pagination convention — deliberately, not as an oversight. Introduce one only when scale demands it.2829## Ownership and nesting3031- If an entity owns a collection of sub-resources (deleting the parent deletes them), represent it as a sub-collection under the parent: `/api/v{version}/{parents}/{id}/{children}`.32- Cap owned sub-collections at one level under the parent unless there is a documented reason to go deeper.33- If an entity holds a collection of entities it does not own (independent lifecycle), those entities are their own root collection. In the referencing entity's representation they appear only as **refs** — the minimal representation of a resource: its id, plus a display name where appropriate.3435## Resource shape3637Related data appears in a representation in one of two forms: **embedded** (the full representation, inline) or as a **ref** (id plus display name). Owned sub-resources may be embedded; non-owned resources appear only as refs. Example — `GET /api/v1/flow-instances/42`:3839```json40{41 "id": 42,42 "name": "Purchased aluminium scrap",43 "flowCategory": { "id": "ALU07", "name": "Aluminium" },44 "processes": [45 { "id": 17, "name": "Remelting" },46 { "id": 23, "name": "Casting" }47 ],48 "flowDetails": [49 { "id": 901, "year": 2025, "inputAmount": 120.5, "unit": "tonne" },50 { "id": 902, "year": 2026, "inputAmount": 98.0, "unit": "tonne" }51 ]52}53```5455- `flowCategory` and `processes` are refs — non-owned resources living in their own root collections. `processes` shows refs in a collection-valued property.56- `flowDetails` is the owned sub-collection, embedded in full — the same resources addressable at `/api/v1/flow-instances/42/flow-details`.57- A ref may optionally carry a self link where that helps clients discover resources: `{ "id": "ALU07", "name": "Aluminium", "links": [{ "rel": "self", "href": "/api/v1/flow-categories/ALU07" }] }`.58- Complete self-discoverability and navigability (full HATEOAS) is a non-goal; individual HATEOAS concepts — like self links — are adopted where they are useful.5960## Operations6162- An operation on the domain model is its own resource, suffixed `-operation`, e.g. `/api/v1/migrate-flow-category-operations`.63- Operation names are verb-first and keep the `-operation` suffix even where a noun name (e.g. `/flow-category-migrations`) would read more naturally — self-declaring and greppable wins.64- Operations may likewise be organized under an `/operations` sub-resource, and deeper, when it makes sense.65- An operation may span several domain objects, of the same or of different types, when appropriate.66- An operation has its own id and is referenceable by URL when its execution must be referenced afterwards (async status, audit/history); otherwise it executes without one.67- `POST` to the operation resource executes it, creating an execution at `/{id}` when the operation has ids.68- A long-running operation with an id returns `202 Accepted` with a `Location` header pointing at the execution; its status is read from that URL.69- An operation has its own response, with the relevant resources embedded or linked.7071## Reports7273- A request that returns data from multiple entities/resources is its own top-level resource, suffixed `-report`, e.g. `/api/v1/flow-instance-data-report`.74- Report names are **singular**, unlike collections: a report is a parameterized singleton view — nothing is enumerated and there is no `/{id}`. If a report needs persisted, addressable runs, model it as an operation with ids instead.75- Reports may be organized under a `/reports` sub-resource, and deeper when needed, e.g. `/api/v1/reports/flow-details/process-report?…`. The grouping prefix stays plural; the leaf names one report.76- A report is read-only: `GET`, with query parameters selecting its content. A request missing required parameters returns `400 Bad Request` naming them.77- A report resource may contain redundant data — values copied from other resources or entities — to be concise and efficient.78- That does not displace embedding or referencing related entities where it makes sense: if nearly all of a resource's properties would be copied into the report, consider embedding the resource instead.