From 1da1ae7739f1dde1e0c1ce7f3371c6938fb5345a Mon Sep 17 00:00:00 2001 From: rikard Date: Mon, 21 Sep 2026 09:49:57 +0200 Subject: [PATCH 01/16] docs: ETU-76297: Added guidelines for sorting, and improved pagination with examples. --- ...agination-and-sorting.md => pagination.md} | 67 +++++++---- doc/sorting.md | 107 ++++++++++++++++++ guidelines.md | 4 +- 3 files changed, 156 insertions(+), 22 deletions(-) rename doc/{pagination-and-sorting.md => pagination.md} (88%) create mode 100644 doc/sorting.md diff --git a/doc/pagination-and-sorting.md b/doc/pagination.md similarity index 88% rename from doc/pagination-and-sorting.md rename to doc/pagination.md index 73f7262..5591bef 100644 --- a/doc/pagination-and-sorting.md +++ b/doc/pagination.md @@ -1,10 +1,8 @@ -# Pagination and Sorting - -## Pagination +# Pagination When implementing pagination, you **MUST** use either Cursor Pagination (preferred) or Offset Pagination, on the formats detailed below. -### Offset Pagination +## Offset Pagination This strategy is based on these query parameters: @@ -21,7 +19,7 @@ Implementations **SHOULD** implement and document default and max values for `li GET /api/v1/bus-stops?city=Oslo&offset=10&limit=20 ``` -#### Response format +### Response format The response **MUST** contain the following fields: @@ -31,7 +29,26 @@ The response **MUST** contain the following fields: | `totalItems` | integer | The total number of items across all pages. **MUST** be named `totalItems` | | `limit` | integer | The requested `limit`, or max limit if given `limit` was over max. | -### Cursor / Keyset Pagination +**Example** + +```json +{ + "items": [ + { + "id": "100", + "name": "Item 100" + }, + { + "id": "101", + "name": "Item 101" + } + ], + "totalItems": 2, + "limit": 100 +} +``` + +## Cursor / Keyset Pagination This strategy is based on these query parameters: @@ -59,7 +76,7 @@ The response includes a cursor for the next page. To fetch the next page: GET /api/v1/bus-stops?city=Oslo&pageSize=20&cursor=eyJpZCI6MTAwfQ ``` -#### Cursor key selection +### Cursor key selection The cursor **MUST** encode a value (or set of values) that uniquely and stably identifies a position in the sorted result set. @@ -78,12 +95,12 @@ Example cursor key for encoding a single value (e.g. database id): ``` -#### Encoding +### Encoding The cursor **MUST** be URL-safe (no URL-encoding required). Because the cursor should be opaque to the client and may contain internal details, it **MAY** be Base64 encoded. For cursors with multiple values, a common solution is to have JSON in string value and then Base64-encode the string. If the cursor contains data that you do not want to expose, the cursor **MAY** be encrypted and then Base64 encoded. -#### Response format +### Response format The response **MUST** contain the following fields: @@ -93,7 +110,25 @@ The response **MUST** contain the following fields: | `cursor` | string | An opaque string pointing to next item to get. If no more items, cursor value is not returned to client. **MUST** be named `cursor` | -### Choosing a Strategy +**Example** + +```json +{ + "items": [ + { + "id": "100", + "name": "Item 100" + }, + { + "id": "101", + "name": "Item 101" + } + ], + "cursor": "eyJpZCI6MTAwfQ" +} +``` + +## Choosing a Strategy Use the comparison table below to select the pagination strategy that best fits your use case. @@ -107,14 +142,4 @@ Use the comparison table below to select the pagination strategy that best fits As a rule of thumb, cursor pagination **SHOULD** be used unless: offset pagination DB queries are not too heavy and inserts and deletes are infrequent OR jumping to a specific position must be supported. ## Sorting -Sorting **MAY** be implemented without pagination, but when using pagination you **MUST** also use sorting. - -:eyes: If you implement sorting, you **MUST** use query parameter `sort`. -You **MAY** also allow sorting on multiple levels, and allow specifying sort order (desc / asc). -In your service, always use a secondary sorting on a unique id, so that two entries with the same primary sorting -(e.g. created date) are always sorted in the same order. - -Example: -```http -GET /api/v1/bus-stops?city=Oslo&sort=name,asc&sort=something,desc -``` \ No newline at end of file +When using pagination you **MUST** also use [sorting](sorting.md). \ No newline at end of file diff --git a/doc/sorting.md b/doc/sorting.md new file mode 100644 index 0000000..755a952 --- /dev/null +++ b/doc/sorting.md @@ -0,0 +1,107 @@ +# Sorting + +If you implement sorting, you **MUST** use the query parameter `sort`. + +| Parameter | Type | Description | +|-----------|--------|-----------------------------------------------------| +| `sort` | string | The field(s) to sort on. **MUST** be named `sort`. | + +Example: +```http +GET /api/v1/bus-stops?city=Oslo&sort=name +``` + +## Sort field and direction + +A `sort` value is a field name, optionally followed by a comma (`,`) and a +sort direction — either `asc` (ascending) or `desc` (descending): + +```http +GET /api/v1/bus-stops?city=Oslo&sort=name,asc +``` + +- If the direction is omitted, the direction **MUST** default to `asc`. +- The direction tokens `asc` and `desc` **MUST** be treated as + case-insensitive. +- The comma separates a **field from its direction**. A single `sort` value + **MUST NOT** contain more than one field (i.e. `sort=name,created` is + invalid). To sort on multiple fields, see below. + +## Sorting on multiple fields + +You **MAY** allow sorting on multiple fields by repeating the `sort` parameter: + +```http +GET /api/v1/bus-stops?city=Oslo&sort=name&sort=created +``` + +Each field **MAY** have its own direction: + +```http +GET /api/v1/bus-stops?city=Oslo&sort=name,asc&sort=created,desc +``` + +When multiple `sort` parameters are given, the **order of the parameters +defines the sort precedence**. The first `sort` is the primary sort key, the +second is the secondary key (used to break ties in the first), and so on. + +## Allowed sort fields + +You **MUST** document which fields are sortable. + +## Default sort order + +You **SHOULD** define and document a default sort order that is applied when +the client does not provide a `sort` parameter. The default order **MUST** be +deterministic and stable across identical requests. + +## Deterministic and stable ordering + +The result order **MUST** be deterministic: if the same request is made again +(with unchanged data), the items **MUST** be returned in the same order. + +To achieve this, the backend **MUST** apply a final tiebreaker on a unique, +stable field (typically `id`) whenever the requested sort fields do not by +themselves guarantee a total ordering. + +## Case sensitivity, collation and null ordering + +- For string fields, the API **SHOULD** document whether sorting is + case-sensitive and which collation/locale is used. For Norwegian data, + sorting **SHOULD** order the letters `æ`, `ø` and `å` according to Norwegian + collation rules. +- The API **SHOULD** document where `null` or missing values are placed + (sorted first or last). + +## Response format + +**Example** + +```http +GET /item/v1/items?sort=name,asc +``` + +```json +{ + "items": [ + { + "id": "100", + "name": "Item A" + }, + { + "id": "101", + "name": "Item A" + }, + { + "id": "99", + "name": "Item B" + } + ] +} +``` + +In this example, the client requested items sorted on `name` ascending. There +are two items with the name "Item A" but the backend applies a secondary sort on `id`, so they are +always returned in the same order (`100` before `101`). The item with `id` 99 +is still returned last, because the primary sort is on `name` and "Item B" +sorts after "Item A". diff --git a/guidelines.md b/guidelines.md index 10ac66b..befeada 100644 --- a/guidelines.md +++ b/guidelines.md @@ -520,10 +520,12 @@ A "de-facto" standard for correlating a request throughout a microservice archit GET /api/v1/bus-stops?city=Oslo&cursor=eyJpZCI6MTAwfQ&pageSize=20 ``` +See [pagination](doc/pagination.md) for more details. + :eyes: If you implement sorting, you **MUST** use query parameter `sort`: > GET /api/v1/bus-stops?city=Oslo&sort=name,asc&sort=something,desc +See [sorting](doc/sorting.md) for more details. -See [pagination and sorting](doc/pagination-and-sorting.md) for more details. ### 6.2 Partial Responses - :eyes: You **MAY** let clients choose which fields to include to reduce data transfer From 8593edb34c78cbc68805e680a7ffc88aa5bba89d Mon Sep 17 00:00:00 2001 From: rikard Date: Mon, 21 Sep 2026 10:16:32 +0200 Subject: [PATCH 02/16] docs: ETU-76297: "Response format" is no longer a separate section, but instead the response format example was moved to "Deterministic and stable ordering". --- doc/sorting.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/doc/sorting.md b/doc/sorting.md index 755a952..16cc81e 100644 --- a/doc/sorting.md +++ b/doc/sorting.md @@ -55,15 +55,6 @@ You **SHOULD** define and document a default sort order that is applied when the client does not provide a `sort` parameter. The default order **MUST** be deterministic and stable across identical requests. -## Deterministic and stable ordering - -The result order **MUST** be deterministic: if the same request is made again -(with unchanged data), the items **MUST** be returned in the same order. - -To achieve this, the backend **MUST** apply a final tiebreaker on a unique, -stable field (typically `id`) whenever the requested sort fields do not by -themselves guarantee a total ordering. - ## Case sensitivity, collation and null ordering - For string fields, the API **SHOULD** document whether sorting is @@ -73,7 +64,14 @@ themselves guarantee a total ordering. - The API **SHOULD** document where `null` or missing values are placed (sorted first or last). -## Response format +## Deterministic and stable ordering + +The result order **MUST** be deterministic: if the same request is made again +(with unchanged data), the items **MUST** be returned in the same order. + +To achieve this, the backend **MUST** apply a final tiebreaker on a unique, +stable field (typically `id`) whenever the requested sort fields do not by +themselves guarantee a total ordering. **Example** From d2075be1425555bbd994c2d74cde7b9de83180b7 Mon Sep 17 00:00:00 2001 From: rikard Date: Wed, 23 Sep 2026 11:29:33 +0200 Subject: [PATCH 03/16] docs: ETU-76297: Removed unnecessary filtering in examples. --- doc/sorting.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/sorting.md b/doc/sorting.md index 16cc81e..b6a9973 100644 --- a/doc/sorting.md +++ b/doc/sorting.md @@ -8,7 +8,7 @@ If you implement sorting, you **MUST** use the query parameter `sort`. Example: ```http -GET /api/v1/bus-stops?city=Oslo&sort=name +GET /api/v1/bus-stops?sort=name ``` ## Sort field and direction @@ -17,7 +17,7 @@ A `sort` value is a field name, optionally followed by a comma (`,`) and a sort direction — either `asc` (ascending) or `desc` (descending): ```http -GET /api/v1/bus-stops?city=Oslo&sort=name,asc +GET /api/v1/bus-stops?sort=name,asc ``` - If the direction is omitted, the direction **MUST** default to `asc`. @@ -32,13 +32,13 @@ GET /api/v1/bus-stops?city=Oslo&sort=name,asc You **MAY** allow sorting on multiple fields by repeating the `sort` parameter: ```http -GET /api/v1/bus-stops?city=Oslo&sort=name&sort=created +GET /api/v1/bus-stops?sort=name&sort=created ``` Each field **MAY** have its own direction: ```http -GET /api/v1/bus-stops?city=Oslo&sort=name,asc&sort=created,desc +GET /api/v1/bus-stops?sort=name,asc&sort=created,desc ``` When multiple `sort` parameters are given, the **order of the parameters From ad46e482f60894ff67e9b895d5ca7b84445af63d Mon Sep 17 00:00:00 2001 From: rikard Date: Wed, 23 Sep 2026 11:30:21 +0200 Subject: [PATCH 04/16] docs: ETU-76297: Comma instead of em-dash. --- doc/sorting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sorting.md b/doc/sorting.md index b6a9973..2543a0c 100644 --- a/doc/sorting.md +++ b/doc/sorting.md @@ -14,7 +14,7 @@ GET /api/v1/bus-stops?sort=name ## Sort field and direction A `sort` value is a field name, optionally followed by a comma (`,`) and a -sort direction — either `asc` (ascending) or `desc` (descending): +sort direction, either `asc` (ascending) or `desc` (descending): ```http GET /api/v1/bus-stops?sort=name,asc From 2993418ecc5488d21d41fff3d865d556c2838b4c Mon Sep 17 00:00:00 2001 From: rikard Date: Wed, 23 Sep 2026 11:32:02 +0200 Subject: [PATCH 05/16] docs: ETU-76297: Removed redundant doc. --- doc/sorting.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/sorting.md b/doc/sorting.md index 2543a0c..9a2d979 100644 --- a/doc/sorting.md +++ b/doc/sorting.md @@ -23,9 +23,6 @@ GET /api/v1/bus-stops?sort=name,asc - If the direction is omitted, the direction **MUST** default to `asc`. - The direction tokens `asc` and `desc` **MUST** be treated as case-insensitive. -- The comma separates a **field from its direction**. A single `sort` value - **MUST NOT** contain more than one field (i.e. `sort=name,created` is - invalid). To sort on multiple fields, see below. ## Sorting on multiple fields From f0083ae9debcedec0e49ee40dcde8237cbd80cd9 Mon Sep 17 00:00:00 2001 From: rikard Date: Wed, 23 Sep 2026 11:44:47 +0200 Subject: [PATCH 06/16] docs: ETU-76297: Improved Sorting section. --- doc/pagination.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/pagination.md b/doc/pagination.md index 5591bef..02eae34 100644 --- a/doc/pagination.md +++ b/doc/pagination.md @@ -142,4 +142,7 @@ Use the comparison table below to select the pagination strategy that best fits As a rule of thumb, cursor pagination **SHOULD** be used unless: offset pagination DB queries are not too heavy and inserts and deletes are infrequent OR jumping to a specific position must be supported. ## Sorting -When using pagination you **MUST** also use [sorting](sorting.md). \ No newline at end of file + +When using pagination you **MUST** return elements in a stable order, meaning the same request always results in a response where the items are in the same order. +Without a stable order, the boundary between pages is undefined: the same item may appear on multiple pages or be skipped entirely as the client paginates, and cursors can no longer reliably point to "the next item". +See [sorting](sorting.md) for more details. Note, however, that pagination does not require you to let clients control sort order, only that the order is stable for a given request. From 3760355510949eed77cba3362bc4f530e574aa37 Mon Sep 17 00:00:00 2001 From: rikard Date: Wed, 23 Sep 2026 11:46:08 +0200 Subject: [PATCH 07/16] docs: ETU-76297: Removed redundant doc. --- doc/sorting.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/doc/sorting.md b/doc/sorting.md index 9a2d979..f8dcd09 100644 --- a/doc/sorting.md +++ b/doc/sorting.md @@ -38,10 +38,6 @@ Each field **MAY** have its own direction: GET /api/v1/bus-stops?sort=name,asc&sort=created,desc ``` -When multiple `sort` parameters are given, the **order of the parameters -defines the sort precedence**. The first `sort` is the primary sort key, the -second is the secondary key (used to break ties in the first), and so on. - ## Allowed sort fields You **MUST** document which fields are sortable. From ed2cedcf5adb7c64ad8fe127b580fd571fca9390 Mon Sep 17 00:00:00 2001 From: rikard Date: Thu, 24 Sep 2026 07:51:10 +0200 Subject: [PATCH 08/16] docs: ETU-76297: Removed redundant " MUST be named ...". --- doc/pagination.md | 16 ++++++++-------- doc/sorting.md | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/pagination.md b/doc/pagination.md index 02eae34..85e276e 100644 --- a/doc/pagination.md +++ b/doc/pagination.md @@ -8,8 +8,8 @@ This strategy is based on these query parameters: | Parameter | Type | Description | |-----------|---------|----------------------------------------------------------------------------| -| `offset` | integer | Zero-based index of the first item to retrieve. **MUST** be named `offset` | -| `limit` | integer | Number of items to get. **MUST** be named `limit` | +| `offset` | integer | Zero-based index of the first item to retrieve. | +| `limit` | integer | Number of items to get. | Implementations **SHOULD** implement and document default and max values for `limit`. @@ -25,8 +25,8 @@ The response **MUST** contain the following fields: | Parameter | Type | Description | |--------------|---------|----------------------------------------------------------------------------| -| `items` | array | **MUST** be named `items`. | -| `totalItems` | integer | The total number of items across all pages. **MUST** be named `totalItems` | +| `items` | array | Returned items. | +| `totalItems` | integer | The total number of items across all pages. | | `limit` | integer | The requested `limit`, or max limit if given `limit` was over max. | **Example** @@ -54,8 +54,8 @@ This strategy is based on these query parameters: | Parameter | Type | Description | |-----------|---------|----------------------------------------------------------------------------------------| -| `cursor` | string | An opaque string identifying the next page of items to get. **MUST** be named `cursor` | -| `pageSize` | integer | Number of items per page. **MUST** be named `pageSize` | +| `cursor` | string | An opaque string identifying the next page of items to get. | +| `pageSize` | integer | Number of items per page. | Cursor-based pagination is based on a `cursor` that is created when handling requests from the client. The cursor is returned to the client in the response body. The cursor points to the next page of items. Sorting parameters, `pageSize` and filters **MAY** also be embedded in the cursor. @@ -106,8 +106,8 @@ The response **MUST** contain the following fields: | Parameter | Type | Description | |-----------|---------|-------------------------------------------------------------------------------------------------------------------------------------| -| `items` | array | **MUST** be named `items`. | -| `cursor` | string | An opaque string pointing to next item to get. If no more items, cursor value is not returned to client. **MUST** be named `cursor` | +| `items` | array | Returned items. | +| `cursor` | string | An opaque string pointing to next item to get. If no more items, cursor value is not returned to client. | **Example** diff --git a/doc/sorting.md b/doc/sorting.md index f8dcd09..f3323b7 100644 --- a/doc/sorting.md +++ b/doc/sorting.md @@ -4,7 +4,7 @@ If you implement sorting, you **MUST** use the query parameter `sort`. | Parameter | Type | Description | |-----------|--------|-----------------------------------------------------| -| `sort` | string | The field(s) to sort on. **MUST** be named `sort`. | +| `sort` | string | The field(s) to sort on. | Example: ```http From 52ad6137477c5daaf06c18494354d7e7eed19ffa Mon Sep 17 00:00:00 2001 From: rikard Date: Thu, 24 Sep 2026 08:18:50 +0200 Subject: [PATCH 09/16] docs: ETU-76297: Simplified Sorting section and referring to sorting.md instead. --- doc/pagination.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/pagination.md b/doc/pagination.md index 85e276e..368b4ab 100644 --- a/doc/pagination.md +++ b/doc/pagination.md @@ -143,6 +143,6 @@ As a rule of thumb, cursor pagination **SHOULD** be used unless: offset paginati ## Sorting -When using pagination you **MUST** return elements in a stable order, meaning the same request always results in a response where the items are in the same order. -Without a stable order, the boundary between pages is undefined: the same item may appear on multiple pages or be skipped entirely as the client paginates, and cursors can no longer reliably point to "the next item". -See [sorting](sorting.md) for more details. Note, however, that pagination does not require you to let clients control sort order, only that the order is stable for a given request. +When using pagination you **MUST** return elements in a deterministic order. Without a stable order, the boundary between pages is undefined: the same +item may appear on multiple pages or be skipped entirely as the client paginates, and cursors can no longer reliably point to "the next item". +See [sorting](sorting.md) for more details. From 23b706f98b721d69cbbfa8aef5d015c45205680d Mon Sep 17 00:00:00 2001 From: rikard Date: Thu, 24 Sep 2026 08:24:07 +0200 Subject: [PATCH 10/16] docs: ETU-76297: Language cleanup in Sorting section. --- doc/pagination.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/pagination.md b/doc/pagination.md index 368b4ab..14cbc04 100644 --- a/doc/pagination.md +++ b/doc/pagination.md @@ -143,6 +143,5 @@ As a rule of thumb, cursor pagination **SHOULD** be used unless: offset paginati ## Sorting -When using pagination you **MUST** return elements in a deterministic order. Without a stable order, the boundary between pages is undefined: the same -item may appear on multiple pages or be skipped entirely as the client paginates, and cursors can no longer reliably point to "the next item". -See [sorting](sorting.md) for more details. +When using pagination you **MUST** return elements in a deterministic order. Without one, the boundary between pages is undefined: the same item may appear on multiple pages or be skipped entirely as the client paginates, and cursors can no longer reliably point to the next page. +See [sorting](sorting.md) for more details. From 71d0808db33a7a02da13f25be299452ac5199768 Mon Sep 17 00:00:00 2001 From: rikard Date: Thu, 24 Sep 2026 08:50:51 +0200 Subject: [PATCH 11/16] docs: ETU-76297: Introduction now defines "sorting", and Tiebreaking is a separate section. "Default sort order" moved to end. --- doc/sorting.md | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/doc/sorting.md b/doc/sorting.md index f3323b7..69bc874 100644 --- a/doc/sorting.md +++ b/doc/sorting.md @@ -1,6 +1,13 @@ # Sorting -If you implement sorting, you **MUST** use the query parameter `sort`. +When an API response is sorted, it means that the order is deterministic: the same +request **MUST** return items in the same order. +Note that a sorted response does not require that the client can control the sorting; the endpoint +may return items in a predetermined order. + +## Client-controlled sorting + +Client-controlled sorting **MUST** use the query parameter `sort`. | Parameter | Type | Description | |-----------|--------|-----------------------------------------------------| @@ -11,7 +18,7 @@ Example: GET /api/v1/bus-stops?sort=name ``` -## Sort field and direction +### Sort field and direction A `sort` value is a field name, optionally followed by a comma (`,`) and a sort direction, either `asc` (ascending) or `desc` (descending): @@ -24,7 +31,7 @@ GET /api/v1/bus-stops?sort=name,asc - The direction tokens `asc` and `desc` **MUST** be treated as case-insensitive. -## Sorting on multiple fields +### Sorting on multiple fields You **MAY** allow sorting on multiple fields by repeating the `sort` parameter: @@ -38,17 +45,11 @@ Each field **MAY** have its own direction: GET /api/v1/bus-stops?sort=name,asc&sort=created,desc ``` -## Allowed sort fields +### Allowed sort fields You **MUST** document which fields are sortable. -## Default sort order - -You **SHOULD** define and document a default sort order that is applied when -the client does not provide a `sort` parameter. The default order **MUST** be -deterministic and stable across identical requests. - -## Case sensitivity, collation and null ordering +### Case sensitivity, collation and null ordering - For string fields, the API **SHOULD** document whether sorting is case-sensitive and which collation/locale is used. For Norwegian data, @@ -57,14 +58,11 @@ deterministic and stable across identical requests. - The API **SHOULD** document where `null` or missing values are placed (sorted first or last). -## Deterministic and stable ordering - -The result order **MUST** be deterministic: if the same request is made again -(with unchanged data), the items **MUST** be returned in the same order. -To achieve this, the backend **MUST** apply a final tiebreaker on a unique, -stable field (typically `id`) whenever the requested sort fields do not by -themselves guarantee a total ordering. +### Tiebreaking +To achieve a deterministic order, the sorting implementation **MUST** include sorting on a unique, +stable field (typically `id`). This means for example that if client requests sorting on `name`, and +`name` is not unique across all items, a secondary sort must also be applied on a unique field. **Example** @@ -96,3 +94,8 @@ are two items with the name "Item A" but the backend applies a secondary sort on always returned in the same order (`100` before `101`). The item with `id` 99 is still returned last, because the primary sort is on `name` and "Item B" sorts after "Item A". + +## Default sort order +You **SHOULD** define and document a default sort order that is applied when +the client does not provide a `sort` parameter. +If the default order is **not** deterministic, that **MUST** also be documented. From 4a7dbca36dc846c5e6325e1cb36add8a5541bbde Mon Sep 17 00:00:00 2001 From: rikard Date: Thu, 24 Sep 2026 10:17:44 +0200 Subject: [PATCH 12/16] docs: ETU-76297: Added OpenAPI documentation examples. --- doc/sorting.md | 74 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/doc/sorting.md b/doc/sorting.md index 69bc874..cef9dc9 100644 --- a/doc/sorting.md +++ b/doc/sorting.md @@ -2,8 +2,6 @@ When an API response is sorted, it means that the order is deterministic: the same request **MUST** return items in the same order. -Note that a sorted response does not require that the client can control the sorting; the endpoint -may return items in a predetermined order. ## Client-controlled sorting @@ -60,9 +58,10 @@ You **MUST** document which fields are sortable. ### Tiebreaking -To achieve a deterministic order, the sorting implementation **MUST** include sorting on a unique, -stable field (typically `id`). This means for example that if client requests sorting on `name`, and -`name` is not unique across all items, a secondary sort must also be applied on a unique field. +To achieve a deterministic order, the sorting implementation **MUST** append a unique +field with a stable value (typically `id`) as a final tiebreaker. For example, if a +client requests sorting on `name` and `name` is not unique across all items, a secondary +sort **MUST** also be applied on a unique field. **Example** @@ -95,7 +94,64 @@ always returned in the same order (`100` before `101`). The item with `id` 99 is still returned last, because the primary sort is on `name` and "Item B" sorts after "Item A". -## Default sort order -You **SHOULD** define and document a default sort order that is applied when -the client does not provide a `sort` parameter. -If the default order is **not** deterministic, that **MUST** also be documented. +### Documenting in OpenAPI + +The documentation requirements above (allowed sort fields, default order, case +sensitivity, collation and null ordering) **MUST** be expressed on the `sort` +query parameter in your OpenAPI specification. + +**Example** +```json +{ + "parameters": [ + { + "name": "sort", + "in": "query", + "description": "Field(s) to sort on. Repeat the parameter to sort on multiple fields. Each value is a field name optionally followed by a direction: `,`.\n\n**Sortable fields:** `name`, `created`.\n\n**Collation:** String fields are sorted case-insensitively using Norwegian collation.\n\n**Null ordering:** Items with a `null` or missing value for the sort field are placed last, regardless of sort direction.", + "schema": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(name|created)(,(asc|desc))?$" + }, + "default": ["name,asc"], + "example": ["name,asc", "created,desc"] + } + } + ] +} +``` + +## Sorting not controlled by client +A sorted response does not require that the client can control the sorting; the endpoint +may return items in a predetermined order. When the order is fixed, you +**MUST** document it on the response field for the collection: + +**Example** +```json +{ + "items": { + "type": "array", + "description": "Sorted by `name` ascending, with `id` as a tiebreaker. This ordering is fixed and cannot be changed by the client.", + "items": { + "$ref": "#/components/schemas/Item" + } + } +} +``` + +## Non-sorted responses +If a returned collection is not sorted, that **MUST** also be documented: + +**Example** +```json +{ + "items": { + "type": "array", + "description": "The order is undefined and **MUST NOT** be relied upon; it may change between requests.", + "items": { + "$ref": "#/components/schemas/Item" + } + } +} +``` \ No newline at end of file From 4b86327e5002ed9246c2fff421edb49c93ff8a32 Mon Sep 17 00:00:00 2001 From: rikard Date: Mon, 28 Sep 2026 08:15:40 +0200 Subject: [PATCH 13/16] docs: ETU-76297: Returned collections must be sorted. --- doc/sorting.md | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/doc/sorting.md b/doc/sorting.md index cef9dc9..0867fe7 100644 --- a/doc/sorting.md +++ b/doc/sorting.md @@ -1,6 +1,6 @@ # Sorting -When an API response is sorted, it means that the order is deterministic: the same +All returned collections **MUST** be sorted, which it means that the order is deterministic: the same request **MUST** return items in the same order. ## Client-controlled sorting @@ -123,9 +123,8 @@ query parameter in your OpenAPI specification. ``` ## Sorting not controlled by client -A sorted response does not require that the client can control the sorting; the endpoint -may return items in a predetermined order. When the order is fixed, you -**MUST** document it on the response field for the collection: +When returning a collection, there is no requirement that the client can control the sorting. However, the endpoint +**MUST** return items in a predetermined order and you **MUST** document the order on the response field for the collection: **Example** ```json @@ -139,19 +138,3 @@ may return items in a predetermined order. When the order is fixed, you } } ``` - -## Non-sorted responses -If a returned collection is not sorted, that **MUST** also be documented: - -**Example** -```json -{ - "items": { - "type": "array", - "description": "The order is undefined and **MUST NOT** be relied upon; it may change between requests.", - "items": { - "$ref": "#/components/schemas/Item" - } - } -} -``` \ No newline at end of file From 7330df141ffec39cdf8ea615c1a020b82e891861 Mon Sep 17 00:00:00 2001 From: rikard Date: Mon, 28 Sep 2026 08:55:47 +0200 Subject: [PATCH 14/16] docs: ETU-76297: Added section about sorting on all collections. --- guidelines.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/guidelines.md b/guidelines.md index befeada..bca0df6 100644 --- a/guidelines.md +++ b/guidelines.md @@ -522,8 +522,13 @@ A "de-facto" standard for correlating a request throughout a microservice archit See [pagination](doc/pagination.md) for more details. -:eyes: If you implement sorting, you **MUST** use query parameter `sort`: +All returned collections **MUST** be sorted, which it means that the order is deterministic: the same +request **MUST** return items in the same order. + +:eyes: If you implement client-controlled sorting, you **MUST** use query parameter `sort`: > GET /api/v1/bus-stops?city=Oslo&sort=name,asc&sort=something,desc + + See [sorting](doc/sorting.md) for more details. From ee512d213a68d44dcb7b36fdb8bfee338441d53c Mon Sep 17 00:00:00 2001 From: rikard Date: Mon, 28 Sep 2026 08:57:13 +0200 Subject: [PATCH 15/16] docs: ETU-76297: Removed redundant info. --- doc/sorting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sorting.md b/doc/sorting.md index 0867fe7..6654656 100644 --- a/doc/sorting.md +++ b/doc/sorting.md @@ -131,7 +131,7 @@ When returning a collection, there is no requirement that the client can control { "items": { "type": "array", - "description": "Sorted by `name` ascending, with `id` as a tiebreaker. This ordering is fixed and cannot be changed by the client.", + "description": "Sorted by `name` ascending, with `id` as a tiebreaker.", "items": { "$ref": "#/components/schemas/Item" } From c426964eea26caab7cc18bb62c330a54d4340246 Mon Sep 17 00:00:00 2001 From: rikard Date: Mon, 28 Sep 2026 09:00:22 +0200 Subject: [PATCH 16/16] docs: ETU-76297: Removed redundant info. --- doc/sorting.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/sorting.md b/doc/sorting.md index 6654656..ee973b2 100644 --- a/doc/sorting.md +++ b/doc/sorting.md @@ -123,8 +123,7 @@ query parameter in your OpenAPI specification. ``` ## Sorting not controlled by client -When returning a collection, there is no requirement that the client can control the sorting. However, the endpoint -**MUST** return items in a predetermined order and you **MUST** document the order on the response field for the collection: +When returning a collection, there is no requirement that the client can control the sorting. When sorting is not controlled by the client, you **MUST** however document the order on the response field for the collection: **Example** ```json