Library / SDK
ppetzold/nestjs-paginate avatar
ppetzold/nestjs-paginate

nestjs-paginate: one query string for pagination, search and filters

Pagination and filtering helper method for TypeORM repositories or query builders using Nest.js framework :book::paperclip:

586 stars130 forksTypeScriptMIT

At a glance

What is it?
nestjs-paginate turns a TypeORM repository or query builder into a list endpoint driven entirely by query parameters, with a JSON:API shaped response, operator filters, multi-column sorting and cursor pagination. The trade is that every route inherits the same parameter vocabulary, and the configuration object is long enough that a careless default exposes a column you did not mean to expose.
Who is it for?
nestjs-paginate fits a NestJS team whose list endpoints are all the same shape and would otherwise each reimplement limit, offset, sorting and filtering by hand, and that wants the response body to follow JSON:API. It does not fit a set of endpoints with genuinely different query semantics, because the library imposes one parameter vocabulary, and it does not fit a service where the full column list must never be exposed to clients without per-route configuration.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The whole list endpoint is one query string

The library's scope is narrow and worth stating plainly: pagination and filtering helper method for TypeORM repositories or query builders using the Nest.js framework. You give it a repository and a configuration object, and it turns that into a list endpoint whose behaviour is driven entirely by the incoming query string. The example in the README shows the shape of a request.

url
http://localhost:3000/cats?limit=5&page=2&sortBy=color:DESC&search=i&filter.age=$gte:3&select=id,name,color,age&withDeleted=true

One parameter carries the page size and page number, one carries the sort column and direction, one is a free-text search, one is a filter with an operator prefix, one restricts the selected columns, and one includes soft-deleted rows. The feature list behind that single line covers pagination conforming to JSON:API, sorting by multiple columns, searching across columns, selecting columns, filtering using operators, including relations and nested relations, virtual column support and cursor-based pagination. The filter operator vocabulary is fixed and worth memorising: $eq, $not, $null, $in, $gt, $gte, $lt, $lte, $btw, $ilike, $sw and $contains.

The audience is a NestJS developer with TypeORM in front of a relational database, and the licence is MIT.

A JSON:API response with meta and links

The response is not a bare array. The README's example result has three top-level keys, data, meta and links, and the meta object carries the pagination state while links carries the navigation.

json
{
  "data": [
    {
      "id": 4,
      "name": "George",
      "color": "white",
      "age": 3
    }
  ],
  "meta": {
    "itemsPerPage": 5,
    "totalItems": 12,
    "currentPage": 2,
    "totalPages": 3,
    "sortBy": [["color", "DESC"]],
    "search": "i",
    "filter": {
      "age": "$gte:3"
    }
  }
}

The meta block is the useful part for a client, because itemsPerPage, totalItems, currentPage and totalPages tell a frontend everything it needs to render a pager without a second request, and the echo of sortBy, search and filter lets the UI show the active query state. The links object holds first, previous, current, next and last URLs, and those URLs carry the whole query string forward, so a user can copy a link to the exact page they are looking at.

The JSON:API conformance is the reason the shape looks the way it does, and it means the response is predictable enough for a generic client to consume.

Cursor pagination for a stable sort order

Offset pagination has the classic problem of skipping or repeating rows when the underlying data changes between requests, and the library offers cursor-based pagination as the alternative. The example request uses a cursor and a sort column.

url
http://localhost:3000/cats?limit=5&sortBy=lastVetVisit:ASC&cursor=V998328469600000

The response in that case carries a cursor in the meta block and only previous, current and next in the links, and the cursor values are opaque strings that encode a position rather than an offset.

json
{
  "meta": {
    "itemsPerPage": 5,
    "cursor": "V998328469600000"
  },
  "links": {
    "previous": "http://localhost:3000/cats?limit=5&sortBy=lastVetVisit:DESC&cursor=V001671616800000",
    "current": "http://localhost:3000/cats?limit=5&sortBy=lastVetVisit:ASC&cursor=V998328469600000",
    "next": "http://localhost:3000/cats?limit=5&sortBy=lastVetVisit:ASC&cursor=V998328037600000"
  }
}

The choice of sort column matters more here than in offset mode, because the cursor is a position in that ordering, so a cursor is only meaningful together with the sort it was created for. The example entity carries a nullable lastVetVisit date column, and the example service sets nullSort to last, which tells the library where rows with a null value sit in the ordering, and that setting is worth understanding rather than copying.

The service layer is where the security decision lives

The example service is short, and the configuration object at the end of it is the part that matters, because it defines what the client is allowed to ask for.

typescript
  public findAll(query: PaginateQuery): Promise<Paginated<CatEntity>> {
    return paginate(query, this.catsRepository, {
      sortableColumns: ['id', 'name', 'color', 'age'],
      nullSort: 'last',
      defaultSortBy: [['id', 'DESC']],
      searchableColumns: ['name', 'color', 'age'],
      select: ['id', 'name', 'color', 'age', 'lastVetVisit'],
      filterableColumns: {
        name: [FilterOperator.EQ, FilterSuffix.NOT],
        age: true,
      },
    })
  }

Four separate lists control four separate capabilities, and the distinction is the point. sortableColumns is the columns that are valid to be sorted by. searchableColumns are the columns that will be searched through when the search query parameter is used, and the config documentation notes you can limit the search scope further with the searchBy query parameter. select is the columns that may be projected. filterableColumns is a map where a bare true means the column can be filtered with any operator, and the example shows the alternative form, a list of FilterOperator and FilterSuffix values that restricts name to equality and negation.

