When working on an API, sometimes, we have to paginate our data in order to provide a good user experience. In this tutorial, we will be doing a NestJS TypeORM Pagination example. The example will be a products API.
If we’re doing pagination, we most probably have a lot of data. And when having a lot of data, we also most probably are storing that data inside a database. To interact with our database we will use TypeORM.
Let’s install and setup the module:
> yarn add @nestjs/typeorm typeorm mysqlI am using MySQL for this example, you can use whichever database you want, you just have to install the correct driver.
Next, let’s import the module in our root module, app.module.ts:
import { Module } from '@nestjs/common' import { AppController } from './app.controller' import { AppService } from './app.service' import { TypeOrmModule } from '@nestjs/typeorm' @Module({ imports: [ TypeOrmModule.forRoot({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'products', entities: [], synchronize: true, }), ], controllers: [AppController], providers: [AppService], }) export class AppModule {}For this module, we will create a small products module. In order to make thing simple, a product will have on an id, name, price, createdAt field.
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; @Entity() export class Product { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() price: number; }Then, we will add it to the entities array in our TypeORM configuration to sync our database and create the corresponding table:
// ...imports import { Product } from './products/products.entity' @Module({ imports: [ // ...code TypeOrmModule.forRoot({ // ...code, entities: [Product], }), ], // ...code }) export class AppModule {}In order to consume this entity, we will create a ProductsService, with a create and findAll methods:
import { Injectable } from "@nestjs/common"; import { Product } from "./product.entity"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { CreateProductDto } from "./dto/CreateProduct.dto"; @Injectable() export class ProductsService { constructor( @InjectRepository(Product) private readonly productsRespository: Repository<Product> ) {} findAll(): Promise<Product[]> { return this.productsRespository.find() } create(productDto: CreateProductDto): Promise<Product> { return this.productsRespository.save(productDto) } }We have injected the ProductRepository and we’re finding or saving new products, pretty simple so far:
Furthermore, we’re using a DTO as the request body type, CreateProductDto. Let’s add it to the project:
export class CreateProductDto { name: string price: number }Next, we need to create a controller that will use this service to handle the requests:
import { Product } from "./product.entity"; import { ProductsService } from "./products.service"; import { Post, Get, Controller, Body } from "@nestjs/common"; import { CreateProductDto } from "./dto/CreateProduct.dto"; @Controller('products') export class ProductsController { constructor( private readonly productsService: ProductsService ) {} @Get() findAll(): Promise<Product[]> { return this.productsService.findAll() } @Post() create(@Body() productDto: CreateProductDto): Promise<Product> { return this.productsService.create(productDto) } }Now we will bring everything together inside a products module:
import { Module } from "@nestjs/common"; import { ProductsService } from "./products.service"; import { ProductsController } from "./products.controller"; import { TypeOrmModule } from "@nestjs/typeorm"; import { Product } from "./product.entity"; @Module({ imports: [TypeOrmModule.forFeature([Product])], controllers: [ProductsController], providers: [ProductsService], }) export class ProductsModule {}Finally, we’ll import the module in our root module:
// ...imports import { ProductsModule } from './products/products.module' @Module({ imports: [ // ...code ProductsModule, ], // ...code }) export class AppModule {}If we start the server using npm run start:dev, we should ba able to create a new product and fetch all the products:
There are multiple ways to implement pagination, in this tutorial, we’ll implement pagination using SQL offset and limit.
The limit is the amount of items we’ll get back.
The offset is the amount of data we will skip, so for the first page we will skip 0, and for the second page, we will skip the limit, etc…
// dto/pagination.dto.ts export class PaginationDto { page: number limit: number } // dto/paginated-products-result.dto.ts import { Product } from "../product.entity"; export class PaginatedProductsResultDto { data: Product[] page: number limit: number totalCount: number }First of all, we add the PaginationDto as the type of our request params object. And pass the data of the findAll service methods. Our method return type should change to Promise<PaginatedProductsResultDto>
// ... imports import { /* ...named imports */ Query } from "@nestjs/common"; import { PaginationDto } from "./dto/Pagination.dto"; @Controller('products') export class ProductsController { constructor( private readonly productsService: ProductsService ) {} @Get() findAll(@Query() paginationDto: PaginationDto): Promise<PaginatedProductsResultDto> { paginationDto.page = Number(paginationDto.page) paginationDto.limit = Number(paginationDto.limit) return this.productsService.findAll({ ...paginationDto, limit: paginationDto.limit > 10 ? 10 : paginationDto.limit }) } // ... code }Even though our DTO specifies that the page and limit properties are numbers, the query params will be strings. Therefore we need to explicitly convert these values. In addition, we’re also handling negative numbers using Math.abs.
When passing the pagination data to the service method, we’re overriding the limit to force it to be bellow 10. This number depends on your use case.
Next, we will implement the pagination in the service layer using the TypeORM query builder:
// ...imports import { PaginationDto } from "./dto/Pagination.dto"; import { PaginatedProductsResultDto } from "./dto/PaginatedProductsResult.dto"; @Injectable() export class ProductsService { // ...code async findAll(paginationDto: PaginationDto): Promise<PaginatedProductsResultDto> { const skippedItems = (paginationDto.page - 1) * paginationDto.limit; const totalCount = await this.productsRespository.count() const products = await this.productsRespository.createQueryBuilder() .orderBy('createdAt', "DESC") .offset(skippedItems) .limit(paginationDto.limit) .getMany() return { totalCount, page: paginationDto.page, limit: paginationDto.limit, data: products, } } // ...code }We did basic pagination, there are a few improvements that can be done. We need to validate the products data when creating new items, we can do that in the CreateProductDto using the classvalidator. Likewise, we can also validate the pagination data in the PaginationDto and remove the custom validation from the controller method.