DEV Community

Alfi Samudro Mulyo
Alfi Samudro Mulyo

Posted on

Build Complete REST API Feature with Nest JS (Using Prisma and Postgresql) from Scratch - Beginner-friendly - PART 4

Scope of discussion:

  1. Update getAllPosts function to have pagination
  2. 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,
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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,
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

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. The data itself contains an array and it is dynamic since we pass generic type T, for example:
const users: User[] = [/* ... */];
const paginateOutput: PaginateOutput<User> = {
  data: users,
  total: users.length,
  page: 1,
  limit: 10,
};
Enter fullscreen mode Exit fullscreen mode
  • function paginate to convert query params into prisma property. It returns an object with skip and page properties and we can use it in prisma. We'll use it in posts.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);
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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:

Pagination post

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)