Paginated Relationships

View Source

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
end

Nested 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
end

Query Parameters

Basic Pagination

Use the included_page query parameter to paginate included relationships:

GET /posts/1?include=comments&included_page[comments][limit]=10

This 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]=20

This 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]=true

Nested Relationships

For nested relationship paths, use dot notation:

GET /authors/1?include=posts.comments&included_page[posts.comments][limit]=5

This paginates the comments included for each post.

Response Format

When a relationship is paginated, the response includes:

  1. Pagination metadata in the relationship's meta object
  2. Pagination links in the relationship's links object
{
  "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"
      }
    }
  ]
}

The links object in a paginated relationship includes:

  • first: Link to the first page of the relationship
  • next: 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 requested
  • offset: The starting position
  • count: The total count (if requested)

For keyset pagination:

  • limit: The number of resources requested
  • more?: Whether there are more resources available
  • count: 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

  1. 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
    end
  2. Client Implementation:

    • Use the pagination links in the relationship object to navigate pages instead of manually constructing URLs
    • Check the meta object to understand pagination state (limit, offset, count, etc.)
    • Check if next link is null to determine if you're on the last page
    • Check if prev link is null to determine if you're on the first page
  3. Nested Pagination: Be cautious with nested pagination - paginating both posts and posts.comments can result in complex queries. Consider whether you really need both levels paginated.

  4. Backwards Compatibility: Non-paginated includes continue to work as before, so adding paginated_includes configuration is backwards compatible. Clients that don't use included_page parameters will receive all related resources as usual.

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

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