Scope of discussion:
- Update
getAllPosts
function 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_NUMBER
to define the default page number, -
const DEFAULT_PAGE_SIZE
to define the default page size per page, -
interface PaginateOutput
to define the response schema we'll receive. Thedata
itself 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 paginate
to convert query params into prisma property. It returns an object withskip
andpage
properties and we can use it in prisma. We'll use it inposts.service
-
function paginateOutput
to 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)