When a user opens a list of orders, they want to see only their own, only from last month, only the cancelled ones — and preferably sorted by date. All of that is passed through query parameters: the part of the URL after the ? sign.
Let's look at how to name them properly, how to build filters, and how to make large lists load page by page.
How to Name Parameters
Query parameter names are written in camelCase — the same way as fields in JSON.
GET /orders?customerId=123&dateFrom=2026-01-01 ✓
GET /orders?customer_id=123 ✗ — snake_case
GET /orders?CustomerID=123 ✗ — PascalCase
A single convention across the whole API saves clients from confusion: no need to remember where there's an underscore and where there isn't.
Filtering
The simplest way to filter is to pass the field name and value directly:
GET /orders?status=CONFIRMED
GET /orders?customerId=550e8400-e29b-41d4-a716-446655440000
For ranges, add the From and To suffixes:
GET /orders?dateFrom=2026-01-01&dateTo=2026-12-31
GET /orders?amountFrom=100&amountTo=500
The interval includes both ends: from dateFrom inclusive to dateTo inclusive. You can pass only one bound — for example, dateFrom without dateTo means "from this date onward".
Two Kinds of Pagination
When there are thousands of records in the database, you can't return them all in a single request. You need page-by-page loading. There are two approaches, and each has its own area of application.
Offset Pagination — for a Classic UI with Pages
The client says: "give me page number 3, 20 items each". The server skips the first 40 and returns the next 20.
GET /orders?page=1&size=20 ← first page
GET /orders?page=3&size=50 ← third page
An important detail: the first page is page=1, not page=0. Zero-based page numbering inside the code is an implementation detail that must not leak into the public contract.
Spring Data has a ready-made setting for this:
spring:
data:
web:
pageable:
one-indexed-parameters: true
The server's response contains the data itself and pagination information:
{
"content": [
{ "orderId": "...", "status": "CREATED" }
],
"page": 1,
"size": 20,
"totalElements": 243,
"totalPages": 13
}
totalElements and totalPages let the UI draw the "1 2 3 … 13" buttons.
When to use: you need page numbers in the interface, the user wants to jump to page 7, the data changes rarely.
Limitations: with active insertion/deletion of records the pages "drift" — an item may appear twice or disappear. On very large OFFSET values the SQL query slows down.
Cursor Pagination — for Feeds and Infinite Scroll
Instead of a page number, the client receives an opaque token (cursor) and passes it in the next request: "give me 20 records after this point".
GET /orders?size=20 ← first page
GET /orders?size=20&cursor=eyJpZCI6MTAwfQ== ← next
The client doesn't know what's inside the cursor, and shouldn't — it's a Base64 string that the server reads itself. There's no need to construct the cursor yourself: you take the nextCursor value from the response and plug it into the next request.
{
"content": [...],
"size": 20,
"nextCursor": "eyJpZCI6MTIwfQ==",
"prevCursor": "eyJpZCI6MTAwfQ==",
"hasNext": true,
"hasPrev": true
}
When to use: the data changes often (a message feed, notifications), you need infinite scroll, large volumes of data.
Limitations: you can't jump straight to page 7, and you can't find out the total number of records without a separate request.
Sorting
The sort parameter takes a field name and a direction separated by a comma:
GET /orders?sort=createdAt,desc
GET /orders?sort=totalAmount,asc
If you need multi-level sorting, the parameter is repeated:
GET /orders?sort=totalAmount,asc&sort=createdAt,desc
Multi-level sorting should be applied with care: composite indexes in the database must match the order of the fields.
Full-Text Search
For free-form text search the q parameter is used:
GET /products?q=keyboard
GET /orders?q=Smith
One parameter, no magic. If the search is complex — see the section below.
Multiple Values for the Same Filter
To pass an array of values, the parameter is simply repeated:
GET /orders?status=CREATED&status=CONFIRMED&status=PAID ✓
GET /orders?status=CREATED,CONFIRMED,PAID ✗
Passing values comma-separated in a single parameter is a common mistake. It breaks if a value itself contains a comma, and it requires manual parsing on the server. Repeating the parameter is the standard behavior that Spring and most frameworks support out of the box.
In OpenAPI this is described like so:
parameters:
- name: status
in: query
schema:
type: array
items:
type: string
style: form
explode: true
When GET Is Not Enough: POST /search
A GET request has physical limits. A URL can't be infinitely long — proxy servers usually cut it off at 2000–8000 characters. You can't pass nested objects in a query string.
When a query is too complex for a URL, use POST /resources/search with a JSON body:
POST /api/v1/orders/search
Content-Type: application/json
{
"statuses": ["CONFIRMED", "PAID", "SHIPPED"],
"dateRange": { "from": "2026-01-01", "to": "2026-12-31" },
"customer": { "regionIds": [1, 5, 12], "segment": "VIP" },
"totalAmount": { "from": 1000, "to": 50000 },
"sort": [
{ "field": "createdAt", "direction": "DESC" },
{ "field": "totalAmount", "direction": "ASC" }
],
"page": 1,
"size": 20
}
When to switch to POST:
- you need nested objects in the filter;
- an array of 10 or more values;
- AND/OR combinations;
- the query needs to be saved and reused.
Rules for POST search:
- URL:
/resources/search— not/query, not/find. - Response code:
200 OK, no resource is created. - The response format is the same as for
GET /resources— the same paginated list.
Common Mistakes
page=0 in a public API. The zero page is an internal implementation detail (Java's zero-based indexing). Clients expect the first page to be 1.
Comma-separated values. ?status=CREATED,CONFIRMED looks compact, but it breaks with values that contain a comma and doesn't follow the standard. Repeat the parameter.
A business action in the query. ?action=cancel is not a filter, it's a command. Commands go through a separate endpoint: POST /orders/{id}/cancel.
The client parses the cursor. If the client decodes the Base64 and reads the cursor's fields — that's a breach of contract. The cursor format may change at any time; the client must treat it as an opaque string.
In Short
- Parameter names — camelCase:
customerId,dateFrom, notcustomer_id. - Filtering: field name = parameter (
?status=CONFIRMED). Ranges:From/Tosuffixes. - Offset pagination:
page(from 1) +size. ReturnstotalElements. For a UI with pages. - Cursor pagination:
cursor(an opaque token) +size. For feeds and infinite scroll. - Sorting:
sort=field,direction; repeated for multi-level. - Multiple values — repeat the parameter:
?status=A&status=B, not?status=A,B. - Complex search —
POST /resources/searchwith a JSON body, response200 OK.
What to Read Next
- URLs and Resource Structure — how to build paths.
- JSON and Response Format — the structure of a paginated response.
- HTTP Methods and Statuses — when to use which method.