AshJsonApi supports pagination for included relationships, allowing you to limit the number of related resources returned when using the include query parameter.
Overview
By default, when you include relationships in a JSON:API request, all related resources are returned. For relationships with many records (e.g., a post with hundreds of comments), this can result in large response payloads and performance issues.
Paginated relationships allow clients to request a specific page of related resources using the included_page query parameter, similar to how top-level resources can be paginated with the page parameter.
Configuration
To enable pagination for specific relationships, add them to the paginated_includes list in your resource's json_api block:
defmodule MyApp.Post do
use Ash.Resource,
extensions: [AshJsonApi.Resource]
json_api do
type "post"
# Allow comments to be included
includes [:comments, :author]
# Configure which relationships can be paginated
paginated_includes [:comments]
end
relationships do
has_many :comments, MyApp.Comment
belongs_to :author, MyApp.Author
end
endNested Relationship Paths
You can also configure pagination for nested relationship paths:
defmodule MyApp.Author do
use Ash.Resource,
extensions: [AshJsonApi.Resource]
json_api do
type "author"
includes posts: [:comments]
# Allow pagination for both posts and nested comments
paginated_includes [
:posts,
[:posts, :comments]
]
end
endQuery Parameters
Basic Pagination
Use the included_page query parameter to paginate included relationships:
GET /posts/1?include=comments&included_page[comments][limit]=10This will include only the first 10 comments.
Offset Pagination
Offset pagination uses limit and offset parameters:
GET /posts/1?include=comments&included_page[comments][limit]=10&included_page[comments][offset]=20This returns 10 comments starting from the 21st comment.
Keyset Pagination
Keyset (cursor-based) pagination uses limit, after, and before parameters:
GET /posts/1?include=comments&included_page[comments][limit]=10&included_page[comments][after]=<cursor>Count Parameter
To include the total count of related resources:
GET /posts/1?include=comments&included_page[comments][limit]=10&included_page[comments][count]=trueNested Relationships
For nested relationship paths, use dot notation:
GET /authors/1?include=posts.comments&included_page[posts.comments][limit]=5This paginates the comments included for each post.
Response Format
When a relationship is paginated, the response includes:
- Pagination metadata in the relationship's
metaobject - Pagination links in the relationship's
linksobject
{
"data": {
"id": "1",
"type": "post",
"attributes": {
"title": "My Post"
},
"relationships": {
"comments": {
"data": [
{"id": "1", "type": "comment"},
{"id": "2", "type": "comment"}
],
"links": {
"self": "/posts/1/relationships/comments",
"related": "/posts/1/comments",
"first": "/posts/1?include=comments&included_page[comments][limit]=10",
"next": "/posts/1?include=comments&included_page[comments][limit]=10&included_page[comments][offset]=10",
"prev": null,
"last": "/posts/1?include=comments&included_page[comments][limit]=10&included_page[comments][offset]=40"
},
"meta": {
"limit": 10,
"offset": 0,
"count": 50
}
}
}
},
"included": [
{
"id": "1",
"type": "comment",
"attributes": {
"body": "First comment"
}
},
{
"id": "2",
"type": "comment",
"attributes": {
"body": "Second comment"
}
}
]
}Pagination Links
The links object in a paginated relationship includes:
first: Link to the first page of the relationshipnext: Link to the next page (null if on the last page)prev: Link to the previous page (null if on the first page)last: Link to the last page (only for offset pagination with count)self: Link to the relationship endpoint (if configured)related: Link to the related resources endpoint (if configured)
These links allow clients to navigate through paginated relationships without manually constructing URLs.
Metadata Fields
For offset pagination:
limit: The number of resources requestedoffset: The starting positioncount: The total count (if requested)
For keyset pagination:
limit: The number of resources requestedmore?: Whether there are more resources availablecount: The total count (if requested)
Error Handling
If you attempt to paginate a relationship that is not configured in paginated_includes, you will receive a 400 Bad Request error:
{
"errors": [
{
"status": "400",
"code": "invalid_pagination",
"title": "InvalidPagination",
"detail": "Invalid pagination: Relationship path author is not configured for pagination. Add it to paginated_includes in the resource.",
"source": {
"parameter": "page"
}
}
]
}Best Practices
Performance: Consider adding default limits at the action level for relationships that are commonly included:
read :read do primary? true pagination offset?: true, default_limit: 20 endClient Implementation:
- Use the pagination
linksin the relationship object to navigate pages instead of manually constructing URLs - Check the
metaobject to understand pagination state (limit, offset, count, etc.) - Check if
nextlink isnullto determine if you're on the last page - Check if
prevlink isnullto determine if you're on the first page
- Use the pagination
Nested Pagination: Be cautious with nested pagination - paginating both
postsandposts.commentscan result in complex queries. Consider whether you really need both levels paginated.Backwards Compatibility: Non-paginated includes continue to work as before, so adding
paginated_includesconfiguration is backwards compatible. Clients that don't useincluded_pageparameters will receive all related resources as usual.JSON:API Compliance: The pagination links in relationships follow the JSON:API specification, which states that "A relationship object that represents a to-many relationship MAY also contain pagination links under the links member."
Example: Complete Flow
1. Configure the resource
defmodule MyApp.Post do
use Ash.Resource,
extensions: [AshJsonApi.Resource]
json_api do
type "post"
includes [:comments, :author]
paginated_includes [:comments]
routes do
base "/posts"
get :read
index :read
end
end
actions do
defaults [:read]
end
relationships do
has_many :comments, MyApp.Comment
belongs_to :author, MyApp.Author
end
end2. Make the API request
curl "http://localhost:4000/posts/1?include=comments&included_page[comments][limit]=5&included_page[comments][count]=true"
3. Process the response
The response will include:
- The post data in
data - Up to 5 comments in
included - Pagination metadata in
data.relationships.comments.meta - Pagination links in
data.relationships.comments.links - The linkage (comment IDs) in
data.relationships.comments.data
4. Navigate to the next page
curl "http://localhost:4000/posts/1?include=comments&included_page[comments][limit]=5&included_page[comments][offset]=5"
Combining with Other Features
Paginated relationships can be combined with:
- Sparse fieldsets:
fields[comment]=body,created_at - Filtering included:
filter_included[comments][status]=published - Sorting included:
sort_included[comments]=-created_at - Field inputs:
field_inputs[comment][calculated_field][arg]=value
Example combining multiple features:
GET /posts/1?
include=comments&
included_page[comments][limit]=10&
filter_included[comments][status]=published&
sort_included[comments]=-created_at&
fields[comment]=body,author_name