7.17 Sorting
Table of Contents
Why Sorting Matters in REST APIs
Sorting lets API clients control the order of returned data. Instead of always getting data in the order stored in the database, clients can ask for:
- Newest items first
- Alphabetical order
- Highest rated items first
- Custom business order
Good sorting support makes your API more flexible and reduces the need for clients to download data and sort it themselves.
In this chapter we focus on how to design and implement sorting in REST APIs, not on database-specific details or performance topics, which are covered elsewhere.
Common Sorting Requirements
Different APIs and clients often need similar sorting behaviors.
Examples:
- A product list:
- Sort by price ascending or descending
- Sort by popularity
- Sort by newest first
- A blog:
- Sort posts by publish date
- Sort by title alphabetically
- A user list:
- Sort by name
- Sort by creation date
- Sort by last login
A typical requirement is:
- Allow sorting by certain fields only
- Support ascending and descending order
- Support stable, predictable sorting
- Sometimes support sorting by multiple fields
Passing Sorting in Query Parameters
Sorting is part of how you want results, not what you want. So it belongs in query parameters, not in the URL path or request body for simple GET list endpoints.
Example endpoint:
GET /products?sort=price
GET /products?sort=-price
GET /products?sort=price,-rating
You will see several common conventions for the sort query parameter.
Sorting Conventions
1. Single field, separate direction parameter
You provide one parameter for field, one for direction.
GET /users?sort_by=name&order=asc
GET /users?sort_by=created_at&order=descThis is easy to understand but can be less compact when you add multiple fields.
Pros
- Very explicit
- Easy for beginners
Cons
- Handling multiple sort fields becomes clumsy:
sort_by=name,created_at&order=asc,descis harder to parse and use
2. Single `sort` parameter, plus-minus convention
You use one parameter, and a minus sign indicates descending order.
GET /users?sort=name # name ascending
GET /users?sort=-name # name descending
GET /users?sort=age,-name # age ascending, then name descendingThis is used by many APIs, for example JSON:API.
Pros
- Compact, powerful
- Natural way to support multiple fields
- Easy to add more sort options in the future
Cons
- Needs documentation, the minus sign is not obvious by itself
- Some API clients or tools might not like punctuation, but usually this is fine
3. Verbose direction syntax
You include both field and direction explicitly in one parameter.
Examples:
GET /users?sort=name:asc
GET /users?sort=name:desc
GET /users?sort=age:asc,name:descor even:
GET /users?sort=+name,-created_atPros
- Very explicit
- Direction is always clear
Cons
- Slightly more complex to parse on the backend
- More verbose to write manually
Supporting Multiple Sort Fields
Sometimes you need to sort by more than one field to get predictable ordering.
Example: You want users ordered by age, and if two users have the same age, order them by name.
In SQL you might write:
ORDER BY age ASC, name ASC;In your API you might express it as:
GET /users?sort=age,nameOr with directions:
GET /users?sort=age,-nameIn this example:
- Primary sort:
ageascending - Secondary sort:
namedescending
The backend should apply these in order.
Important rule: Sorting by multiple fields should always be deterministic.
If you allow sorting by one field only, consider adding a hidden tiebreaker like the primary key:
ORDER BY given_field, id
This avoids unstable result order when data changes between requests.
Safe and Allowed Sort Fields
Never allow clients to sort by any arbitrary string they pass in. This is dangerous:
- It can lead to SQL injection if you build queries incorrectly
- It can leak internal field names or structure
- It may allow very expensive sorts on large text fields
Instead, always define a whitelist of allowed sort fields.
Example strategy in pseudocode:
ALLOWED_SORT_FIELDS = {
"name": "users.name",
"age": "users.age",
"created_at": "users.created_at",
}Workflow:
- Parse the
sortquery parameter. - For each requested sort field:
- Remove any leading
-. - Check if the field is in
ALLOWED_SORT_FIELDS. - If not, return a 400 Bad Request with a helpful error message.
- Build your ORDER BY clause only from allowed, known fields.
Rule: Never put user input directly into an ORDER BY clause.
Always map client-visible sort names to internal, hardcoded column names or expressions.
Handling Invalid Sorting Requests
When clients send invalid sorting options, your API should respond clearly.
Common invalid cases:
- Unknown sort field
- Wrong format
- Conflicting parameters such as using both
sortandorder_byat once
A good approach is to return 400 Bad Request with a structured error response.
Example response:
{
"detail": {
"error": "invalid_sort",
"message": "Cannot sort by 'height'. Allowed fields: name, age, created_at.",
"field": "sort"
}
}This is easier to debug than silently ignoring bad sort parameters.
Sorting and Default Order
You should define a default sort order for each list endpoint. This default is used when the client does not specify sorting.
Examples:
GET /users
Default: newest users first
Implementation:ORDER BY created_at DESCGET /products
Default: alphabetically by name
Implementation:ORDER BY name ASC
You have two main choices when the client sends sort:
- Override the default completely with the requested sorting.
- Append the default as a tiebreaker.
For example, if the client asks:
GET /products?sort=priceYou can:
- Use
ORDER BY price ASConly. - Or add default tiebreaker:
ORDER BY price ASC, id ASC.
Appending a tiebreaker like primary key is usually helpful, but you should not override the client’s intent about which field is primary.
Sorting and Pagination
Sorting and pagination are tightly connected. If you change sort order, page 1 will show different items.
Key points:
- Always apply sorting before pagination in your implementation.
- Document that changing sorting results in different pages.
- If clients use cursor-based pagination (covered in pagination chapter), sorting affects how cursors are built and interpreted.
Example workflow for a typical page-number pagination:
- Parse
sortparameter, decide sort fields and directions. - Apply
ORDER BYto the query. - Apply
LIMITandOFFSET(or equivalent).
In SQL terms:
SELECT *
FROM products
ORDER BY price ASC, id ASC
LIMIT 20 OFFSET 40; -- page 3 of size 20
If a client changes sort from price to name, the same page and page size will show a completely different subset of products.
You should document:
- Which fields are sortable
- Default sort
- How sort interacts with pagination
Examples of Sorting in REST APIs
Example 1: Simple user listing
Endpoint:
GET /usersSupported sort fields:
namecreated_atlast_login
Client examples:
GET /users
Meaning: default sort, for example created_at descending.
GET /users?sort=nameMeaning: order by name ascending.
GET /users?sort=-last_loginMeaning: most recently logged in first.
Response (sorted by name):
{
"items": [
{"id": 3, "name": "Alice", "created_at": "2024-01-10T10:00:00Z"},
{"id": 5, "name": "Bob", "created_at": "2024-01-12T12:30:00Z"},
{"id": 1, "name": "Charlie", "created_at": "2023-12-20T09:15:00Z"}
]
}Example 2: Products with combined sort and pagination
Endpoint:
GET /productsQuery:
GET /products?sort=price,-rating&page=1&page_size=3Assume the data:
| id | name | price | rating |
|---|---|---|---|
| 1 | A | 10 | 4.5 |
| 2 | B | 10 | 3.0 |
| 3 | C | 5 | 5.0 |
| 4 | D | 20 | 4.0 |
| 5 | E | 5 | 4.0 |
Sort: price ascending, then rating descending.
Sorted order:
C(price 5, rating 5.0)E(price 5, rating 4.0)A(price 10, rating 4.5)B(price 10, rating 3.0)D(price 20, rating 4.0)
For page=1&page_size=3, the API returns the first 3 in that order.
Response:
{
"items": [
{"id": 3, "name": "C", "price": 5, "rating": 5.0},
{"id": 5, "name": "E", "price": 5, "rating": 4.0},
{"id": 1, "name": "A", "price": 10, "rating": 4.5}
],
"page": 1,
"page_size": 3,
"total": 5
}Sorting and Filtering Together
Sorting is often used together with filtering (covered in another chapter). The typical order in implementation:
- Apply filters (where conditions).
- Apply sorting (order by).
- Apply pagination.
Example request:
GET /products?category=books&min_price=10&sort=-created_atMeaning:
- Only show products in category "books".
- Only show products with price >= 10.
- Newest products first.
The API might translate that to something like:
SELECT *
FROM products
WHERE category = 'books'
AND price >= 10
ORDER BY created_at DESC, id DESC
LIMIT 20;Note how sorting comes after filtering in the SQL query.
Sorting Best Practices
To make your API sorting support robust and user friendly, follow these guidelines.
Sorting best practice checklist
- Use a single query parameter for sorting, such as
sort. - Support both ascending and descending order.
- Use a consistent convention, for example:
sort=fieldfor ascending,sort=-fieldfor descending.- Always whitelist allowed sort fields.
- Handle invalid sort values with clear 400 Bad Request responses.
- Document:
- List of sortable fields.
- Default sort order.
- How to sort descending.
- How to sort by multiple fields.
- Apply sorting before pagination and after filtering.
- Add a stable tiebreaker such as primary key in your actual query, so the order is deterministic.
If you follow these rules, clients can predictably control data order and your API remains safe and maintainable.
Summary
In REST APIs, sorting is controlled through query parameters and usually uses a dedicated parameter such as sort. You should:
- Choose and document a clear sorting convention.
- Limit sorting to known, safe fields.
- Provide useful defaults.
- Integrate sorting correctly with filtering and pagination.
This makes your API flexible to client needs without sacrificing security or clarity.
Views: 6
KAHIBARO