Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
17db15d
docs: ETU-74448: Updated guidelines on pagination.
rikard-swahn Aug 13, 2026
fad87ac
docs: ETU-74448: Stricter requirements.
rikard-swahn Aug 13, 2026
c8d1ea1
docs: ETU-74448: Stricter requirements.
rikard-swahn Aug 13, 2026
450368f
docs: ETU-74448: Stricter requirements, and typo fixes.
rikard-swahn Aug 13, 2026
4e73cab
docs: ETU-74448: Typo.
rikard-swahn Aug 13, 2026
d54aae0
docs: ETU-74448: Updated guidelines on pagination.
rikard-swahn Aug 14, 2026
cb42557
docs: ETU-74448: Updated guidelines on pagination.
rikard-swahn Aug 14, 2026
7dc9cc6
docs: ETU-74448: Moved guidelines.md back to root.
rikard-swahn Aug 14, 2026
9e19139
docs: ETU-74448: Clarification on embedding paging, filtering etc in …
rikard-swahn Aug 25, 2026
da33575
docs: ETU-74448: Clarification on cursor pagingation preference.
rikard-swahn Aug 25, 2026
78abfea
docs: ETU-74448: Renamed param "size" -> "pageSize"
rikard-swahn Aug 25, 2026
e8e9339
docs: ETU-74448: MUST be named items
rikard-swahn Aug 25, 2026
4fce6b7
docs: ETU-74448: Renamed totalCount -> totalItems
rikard-swahn Aug 25, 2026
b7c47ef
docs: ETU-74448: Added pageSize and totalPages response fields.
rikard-swahn Aug 25, 2026
bdfe5be
docs: ETU-74448: Corrected description of cursor: it does not have to…
rikard-swahn Aug 25, 2026
723fae1
docs: ETU-74448: Improved cursor description.
rikard-swahn Aug 25, 2026
65b7d5d
docs: ETU-74448: Corrected description of cursor.
rikard-swahn Aug 26, 2026
e0614a3
docs: ETU-74448: Added examplpes for "Cursor key selection" and simpl…
rikard-swahn Aug 26, 2026
b7888ae
docs: ETU-74448: Improved Encoding section with regards to Base64 and…
rikard-swahn Aug 26, 2026
0909a90
docs: ETU-74448: Removed hasMore from cursor response format.
rikard-swahn Aug 26, 2026
57f633d
docs: ETU-74448: Removed NoSQL-line in Choosing a Strategy, not reall…
rikard-swahn Aug 26, 2026
c68cc48
docs: ETU-74448: Revised rule of thumb text for Choosing a Strategy.
rikard-swahn Aug 26, 2026
1cb71ff
docs: ETU-74448: Offset pagination use offset + limit instead of page…
rikard-swahn Aug 27, 2026
233b28e
docs: ETU-74448: Cleanup
rikard-swahn Aug 28, 2026
17c9ab3
docs: ETU-74448: Added SHOULD about default and max for limit.
rikard-swahn Aug 28, 2026
f28cca4
docs: ETU-74448: Corrected pagination parameter names.
rikard-swahn Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions doc/pagination-and-sorting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Pagination and Sorting

## Pagination

When implementing pagination, you **MUST** use either Cursor Pagination (preferred) or Offset Pagination, on the formats detailed below.

### Offset Pagination

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` |

Implementations **SHOULD** implement and document default and max values for `limit`.

**Example request:**

```http
GET /api/v1/bus-stops?city=Oslo&offset=10&limit=20
```

#### Response format

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` |
| `limit` | integer | The requested `limit`, or max limit if given `limit` was over max. |

### Cursor / Keyset Pagination

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-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.

On the next request from the client, the cursor is sent back to the service.
The service returns the requested items and calculates a new cursor. In this way, the client can paginate through items.

Clients should not inspect or parse cursors - a cursor should be treated as an opaque string with an unknown and possibly changing format.

**Example requests:**

First request (no cursor available to client yet):
```http
GET /api/v1/bus-stops?city=Oslo&pageSize=20
```
The response includes a cursor for the next page. To fetch the next page:
```http
GET /api/v1/bus-stops?city=Oslo&pageSize=20&cursor=eyJpZCI6MTAwfQ
```

#### 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.

Example cursor with multiple values:

```json
{
"id": "fa760939-dacc-4653-be5b-bfe6e87d9fcf",
"sort": "name"
}
```

Example cursor key for encoding a single value (e.g. database id):
```
100
```


#### 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

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` |


### Choosing a Strategy

Use the comparison table below to select the pagination strategy that best fits your use case.

| Criterion | Offset Pagination | Cursor Pagination |
|------------------------------------|--------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
| **Ease of use** | Widely understood; broad framework support | Less familiar to most clients; a bit more work on the server side |
| **Jump to arbitrary position** | ✅ Supported | ❌ Not supported — only sequential traversal |
| **Consistency under data changes** | ⚠️ Inserts/deletes between requests may cause duplicates or missing items | ✅ Stable — cursor anchors position in the data set |
| **Performance on large data sets** | ⚠️ `OFFSET` queries degrade as offset gets bigger, because the database must scan and discard all rows before the offset | ✅ Constant-time lookups |

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
Comment thread
rikard-swahn marked this conversation as resolved.
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
```
27 changes: 16 additions & 11 deletions guidelines.md
Comment thread
rikard-swahn marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ owner |`string`|**REQUIRED**. The Entur team responsible for this specificat
parentId |`string`|Id of the parent specification, used when merging. [Read more](#243-merging-specifications).

Example:
```
```json
{
"info": {
"x-entur-metadata": {
Expand Down Expand Up @@ -390,7 +390,7 @@ Example:
- :eyes: Response: Always serialized with the standard number of decimals. For NOK, this means 2, since øre is the smallest unit.

Example:
```
```json
{
"amount": "99.00"
"currency": "NOK"
Expand Down Expand Up @@ -426,18 +426,23 @@ A "de-facto" standard for correlating a request throughout a microservice archit
<!-- More complex design patterns -->


### 6.1 Filtering, Sorting & Pagination
- :eyes: You **MAY** allow filtering, sorting, and pagination to retrieve specific data
- :eyes: If you implement pagination, you **MUST** use either query parameters "page" (zero based page to get) and "size" (number of items per page),
**OR** query parameters offset (zero based) and limit (number of items)
- :eyes: If you implement sorting, you **SHOULD** use query parameter "sort". Sorting can be done on multiple levels, and sort order (desc / asc) is also specified, like so: `sort=<field1>,<asc|desc>&sort=<field2>,<asc|desc>`
- **TODO**: Requirements for response format for pagination and sorting
### 6.1 Pagination and Sorting
Comment thread
rikard-swahn marked this conversation as resolved.

The requirements above are based on the Spring way of doing things: https://docs.spring.io/spring-data/rest/reference/paging-and-sorting.html
:eyes: If you implement pagination, you **MUST** use one of these approaches:
- Offset pagination with query parameters `offset` and `limit`:
```http
GET /api/v1/bus-stops?city=Oslo&offset=10&limit=20
```

Example:
> GET /api/v1/bus-stops?city=Oslo&sort=name,asc&sort=something,desc&page=0&size=20
- Cursor / Keyset Pagination with query parameters `cursor` and `pageSize`:
```http
GET /api/v1/bus-stops?city=Oslo&cursor=eyJpZCI6MTAwfQ&pageSize=20
```

: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 [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
Expand Down
Loading