nnestjs-drizzle-crud
Guides

Pagination & sorting

The findAll() return shape, default sort, and the sortBy/sortOrder options.

findAll(filters?, pagination?, options?) returns a paginated result envelope — not a bare array.

Return shape

type FindAllResult<T> = {
  data: T[];
  total: number;   // total rows matching the filter, ignoring pagination
  page: number;    // echo of the page you requested (1-indexed)
  limit: number;   // echo of the limit you requested, after capping
};
const { data, total, page, limit } = await service.findAll(
  { status: 'active' },
  { page: 2, limit: 25 },
  { search: { term: 'john', columns: ['name', 'email'] } },
);
// data: 25 active users matching "john" for page 2
// total: 240 active users matching "john"
// page: 2
// limit: 25

The pagination argument is optional. When omitted, findAll uses defaults.pagination.defaultLimit (default 20) and starts at page 1. total is the row count after filters and options.search, but before pagination, so it stays correct across pages.

Pagination options

FieldTypeDefaultNotes
pagenumber11-indexed.
limitnumberdefaults.pagination.defaultLimitCapped at defaults.pagination.maxLimit (default 100).
sortBystring | string[]defaultSort (entity)Column name(s).
sortOrder'asc' | 'desc''desc'Applied when sortBy is a string.

Sorting

Explicit sortBy

await service.findAll(
  {},
  { sortBy: 'createdAt', sortOrder: 'desc' },
);

sortBy accepts a string or an array. When an array is passed, sortOrder is ignored — each column carries its own order via the entity's defaultSort definition (see below).

Default sort per entity

Define an ordered list of columns in forFeature config — they're applied as the ORDER BY clause when no explicit sortBy is passed. The first column is the primary sort, the rest are tiebreakers.

posts/posts.module.ts
DrizzleCrudModule.forFeature([
  {
    service: PostsService,
    table: posts,
    config: {
      defaultSort: [
        { column: 'position', order: 'asc' },
        { column: 'created_at', order: 'desc' }, // tiebreaker
      ],
    },
  },
])
// ORDER BY position ASC, created_at DESC
await service.findAll();

// ORDER BY name DESC  — the default is replaced, not appended
await service.findAll({}, { sortBy: 'name' });

sortBy replaces defaultSort

An explicit sortBy replaces the entity's defaultSort entirely. Pass an array to sortBy if you want the same multi-column ordering behavior.

Opt-in created_at fallback

Set defaults.sortOrder in forRoot to apply a created_at fallback to every entity that doesn't define its own defaultSort:

app.module.ts
DrizzleCrudModule.forRoot({
  dialect: 'postgresql',
  connectionString: process.env.DATABASE_URL,
  schema,
  defaults: {
    sortOrder: 'desc', // every findAll() orders by created_at DESC unless overridden
  },
});

Per-column order defaults to 'asc' when you don't specify it. Unknown columns in defaultSort are skipped and emit a [nestjs-drizzle-crud] defaultSort references unknown column "<col>"; skipping. warning at startup — they never throw. (Caller-supplied sortBy is different: it throws on unknown columns, see below.)

Cap on limit

limit is capped at defaults.pagination.maxLimit. Anything over the cap is silently lowered to the cap. To return more rows per page, raise the cap in forRoot:

defaults: {
  pagination: { defaultLimit: 20, maxLimit: 200 },
}

The cap is enforced on every call. If you genuinely need to return every row (e.g. an export endpoint), use a custom method that bypasses findAll and calls Drizzle directly.

Clamping & fail-fast

Pagination and sorting inputs are normalized before they reach SQL. Bad input is either silently clamped to a safe value or rejected as BadRequestException (→ 400), depending on the failure mode.

Silently clamped (never throws):

InputResult
page < 11
limit <= 01
limit > maxLimitmaxLimit
page / limit not a finite number1 / defaultLimit

A LIMIT 0 or negative OFFSET can never reach SQL — every value is clamped via the package's shared resolvePagination helper.

Throws BadRequestException (→ 400):

InputWhy
Caller sortBy not a column on the tableMirrors the unknown-column guard — typos surface immediately instead of returning unsorted data.
Caller sortOrder other than 'asc' / 'desc'Without the guard, anything other than 'desc' silently sorted ascending.
{ gt / gte / lt / lte: NaN } or { …: Infinity }Reaches SQL as an opaque 500; rejected as 400 instead. Affects numeric columns only — date / string operands pass through unchanged.

For the full per-method exception table (404 / 409 / 400 / 500 mapping), see Errors.

Cursor-based pagination

The package does not ship cursor-based pagination out of the box. findAll is offset-based. For large datasets, write a custom method that uses Drizzle's where(...).orderBy(...).limit(...) chain with a cursor column (e.g. id > lastSeenId).

Next

On this page