Pagination in Prisma Can be Painful, Not Anymore

Sugam Subedi
4 min readFeb 26, 2024

--

The end result of this tutorial, see how painless pagination has been now, I love TypeScript!

Dealing with pagination across various models can be a real headache šŸ˜“Itā€™s like youā€™re stuck in a loop, constantly repeating code and risking errors creeping in. But fear not, because Iā€™ve got just the solution to make your life easier and your code cleaner.

In this tutorial, I will cover how you can implement a reusable, customizable and painless pagination in your backend system that uses Prisma.

Even though this tutorial is particularly done in NestJs System, you surely can implement this anywhere as long as youā€™re using Prisma.

Understanding the Pagination Struggle

So, picture this: youā€™re working with different models, trying to keep pagination consistent. Itā€™s a hassle, right? But hey, Iā€™ve been there, and Iā€™ve found a way to make this process smooth as butter.

Introducing the Reusable Pagination Method

Alright, letā€™s jump into it! Iā€™m about to create my pagination.service.ts file for NestJS. Youā€™ll want to adjust this according to your stack and project structure. Letā€™s get started! šŸ› ļø

pagination.service.ts

Step 1: Import Essentials

Letā€™s start by setting up the basics with some imports. In case you are not using NestJS, you do not need some of them.

// Necessary Imports
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { BasePaginationDto } from 'src/pagination/dto/pagination.dto';
import { Prisma } from '@prisma/client';

Step 2: Define Types

Letā€™s take advantage of TypeScript by defining some essential types. This will ensure clarity and type safety throughout our pagination implementation.

// Define a union type of all model names available in Prisma
export type ModelNames =
(typeof Prisma.ModelName)[keyof typeof Prisma.ModelName];

// Define a type for Prisma operations specific to a given model
type PrismaOperations<ModelName extends ModelNames> =
Prisma.TypeMap['model'][ModelName]['operations'];

// Define a type for Prisma findMany arguments specific to a given model
type PrismaFindManyArgs<ModelName extends ModelNames> =
PrismaOperations<ModelName>['findMany']['args'];

// Define a type for pagination options, including model name, query filters, and pagination parameters
type PaginationOptions<ModelName extends ModelNames> = {
modelName: ModelName; // Name of the model to paginate
where?: PrismaFindManyArgs<ModelName>['where']; // Filtering conditions for the query
orderBy?: PrismaFindManyArgs<ModelName>['orderBy']; // Sorting criteria for the query
include?: PrismaFindManyArgs<ModelName>['include']; // Related models to include in the query
page?: string; // Page number for pagination
pageSize?: string; // Number of items per page for pagination
};

Iā€™ve only included where, orderBy & include as per my needs, but you can definitely adjust it according to your needs.

Step 3: Finally the Reusable Paginate Method

Now, letā€™s roll up our sleeves and build the heart of our solution: the reusable paginate method. This function will handle pagination seamlessly, taking in parameters like page number, page size, model name, and query filters.

async paginate<ModelName extends ModelNames>({
page,
pageSize,
modelName,
where,
orderBy,
include,
}: PaginationOptions<ModelName>) {
try {
const db = this.prismaService[modelName as string];
// Get the Prisma service corresponding to the modelName provided
// This is equivalent to this.prismaService.user
// (assuming that modelName is user)

if (!page || !pageSize) {
const items = await db.findMany({
where: where || {},
orderBy: orderBy || {
createdAt: 'asc',
},
include: include || {},
});
return {
items,
totalCount: items.length,
};
}

const skip = (+page - 1) * +pageSize;
// Calculate the number of items to skip based on the current page and page size

const totalCount = await db.count({
where,
});
// Get the total count of items satisfying the provided conditions

const items = await db.findMany({
where,
orderBy,
skip,
take: pageSize,
});
// Fetch paginated items based on the provided conditions, ordering, skip, and take

return {
items,
totalCount,
currentPage:1,
prevPage:
};
} catch (error) {
throw new NotFoundException('Data not found', error);
// Throw a NotFoundException if data is not found or an error occurs
}
}

Step 4: Apply Pagination in Practice

With our pagination service ready to roll, letā€™s see it in action! Iā€™ll guide you through applying pagination in your NestJS project, demonstrating its flexibility and ease of use.

tracks.controller.ts

// Destructure necessary elements from params object
const {
page,
pageSize,
search,
sortOrder,
selectedGenre,
selectedTag,
...rest
} = cleanObject(params);

// Call the paginate method from paginationService
return await this.paginationService.paginate({
// Specify the model name for pagination
modelName: 'Track',
// Include additional fields for customization, including creator details
include: {
...rest,
creator: {
// Select specific fields from the creator model
select: {
id: true,
username: true,
email: true,
role: true,
profile: true,
},
},
},
// Specify pagination parameters
page,
pageSize,
// Define conditions for filtering
where: {
AND: {
// Set your desired conditions
...(selectedGenre && { genreId: selectedGenre }),

...(selectedTag && { tags: { some: { id: selectedTag } } }),

...(search && { title: { contains: search, mode: 'insensitive' } }),
},
},
});

The Result: Beautiful Pagination Response

Take a look at this neat pagination response! Itā€™s all about keeping things simple and easy to understand. With this setup, youā€™ll get the same clear results every time, making it effortless to handle your data.


{
"path": "/api/tracks",
"success": true,
"statusCode": 200,
// Result object containing paginated items and total count
"result": {
"items": [
{
"id": "52b1e7ac-2e75-420d-a309-6687ee459124",
// Title of the track
"title": "A Track",
// Other properties specific to the track
// ...
},
{
"id": "e5a59b5b-360d-4f6e-91ad-f49ee843796e",
"title": "Another Track",
// Other properties specific to the track
// ...
}
],
// Total count of tracks returned by the query
"totalCount": 2
}
}

Conclusion

In conclusion, this pagination approach rocks! Itā€™s customizable, thanks to our carefully crafted types, ensuring type safety and making it a breeze to tailor to our specific needs. Plus, itā€™s reusable, saving us time and effort with each implementation. The cherry on top? Consistent responses every time, making data handling a walk in the park.

And hey, if Iā€™ve made any mistakes along the way, no worries! Weā€™re all beginners here, and feedback and suggestions are always welcome.

Happy Coding! šŸš€

Sign up to discover human stories that deepen your understanding of the world.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Sugam Subedi
Sugam Subedi

Written by Sugam Subedi

Software Developer | Go | TypeScript | Vue | NextJS | React | React Native | NodeJs | NestJS | Grinding!

Responses (3)

Write a response