Implementing pagination with GraphQL in NestJS
Pagination is a common UX problem that comes up in many data-driven applications. We need to limit how much is shown on screen; if my content requires too much scrolling, navigating around my site becomes a painful experience for my users.
In this post, we’ll look at a common way to solve this problem by building a NestJS server with GraphQL and then consume it on a React frontend. By going step by step through the following sections, we will build a simple pagination system that can be applied to all kinds of applications.
This guide will be split into three main sections:
- The pagination algorithm
- Setting up an API using NestJS, GraphQL, and Mongoose
- Building a React frontend to consume the API
This is a practical guide to implementing a simple pagination system in NestJS and GraphQL. You can improve on the application that we will build in this guide to create something more production-ready.
I recommend coding along to solidify the concepts. All the code written in this guide can be found on my GitHub.
What are we building?
The application we’ll build is a simple React frontend that allows a user to page through a list of users. It’s just simple enough to easily understand the different concepts we’ll cover, while still practical enough to modify for existing applications.
The pagination algorithm
Before setting up the project, it’s worth it to go through the pagination algorithm we’ll be implementing. This will help you make sense of each part of the project as we start creating files and writing code.
Let’s skip ahead and take a look at the final GraphQL query that we’ll be calling to fetch and paginate the list of users.
{ count users(take: 20, skip: 0) { firstName lastName } }
The query consists of two resources, count
and users
.
The first one, count
, as you can probably tell from the name, simply returns a count of all the users in the database. The other resource, users
lets us specify how many users we want to retrieve (take
), as well as an offset to start fetching from (skip
).
How can we implement pagination with this simple query?
Consider a scenario where we have five resources:
[one, two, three, four, five]
If we run the above query with the arguments take = 2, skip = 0
, we’ll get the following resources:
[one, two]
And if we ran the same query again, but with the following arguments:
take = 2, skip = 2
we’d get the following resources:
[three, four]
By keeping track of how many users we’ve retrieved on the frontend, we can pass a number to the skip
argument to retrieve the correct number of next users. This will become clearer when we implement the frontend.
For now, let’s set up the API to implement the functionality discussed so far.
Setting up an API using NestJS, GraphQL, and Mongoose
Usually, we’d start by setting up a fresh NestJS project and installing a few dependencies to get us going.
However, to skip all the painful parts of setting up a project to follow a tutorial, I’ve gone ahead and set up a repository with all the necessary libraries and setup files.
The repository is a monorepo containing both backend and frontend components. This allows us to build both the API and the frontend in a single repo, unlocking extra speed in development time.
It relies on Yarn workspaces, so you’ll need to have both npm and Yarn installed.
Clone the repository and run the following commands to get started.
git clone https://github.com/ovieokeh/graphql-nestjs-pagination-guide.git npm install cd ../workspaces/frontend npm install cd workspaces/backend npm install mkdir src && cd src
If you run any of the commands in the package.json
files, they’ll most likely error out. You may also see eslint
errors if you have your editor configured. This is fine. We’ll fix these as we work through the guide.
Now that you’ve installed all of the required packages, we can start building the different components of our API.
Mongoose schema setup
First, we need to set up a database that will query GraphQL. I’ve decided to go with Mongoose for this guide because it’s one of the most popular database ORMs out there, but you should be able to apply the same concepts with other ORMs.
We’ll start by creating a src/mongoose
folder and a src/mongoose/schema.ts
file to hold our database types, models, and schema.
mkdir mongoose touch mongoose/schema.ts
Now, let’s configure our schema.ts
file.
// src/mongoose/schema.ts import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' import { Document } from 'mongoose' export type UserDocument = UserModel & Document @Schema() export class UserModel { @Prop() firstName: string @Prop() lastName: string @Prop() email: string @Prop() dateOfBirth: Date } export const UserSchema = SchemaFactory.createForClass(UserModel)
UserDocument
is a TypeScript type representing a user model and Mongoose documentUserModel
represents a single user to be stored in the databaseUserSchema
is a Mongoose schema derived from theUserModel
We’ll be making use of these as we finish setting up the API.
NestJS and GraphQL
Next, we need to create some files and folders, which will be explained as we fill the contents.
mkdir users && cd users mkdir dto entities touch dto/fetch-users.input.ts entities/user.entity.ts
dto/fetch-users.input.ts
// dto/fetch-users.input.ts import { Field, Int, ArgsType } from '@nestjs/graphql' import { Max, Min } from 'class-validator' @ArgsType() export class FetchUsersArgs { @Field(() => Int) @Min(0) skip = 0 @Field(() => Int) @Min(1) @Max(50) take = 25 }
FetchUsersArgs
is a data transfer object (DTO), which means that it describes a piece of data being sent over the network. In this case, it’s describing the arguments, skip
and take
, that we will pass to the API when querying the users.
The next set of files we’ll create are the user service, resolver, and module.
Creating the users.service.ts
file
touch users.service.ts users.resolver.ts users.module.ts import { Model } from 'mongoose' import { Injectable } from '@nestjs/common' import { InjectModel } from '@nestjs/mongoose' import { UserDocument, UserModel } from '../../mongoose/schema' import { FetchUsersArgs } from './dto/fetch-users.input' import { User } from './entities/user.entity' @Injectable() export class UsersService { constructor( @InjectModel(UserModel.name) private userModel: Model<UserDocument>, ) {} ... continues below (1) ...
NestJS injects the Mongoose database we created earlier into the UsersService
class using the @InjectModel
decoration. This allows us to query the database using the getCount
and findAll
methods.
... continues from (1) ... async getCount(): Promise<number> { const count = await this.userModel.countDocuments() return count } ... continues below (2) ...
UsersService.getCount()
is a method that allows us to fetch the total number of users in the database. This count will be useful for implementing the numbered pagination component in the frontend.
... continues from (2) ... async findAll(args: FetchUsersArgs = { skip: 0, take: 5 }): Promise<User[]> { const users: User[] = (await this.userModel.find(null, null, { limit: args.take, skip: args.skip, })) as User[] return users } }
UsersService.findAll({ skip, take })
is a method that fetches a specified amount of users (with the take
argument) along with an offset (skip
).
These two methods form the base of the pagination system we’ll be building.
Creating the users.resolver.ts
file
import { Resolver, Query, Args } from '@nestjs/graphql' import { User } from './entities/user.entity' import { UsersService } from './users.service' import { FetchUsersArgs } from './dto/fetch-users.input' @Resolver(() => User) export class UsersResolver { constructor(private readonly usersService: UsersService) {} @Query(() => Number, { name: 'count' }) async getCount(): Promise<number> { return this.usersService.getCount() } @Query(() => [User], { name: 'users' }) async findAll(@Args() args: FetchUsersArgs): Promise<User[]> { return this.usersService.findAll(args) } }
The UsersResolver
class is the GraphQL resolver for the count
and users
queries. The methods simply call the corresponding UsersService
methods.
Creating the users.module.ts
file
import { Module } from '@nestjs/common' import { MongooseModule } from '@nestjs/mongoose' import { UserModel, UserSchema } from '../../mongoose/schema' import { UsersService } from './users.service' import { UsersResolver } from './users.resolver' @Module({ imports: [ MongooseModule.forFeature([{ name: UserModel.name, schema: UserSchema }]), ], providers: [UsersResolver, UsersService], }) export class UsersModule {}
The UsersModule
class imports the Mongoose schema and configures the resolver and service classes, as defined above. This module gets passed to the main app module and allows for the query defined earlier.
Creating the app.module.ts
file
Finally, to tie everything together, let’s create an app.module.ts
file to consume all the modules we’ve defined so far.
import { Module } from '@nestjs/common' import { GraphQLModule } from '@nestjs/graphql' import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo' import { MongooseModule } from '@nestjs/mongoose' import { UsersModule } from './users/users.module' import { ConfigModule, ConfigService } from '@nestjs/config' import configuration from '../nest.config' @Module({ imports: [ UsersModule, ConfigModule.forRoot({ load: [configuration], }), MongooseModule.forRootAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ uri: configService.get('databaseUrl'), }), inject: [ConfigService], }), GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: 'schema.gql', include: [UsersModule], }), ], }) export class AppModule {}
This should all be familiar if you already have experience with GraphQL and NestJS. We’re importing:
GraphQLModule
for setting up GraphQLMongooseModule
for the databaseUsersModule
for the users resourceConfigModule
for setting up environment variables
Now, make sure to setup a MongoDB database and create a .env
file using the .env.example
as a guide before adding your database connection URI.
At this point, you can now test the API by doing the following:
- Ensure that you’re in the backend directory —
cd src/workspaces/backend
- Run
yarn seed
to seed in some fake user data - Run
yarn start:dev
to start the server on port 3000 - Navigate to
http://localhost:3000/graphql
on your browser to open the GraphQL playground, where you can try the query from the “The pagination algorithm” section like so:{ count users(take: 20, skip: 0) { firstName lastName } }
If you’ve made it this far, you’re a rock star .
This is a good time to take a break and go through the backend code again. Take your time to understand it, maybe grab a cup of juice (or tea, if you’re fancy), and then continue with the frontend.
Building a React frontend to consume the API
With our backend all set up, we can now create a shiny React frontend to implement a basic pagination system.
Building the components
Instead of setting up a whole new frontend project, you can make use of the workspaces/frontend
folder, which already has a React app set up with all the necessary dependencies installed.
cd ../frontend/src
Let’s start with a bottom-up approach to building out the components, before finally integrating it all at the end.
We’ll need the following components:
Users
— queries the API and renders a list of usersPagination
— provides the pagination logic and renders the controlsApp
— renders both Users and PaginationIndex
— wraps the app in an Apollo provider and renders to the DOM
Writing our users.tsx
component
The component will query the GraphQL API using the @apollo/client
library and render a list of users when the query is resolved.
// ensure you're in /workspaces/frontend/src touch Users.tsx
Open up the newly created file.
// Users.tsx import { gql, useQuery } from '@apollo/client' const GET_USERS = gql` query GetUsers($skip: Int!, $amountToFetch: Int!) { users(skip: $skip, take: $amountToFetch) { id firstName lastName } } ` type User = { id: string firstName: string lastName: string } ... continues below (3) ...
At the top of the file, we import gql
and useQuery
from the @apollo/client
library mentioned earlier.
gql
allows us to build a GraphQL query with functionality like dynamic variable replacement. The GET_USERS
variable is a query that requests a list of users
of length $amountToFetch
from an offset $skip
.
We’re querying the id
, firstName
, and lastName
properties of each user. The User
variable is a TypeScript type that specifies the structure of a user.
... continues from (3) ... const Users = (props: { skip?: number; amountToFetch?: number }) => { const { data } = useQuery<{ count: number; users: User[] }>(GET_USERS, { variables: props, }) const renderedUsers = data?.users?.map(({ id, firstName, lastName }) => { const name = `${firstName} ${lastName}` return ( <div key={id}> <p>{name}</p> </div> ) }) return <div className="Users">{renderedUsers}</div> } export default Users
Finally, we have a Users
component that accepts two props: skip
and amountToFetch
.
It immediately kicks off a query to the API the GET_USERS
query as well as passing the props
as variables
.
Then we map over the array of users (using the ternary operator, in case the data isn’t ready yet) and return a div
containing the name of each user.
At the end, the return statement completes this component.
The pagination.tsx
component
Hopefully you’re familiar with the renderProps
technique in React. This component utilizes renderProps
to render a component with props as well as render a select input and some buttons.
Create a new Pagination.tsx
file and open it.
// ensure you're in /workspaces/frontend/src touch Pagination.tsx
We’ll start by importing some types and utilities from React and setup some state variables to track the current state of the pagination component.
import { ChangeEvent, cloneElement, FunctionComponentElement, useState } from 'react' const Pagination = ({ count, render }: { count: number render: FunctionComponentElement<{ skip: number; amountToFetch: number }> }) => { const [step, setStep] = useState(0) const [amountToFetch, setAmountToFetch] = useState(10) ... continues below (4) ...
The Pagination
component accepts two props:
count
— The total number of users in the database. Used to calculate the number of steps to render in the UIrender
— A React component that will receive additional props from thePagination
component
It also has two state variables:
step
— The current step in being renderedamountToFetch
— Amount of users to fetch at any given time
... continues from (4) ... const steps = count ? Math.ceil(count / amountToFetch) : 0 const renderedSteps = new Array(steps).fill(0).map((num, index) => ( <button data-is-active={index === step} key={index} type="button" onClick={() => setStep(index)} > {index + 1} </button> )) const renderWithProps = cloneElement(render, { skip: step * amountToFetch, amountToFetch, }) ... continues below (5) ...
Next, define three variables:
steps
— This does some simple arithmetic to get the number of steps to render
> if count = 10 users and amountToFetch = 5
> steps = 2 // < 1 2 >
> if count = 10 users and amountToFetch = 2
> steps = 5 // < 1 2 3 4 5 >renderedSteps
— Makes use ofsteps
to render an array of buttons from1..steps
. Each button has anonClick
handler that updates thestep
staterenderWithProps
— Clones the component passed in therender
prop and adds two new props to it:skip
— how much to skip by when querying the usersamountToFetch
— the amount of users to retrieve
... continues from (5) ... return ( <> {renderWithProps} <select name="amount to fetch" id="amountToFetch" value={amountToFetch} onChange={(e: ChangeEvent<HTMLSelectElement>) => { const newAmount = +e.target.value setAmountToFetch(newAmount) setStep(0) }} > <option value={10}>10</option> <option value={20}>20</option> <option value={50}>50</option> </select> <button type="button" disabled={step === 0} onClick={() => setStep((prevstep) => prevstep - 1)} > {'<'} </button> {renderedSteps} <button type="button" disabled={(step + 1) * amountToFetch > count} onClick={() => setStep((prevstep) => prevstep + 1)} > {'>'} </button> </> ) } export default Pagination
Finally, we render five elements to the DOM:
renderWithProps
: Therender
component cloned with props addedselect
: Controls theamountToFetch
state variable and allows a user to change how much users to fetch per page. We’ve currently hardcoded three steps of 20, 50, and 100. TheonChange
handler updates theamountToFetch
state and resets thestep
button
: Allows the user to move back one steprenderedSteps
: A list of buttons that allows switching to the corresponding stepbutton
: Allows the user to move forward one step
Again, take some time to breathe, relax, and understand the concepts covered so far. Taking a walk may not be such a bad idea
React and Apollo
We’re so close to the finish line now! All that remains is to hook up the Users
component with the Pagination
component and render.
Create an App.tsx
file and open it.
// ensure you're in /workspaces/frontend/src touch App.tsx
Here are our file contents:
import { gql, useQuery } from '@apollo/client' import Users from './Users' import Pagination from './Pagination' import './App.css' const GET_USERS_COUNT = gql` query GetUsersCount { count } ` function App() { const { data } = useQuery<{ count: number }>(GET_USERS_COUNT) return ( <div className="App"> <Pagination count={data?.count || 0} render={(<Users />) as any} /> </div> ) } export default App
This is a relatively simple component. We import:
gql
anduseQuery
for a query we will define below- The
Users
andPagination
components - A CSS stylesheet that comes with the project
Then we define the GET_USERS_COUNT
query, which simply requests the total amount of users in the database.
The App
function requests the GET_USERS_COUNT
query and stores the result in the data
variable.
In the return
statement, we render the Pagination
component in a div and —
- Pass the
data.count
variable as thecount
prop - Pass the
Users
component as therender
prop
Just one final piece remains and you’ll be able to test your results in the browser. Whew!
Now, create an index.tsx
file and open it.
// ensure you're in /workspaces/frontend/src touch index.tsx
Here again are our file contents:
import React from 'react' import ReactDOM from 'react-dom/client' import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client' import App from './App' import './index.css' const client = new ApolloClient({ uri: process.env.REACT_APP_API_GRAPHQL_URL, cache: new InMemoryCache(), }) const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) root.render( <React.StrictMode> <ApolloProvider client={client}> <App /> </ApolloProvider> </React.StrictMode> )
Most of the content in this file should feel familiar by now. What’s interesting is that we’re creating a new Apollo client to connect to our API and passing it to the Apollo provider in the root.render
statement.
Note: Make sure to create a .env
file using the .env.example
as a guide, and adding your API URL (most likely http:localhost:3000/graphql
).
At this point, you can now start the frontend in the browser and marvel at your creation.
- Ensure that the backend is running (
yarn start:dev
) - Ensure that you’re in
workspaces/frontend
and runyarn start
- Navigate to http://localhost:3001
Conclusion
Go ahead and interact with the pagination controls. Maybe you can find a way to truncate the middle section or even add some nice styling; this is a base pagination system that you can customize to whatever data type or scenario.
You can find the source code for this article on my GitHub.
If you managed to stay until the end, you deserve a good pat on the back. I know it was a little dense at times but hopefully this was useful to you.
The post Implementing pagination with GraphQL in NestJS appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/Z7vjYpT
via Read more