---
title: "Pagination"
manual: "TypoGraph"
permalink: "https://docs.typo3.org/permalink/digicademy/typograph:pagination"
source: "pagination/index.rst"
rendered: "2026-09-18T09:10:56+00:00"
---

# Pagination {#pagination-1}

-   [How It Works](https://docs.typo3.org/permalink/digicademy/typograph:how-it-works)
-   [Schema Setup](https://docs.typo3.org/permalink/digicademy/typograph:schema-setup)
-   [Pagination Configuration](https://docs.typo3.org/permalink/digicademy/typograph:pagination-configuration)
-   [Querying With Pagination](https://docs.typo3.org/permalink/digicademy/typograph:querying-with-pagination)
-   [Response Structure](https://docs.typo3.org/permalink/digicademy/typograph:response-structure)

TypoGraph supports cursor-based pagination following the
[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm).

## How It Works {#how-it-works}

When a root query field returns a **Connection type** (a type whose name ends
with `Connection`), TypoGraph automatically applies cursor-based pagination. If
the return type is a plain list (e.g. `[Discipline]`), the resolver behaves as
before and no pagination is applied.

Cursors are opaque, base64-encoded strings. Clients should treat them as opaque
tokens and never parse or construct them manually.

## Schema Setup {#schema-setup}

To enable pagination for a type, you need to define three types in your GraphQL
schema and include the shared `Pagination.graphql` schema file provided by the
TypoGraph extension.

### 1\. Include the shared PageInfo type {#1-include-the-shared-pageinfo-type}

The `Pagination.graphql` file ships with TypoGraph and defines:

**PageInfo schema**

```text
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}
```

Add it as the first entry in `schemaFiles` in the site configuration:

**config/sites/\<site-identifier>/config.yaml**

```yaml
typograph:
  schemaFiles:
    - 'EXT:typograph/Resources/Private/Schemas/Pagination.graphql'
    - 'EXT:sitepackage/Resources/Private/Schemas/Query.graphql'
    # ...
```

### 2\. Define Connection and Edge types for your entity {#2-define-connection-and-edge-types-for-your-entity}

**Example pagination schema for the Expert type**

```text
type Expert {
  familyName: String
  givenName: String
}

type ExpertConnection {
  edges: [ExpertEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type ExpertEdge {
  cursor: String!
  node: Expert!
}
```

### 3\. Define both a plain-list field and a Connection field in the Query type {#3-define-both-a-plain-list-field-and-a-connection-field-in-the-query-type}

The recommended convention is to expose two root fields per entity: a plain-list
field for unpaginated access and a `*Connection` field for paginated access.
This preserves backwards compatibility for clients that do not need pagination.

**Query schema for plain and paginated Expert type**

```text
type Query {
  experts(familyName: String): [Expert]
  expertsConnection(familyName: String, first: Int, after: String): ExpertConnection
}
```

The `first` and `after` arguments on the Connection field are recognised as
pagination arguments and are **not** turned into `WHERE` conditions. All other
arguments (like `familyName`) continue to work as filters on both fields.

Both field names must be present in `tableMapping`.

## Pagination Configuration {#pagination-configuration}

Configure default and maximum page sizes in the site configuration:

**config/sites/\<site-identifier>/config.yaml**

```yaml
typograph:
  pagination:
    defaultLimit: 20
    maxLimit: 100
```

| Setting | Default | Description |
| --- | --- | --- |
| `defaultLimit` | `20` | Page size when `first` is not provided |
| `maxLimit` | `100` | Upper bound for `first`; requests above this are clamped |

## Querying With Pagination {#querying-with-pagination}

Using the pagination arguments `first` and `after`, or `last` and `before`
allows for fine-grained pagination that can be combined the the usual filter
arguments.

**First page**

```text
{
  expertsConnection(first: 10) {
    edges {
      cursor
      node {
        familyName
        givenName
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
    totalCount
  }
}
```

**Next page**

```text
{
  expertsConnection(first: 10, after: "Y3Vyc29yOjQy") {
    edges {
      cursor
      node {
        familyName
        givenName
      }
    }
    pageInfo {
      hasNextPage
      hasPreviousPage
      endCursor
    }
  }
}
```

**Combining filters with pagination**

```text
{
  expertsConnection(familyName: "Smith", first: 5) {
    edges {
      node {
        familyName
        givenName
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
    totalCount
  }
}
```

## Response Structure {#response-structure}

**A paginated response has this shape**

```json
{
  "data": {
    "expertsConnection": {
      "edges": [
        {
          "cursor": "Y3Vyc29yOjE=",
          "node": {
            "familyName": "Smith",
            "givenName": "Jane"
          }
        }
      ],
      "pageInfo": {
        "hasNextPage": true,
        "hasPreviousPage": false,
        "startCursor": "Y3Vyc29yOjE=",
        "endCursor": "Y3Vyc29yOjE="
      },
      "totalCount": 42
    }
  }
}
```

| Field | Description |
| --- | --- |
| `edges` | Array of edge objects, each containing a `cursor` and a `node` (the actual entity) |
| `pageInfo.hasNextPage` | `true` if more records exist after this page |
| `pageInfo.hasPreviousPage` | `true` if an `after` cursor was provided (i.e. this is not the first page) |
| `pageInfo.startCursor` | Cursor of the first edge in this page (`null` if empty) |
| `pageInfo.endCursor` | Cursor of the last edge in this page (`null` if empty). Pass this as the `after` argument to fetch the next page. |
| `totalCount` | Total number of matching records across all pages. Only queried from the database when this field is actually requested in the GraphQL selection. |
