Scope of discussion:
- Update
getAllPostsfunction to have pagination - Create a utility function to handle pagination
In the third part, we've created Post endpoints:
-
POST /posts: Create a new post, -
GET /posts: Get all posts, -
GET /posts/:id: Get post by ID, -
PATCH /posts/:id: Update post by ID, -
DELETE /posts/:id: Delete post by ID.
Let's focus on get all posts endpoint. To get the posts data, we use prisma ORM await this.prisma.post.findMany() and it will return all the posts data. Looks fine, right? but it's not actually fine. Imagine if our posts data is getting bigger, let's say our posts have 100k rows, it will make the API response slow. The more data, the slower it will be.
So, what's the solution?
Fortunately, prisma provide skip and take property in findMany ORM; for example:
await this.prisma.post.findMany({
skip: 0,
take: 10,
});
We'll use query parameter in the URL to input the pagination variable GET /posts?page=1&size=2. page represents the selected page, size represents how many data we want to display.
Let's create a new DTO (Data Transfer Object) to validate the query params. Create a new file called query-pagination.dto.ts:
// src/common/dtos/query-pagination.dto.ts
import { IsNumberString, IsOptional } from 'class-validator';
export class QueryPaginationDto {
@IsOptional()
@IsNumberString()
page?: string;
@IsOptional()
@IsNumberString()
size?: string;
}
We'll create a utility code to handle our pagination called pagination.utils.ts:
// src/common/utils/pagination.utils.ts
import { NotFoundException } from '@nestjs/common';
import { QueryPaginationDto } from '../dtos/query-pagination.dto';
const DEFAULT_PAGE_NUMBER = 1;
const DEFAULT_PAGE_SIZE = 10;
export interface PaginateOutput<T> {
data: T[];
meta: {
total: number;
lastPage: number;
currentPage: number;
totalPerPage: number;
prevPage: number | null;
nextPage: number | null;
};
}
export const paginate = (
query: QueryPaginationDto,
): { skip: number; take: number } => {
const size = Math.abs(parseInt(query.size)) || DEFAULT_PAGE_SIZE;
const page = Math.abs(parseInt(query.page)) || DEFAULT_PAGE_NUMBER;
return {
skip: size * (page - 1),
take: size,
};
};
export const paginateOutput = <T>(
data: T[],
total: number,
query: QueryPaginationDto,
// page: number,
// limit: number,
): PaginateOutput<T> => {
const page = Math.abs(parseInt(query.page)) || DEFAULT_PAGE_NUMBER;
const size = Math.abs(parseInt(query.size)) || DEFAULT_PAGE_SIZE;
const lastPage = Math.ceil(total / size);
// if data is empty, return empty array
if (!data.length) {
return {
data,
meta: {
total,
lastPage,
currentPage: page,
totalPerPage: size,
prevPage: null,
nextPage: null,
},
};
}
// if page is greater than last page, throw an error
if (page > lastPage) {
throw new NotFoundException(
`Page ${page} not found. Last page is ${lastPage}`,
);
}
return {
data,
meta: {
total,
lastPage,
currentPage: page,
totalPerPage: size,
prevPage: page > 1 ? page - 1 : null,
nextPage: page < lastPage ? page + 1 : null,
},
};
};
In the code above, we created:
-
const DEFAULT_PAGE_NUMBERto define the default page number, -
const DEFAULT_PAGE_SIZEto define the default page size per page, -
interface PaginateOutputto define the response schema we'll receive. Thedataitself contains an array and it is dynamic since we pass generic typeT, for example:
const users: User[] = [/* ... */];
const paginateOutput: PaginateOutput<User> = {
data: users,
total: users.length,
page: 1,
limit: 10,
};
-
function paginateto convert query params into prisma property. It returns an object withskipandpageproperties and we can use it in prisma. We'll use it inposts.service -
function paginateOutputto handle the pagination output response. It has a generic type and three parameters (data, total, and query).
All set. Now, we're ready to update our posts.controller and posts.service. Let's start with posts.service by updating getAllPosts function:
async getAllPosts(query?: QueryPaginationDto): Promise<PaginateOutput<Post>> {
const [posts, total] = await Promise.all([
await this.prisma.post.findMany({
...paginate(query),
}),
await this.prisma.post.count(),
]);
return paginateOutput<Post>(posts, total, query);
}
In the getAllPosts function above, we add a QueryPaginationDto as an optional parameter called query. We also updated the function return type to Promise<PaginateOutput<Post>> since we want the response to be a pagination.
The tricky part is on the Promise.all. Besides we need the data, we need the total data or count data as well, that's why also invoke await this.prisma.post.count(). Posts data will be stored in posts variable, and total data will be stored in total using Promise.all.
Then, we return the data using paginateOutput and pass parameters into it paginateOutput<Post>(posts, total, query);.
We're done with posts.service. Now let's move on to posts.controller.
In the posts.controller, we'll only update getAllPosts function:
getAllPosts(
@Query() query?: QueryPaginationDto,
): Promise<PaginateOutput<CPost>> {
return this.postsService.getAllPosts(query);
}
It's more simpler compared to posts.service. What we need to do is just add @Query decorator and pass it into the getAllPosts service.
So we're done with the pagination.
Let's test the pagination:
Great! All works as we expect 🔥
The full code of part 4 can be accessed here: https://github.com/alfism1/nestjs-api/tree/part-four
Moving on to part 5:
https://dev.to/alfism1/build-complete-rest-api-feature-with-nest-js-using-prisma-and-postgresql-from-scratch-beginner-friendly-part-5-1ggd

Top comments (0)