That last distinction is the one to get right. If a column appears in filterableColumns with true, every operator in the vocabulary applies to it, including the ones that expose a range of values. Leaving a column out of the map entirely is what keeps it unfilterable.

The controller side is a decorator and a return type.

typescript
  @Get()
  public findAll(@Paginate() query: PaginateQuery): Promise<Paginated<CatEntity>> {
    return this.catsService.findAll(query)
  }

The @Paginate() decorator parses and validates the incoming query into a PaginateQuery object, and the service hands that object straight to paginate along with the repository and the config. The Paginated type on the return value is what gives Swagger its response schema, which is the connection to the next point.

Global configuration has to be set in bootstrap

Some settings apply to every paginated route, and the README says to set them as soon as possible in the application main bootstrap method, because it affects all paginated routes and the swagger generation logic.

typescript
import { updateGlobalConfig } from 'nestjs-paginate'

updateGlobalConfig({
  // this is default configuration
  defaultOrigin: undefined,
  defaultLimit: 20,
  defaultMaxLimit: 100,
})

Three values here are worth understanding. defaultLimit of 20 is the page size a request gets when it does not ask for one. defaultMaxLimit of 100 is the ceiling, and it is the line that stops a client from asking for a million rows in one response. defaultOrigin of undefined matters for the links object, because the generated first, previous, next and last URLs need a base origin, and if it is not set the library has to infer it from the request, which is where reverse proxy and multi-host setups tend to produce wrong links.

Setting this after the routes are registered is the mistake the README is warning about, since the swagger generation logic reads the same configuration, and a Swagger document that disagrees with the runtime defaults is worse than no Swagger document.

The repository, the build and the test databases

The repository is a single TypeScript package with a conventional layout: src/ for the library, a .github directory for CI, a docker-compose.yml, jest.setup.ts, an .eslintrc.json, a .prettierrc, a .husky directory for git hooks, a renovate.json for dependency updates and a .nvmrc pinning the Node version. The package.json declares the engines field as node >=24.9.0, which is a high floor and worth checking against your own runtime before you install anything.

The scripts show the development loop. The build is a plain tsc, the prepare script runs tsc and husky, the format scripts run prettier over src, the lint script runs eslint with --max-warnings 0, and the tests run jest with NODE_OPTIONS=--experimental-vm-modules, which is how the ESM test suite is executed. The dev:yalc script uses yalc, so a change to the library can be pushed into a consuming application without publishing.

The Compose file brings up both supported databases for the test suite, Postgres and MariaDB.

yaml
services:
  postgres:
    container_name: postgres_nestjs_paginate
    image: postgres:latest
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: test
    ports:
      - "${POSTGRESS_DB_PORT:-5432}:5432"

  mariadb:
    container_name: mariadb_nestjs_paginate
    image: mariadb:latest
    environment:
      MYSQL_ROOT_PASSWORD: pass
      MYSQL_DATABASE: test
    ports:
      - "${MARIA_DB_PORT:-3306}:3306"

Both database ports are overridable by environment variable, so the suite can run alongside whatever you already have on the machine. The dependency on both engines is the interesting detail, because it means the generated SQL is expected to behave the same on Postgres and MySQL, which is where a query builder helper earns its keep.

Editorial conclusion

nestjs-paginate fits a NestJS team whose list endpoints are all the same shape and would otherwise each reimplement limit, offset, sorting and filtering by hand, and that wants the response body to follow JSON:API. It does not fit a set of endpoints with genuinely different query semantics, because the library imposes one parameter vocabulary, and it does not fit a service where the full column list must never be exposed to clients without per-route configuration. Before you adopt it, decide which columns are sortable, searchable and filterable, since every column named in the config becomes reachable from the query string, and set updateGlobalConfig early, since the README says it should be done as soon as possible in the bootstrap method because it affects all paginated routes and the swagger generation logic.

Frequently asked questions

What does nestjs-paginate do?

It is a pagination and filtering helper method for TypeORM repositories or query builders using the Nest.js framework. It turns a repository into a list endpoint driven by query parameters, with pagination conforming to JSON:API, sorting by multiple columns, search across columns, column selection, operator filters, relations and nested relations, virtual column support and cursor-based pagination.

How do I install nestjs-paginate?

With npm install nestjs-paginate. The package requires Node 24.9.0 or newer according to its engines field, and it is MIT licensed with the author listed as Philipp Petzold.

Which filter operators does nestjs-paginate support?

The documented set is $eq, $not, $null, $in, $gt, $gte, $lt, $lte, $btw, $ilike, $sw and $contains. A column listed in filterableColumns with true accepts any of them, while a column can instead be restricted to a specific list of FilterOperator and FilterSuffix values.

What do the sortableColumns, searchableColumns and select options control?

sortableColumns is the list of columns that are valid to sort by, searchableColumns is the list of columns that will be searched through when the search query parameter is used, and select restricts which columns may be projected. You can narrow the search scope further with the searchBy query parameter, and the global defaults such as defaultLimit of 20 and defaultMaxLimit of 100 are set with updateGlobalConfig in the bootstrap method.

How does cursor-based pagination work in nestjs-paginate?

You pass a cursor query parameter along with a sort column, and the response meta block carries the current cursor while the links object holds previous, current and next URLs with new cursor values. The example sorts by lastVetVisit and sets nullSort to last so that rows with a null date are ordered predictably.

Official sources

  1. Issues
  2. License: MIT
  3. ppetzold/nestjs-paginate on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ppetzold-nestjs-paginate.svg)](https://hysenlabs.com/projects/ppetzold-nestjs-paginate)

Was this useful?