Pagination is a method of splitting a list of data into smaller chunks, like pages in a book. This often useful in API requests where the full list would be slow to return, or unwieldy to display all at once. For example, a feed of all blog posts on a site is often split into multiple pages.
One common approach to implementing this is cursor-based pagination. In this tutorial, you will learn how cursor-based pagination works and implement an example using the following stack:
- SQLite: a file-based local SQL database
- Prisma Migrate: a database migration tool which you'll use for seeding the database with example data
- Prisma Client: a database client for TypeScript and Node.js
- GraphQL Nexus: a GraphQL schema definition and resolver implementation
- Apollo Server: an HTTP server for GraphQL APIs with an inbuilt sandbox for testing queries
A reference implementation of the example project is available in this repository.
Note: You will need Node.js (version 12.6 or higher) for this tutorial.
To understand the advantages of cursor-based pagination, it will be useful to first compare it to another common method, offset pagination, which returns a set of elements with a given offset from the start of the list. Offset pagination in Prisma uses the following parameters:
skip
: the number of elements of the list to skiptake
: the number of elements of the list to return
For example, the following diagram skip
s the first 2 posts and take
s the next 3:
Offset pagination is useful because it allows selection of data at any point in the list. However, offset pagination also has disadvantages:
- It scales poorly. Offsetting relies on an underlying
OFFSET
feature in the SQL database which has to traverse all the skipped records before returning the ones requested. This leads to slow performance on large datasets. - It can lead to skipped data if the list is still being actively modified. For example, say someone else deletes the third post in the diagram above while you are viewing the page, and then you select to view the next page with
skip:2 take:5
. The sixth page of the original list will be missed out.
Cursor-based pagination instead uses a cursor to keep track of the current place in the list. Cursor-based pagination in Prisma uses an extra parameter:
cursor
: a unique id corresponding to the last element of the list returned
With cursor-based pagination, you first access the first page of the list with skip
and take
:
To get the second page, you take a unique id
from the last element of the first page, and use this as your cursor value (cursor: 6
in this case). You then need to skip: 1
to start from the next element of the list:
Cursor-based pagination doesn't require traversing the whole list of records in the database, so it scales much better. However, you do have to start from the beginning of the list.
In this section you'll get the example project set up with all the required dependencies and some initial test data.
As a first step, create a project directory and navigate into it:
mkdir cursor-pagination-tutorial
cd cursor-pagination-tutorial
Next, initialize a new project:
npm init -y
This creates a package.json
file with a basic initial setup for your TypeScript app. Add the following to this file:
"dependencies": {
"@prisma/client": "3.1.1",
"apollo-server": "3.3.0",
"graphql": "15.6.0",
"nexus": "1.1.0"
},
"devDependencies": {
"@types/node": "14.17.19",
"prisma": "3.1.1",
"ts-node": "10.2.1",
"ts-node-dev": "1.1.8",
"typescript": "4.4.3"
}
Now run:
npm install
This will install the required dependencies.
TypeScript also requires a tsconfig.json
configuration file in the root directory of the project. Create this and add the following:
{
"compilerOptions": {
"sourceMap": true,
"outDir": "dist",
"strict": true,
"lib": ["esnext"],
"esModuleInterop": true
}
}
First run the following command to initialise a new Prisma schema file:
npx prisma init --datasource-provider sqlite
Add the following Prisma data model to the newly-created file in prisma/schema.prisma
:
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
This defines a simple blog post model to use as a pagination example, where each User
can have multiple Post
s.
Now use Prisma Migrate to map this data model to the database schema:
npx prisma migrate dev --name init
Next, you'll need some example blog post data. Create a new file named prisma/seed.ts
and copy in the example seed file from this Github repository .
Add a reference to this in the package.json
file:
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
You can now run:
npx prisma db seed
Add the following to a new context.ts
file in the root directory of the project:
import { PrismaClient } from '@prisma/client'
export interface Context {
prisma: PrismaClient
}
const prisma = new PrismaClient()
export const context: Context = {
prisma: prisma,
}
Nexus will need this context for its resolvers, which fetch the required data from the Prisma data source.
This file has the code for initialising and running an Apollo Server instance. Create a new server.ts
file and add the following:
import { ApolloServer } from 'apollo-server'
import { schema } from './schema'
import { context } from './context'
const server = new ApolloServer({
schema: schema,
context: context,
})
server.listen().then(async ({ url }) => {
console.log(`\
Server ready at: ${url}
`)
})
This file defines the GraphQL schema. First add the following imports to a new schema.ts
file:
import { Context } from './context'
import {
intArg,
makeSchema,
objectType,
} from 'nexus'
Next, use the Nexus objectType
function to create new GraphQL types for User
and Post
, then use resolve
to map these to the corresponding Prisma types:
const Post = objectType({
name: 'Post',
definition(t) {
t.nonNull.int('id')
t.nonNull.string('title')
t.string('content')
t.field('author', {
type: 'User',
resolve: (parent, _, context: Context) => {
return context.prisma.post
.findUnique({
where: { id: parent.id || undefined },
})
.author()
},
})
},
})
const User = objectType({
name: 'User',
definition(t) {
t.nonNull.int('id')
t.string('name')
t.nonNull.string('email')
t.nonNull.list.nonNull.field('posts', {
type: 'Post',
resolve: (parent, _, context: Context) => {
return context.prisma.user
.findUnique({
where: { id: parent.id || undefined },
})
.posts()
},
})
},
})
You'll also need a GraphQL Query
type. For this example, you'll create two queries to demonstrate offset and cursor-based pagination. The offsetPagination
query takes in two parameters, skip
and take
, while the cursorPagination
query also takes in a cursor
parameter. These correspond to the Prisma pagination options discussed in the What is cursor-based pagination? section above.
const Query = objectType({
name: 'Query',
definition(t) {
t.nonNull.list.nonNull.field('offsetPagination', {
type: 'Post',
args: {
skip: intArg(),
take: intArg(),
},
resolve: (_parent, args, context: Context) => {
return context.prisma.post.findMany({
skip: args.skip || undefined,
take: args.take || undefined,
})
},
})
t.nonNull.list.nonNull.field('cursorPagination', {
type: 'Post',
args: {
skip: intArg(),
take: intArg(),
cursor: intArg(),
},
resolve: (_parent, args, context: Context) => {
return context.prisma.post.findMany({
skip: args.skip || undefined,
take: args.take || undefined,
cursor: {
id: args.cursor || undefined,
},
})
},g
})
},
})
Finally, include the following code to create and export the schema:
export const schema = makeSchema({
types: [
Query,
Post,
User
],
outputs: {
schema: __dirname + '/../schema.graphql',
typegen: __dirname + '/generated/nexus.ts',
},
contextType: {
module: require.resolve('./context'),
export: 'Context',
},
sourceTypes: {
modules: [
{
module: '@prisma/client',
alias: 'prisma',
},
],
},
})
You're now ready to start your GraphAPI server and run some queries. Add the following line to the scripts
section of your package.json
file:
"scripts": {
+ "dev": "ts-node-dev --no-notify --respawn --transpile-only server",
"test": "echo \"Error: no test specified\" && exit 1"
},
Now run:
npm run dev
This will get the Apollo server up and running. Go to http://localhost:4000
, which will redirect you to the Apollo Studio Explorer:
Select 'Query your server', and then enter the following example query in the Operations tab:
query Query {
offsetPagination(skip: 0, take: 3) {
id
title
content
}
}
Now select 'Query' to run this. You should now see the first three posts in the 'Response' tab:
{
"data": {
"offsetPagination": [
{
"id": 2,
"title": "First post by Alice",
"content": "Hello world!"
},
{
"id": 5,
"title": "Update from Alice",
"content": "Some recent news"
},
{
"id": 6,
"title": "Another post by Alice",
"content": "Another update"
}
]
}
}
Now query again using cursor-based pagination to get the next three posts. To do this, pass the id
of the final post in the list to the cursor
parameter. Then skip
one post to start from the next one:
query Query {
cursorPagination(skip: 1, take: 3, cursor: 6) {
id
title
content
}
}
This returns the following posts:
{
"data": {
"cursorPagination": [
{
"id": 9,
"title": "First post by Bob",
"content": "This is my first post!"
},
{
"id": 14,
"title": "Update from Bob",
"content": "What I've been working on"
},
{
"id": 16,
"title": "First post by Charlie",
"content": "Hi everyone!"
}
]
}
}
You can use the Query Explorer to play with this example further. Use Prisma's GraphQL Server Example for ideas on how to add further queries.