JCC Express

JCC Eloquent ORM

Pagination

Introduction

JCC-Eloquent provides pagination at both model and builder level.

Pagination reads the current HTTP request via the global request() helper — you do not pass req as the first argument. Page number comes from request().query.page (default 1). Link URLs use request().protocol, host, and path.

Both methods return data + pagination metadata.


Model pagination

Model.paginate(perPage?, page?):

TypeScript
const paginatedUsers = await User.paginate(15);

This returns:

  • data
  • meta (current_page, per_page, total, total_pages, has_more, links)
  • links (first, last)

Query builder pagination

Builder pagination works on filtered queries:

TypeScript
const page = await User.where("active", true)
  .orderBy("created_at", "desc")
  .paginate(20);

Use this when pagination must respect custom constraints.


Simple pagination

simplePaginate uses offset pagination without running a COUNT(*) query — faster for large tables when you don't need a total count:

TypeScript
const page = await User.where("active", true).simplePaginate(15);

Response shape:

JSON
{
  "data": [...],
  "meta": {
    "current_page": 2,
    "per_page": 15,
    "has_more": true,
    "next_page": 3,
    "prev_page": 1
  }
}

No total or total_pages — just whether there is a next page.


Cursor pagination

cursorPaginate implements keyset (cursor-based) pagination — stable and O(1) for any page depth, regardless of table size:

TypeScript
const page = await User.orderBy("id").cursorPaginate(20);

Response shape:

JSON
{
  "data": [...],
  "meta": {
    "per_page": 20,
    "has_more": true,
    "next_cursor": "eyJpZCI6MTAwfQ==",
    "prev_cursor": null,
    "cursor_column": "id",
    "direction": "asc"
  },
  "links": {
    "path": "http://localhost/api/users",
    "next": "http://localhost/api/users?cursor=eyJpZCI6MTAwfQ==",
    "prev": null
  }
}

Pass the next page by forwarding ?cursor=<token> from meta.next_cursor.

When to use cursor vs offset pagination:

paginatesimplePaginatecursorPaginate
Runs COUNT(*)yesnono
Supports total pagesyesnono
Stable under insertsnonoyes
Random page accessyesnono

Manual offset/limit alternative

For full manual control:

TypeScript
const currentPage = Number(request().query?.page || 1);
const perPage = 20;
const offset = (currentPage - 1) * perPage;

const rows = await User.orderBy("id", "desc").offset(offset).limit(perPage).get();

Notes

  • page is clamped to a minimum of 1.
  • perPage is cast to number internally.
  • Pagination link generation uses request protocol/host/path.

Summary

  • Use paginate(perPage) for standard pagination with total counts and page links.
  • Use simplePaginate(perPage) for large tables where you don't need a total count.
  • Use cursorPaginate(perPage) for high-performance, stable pagination on large or frequently-updated tables.
  • Pagination uses the global request() helper for page number and link generation — call it inside a route handler or middleware context.
  • Use offset + limit only for fully custom paging flows.