Quick Start

Install Paginate and fetch your first page with Drizzle ORM—no custom adapter required.

If you already use Drizzle ORM, you can start paginating immediately. You do not need to create an adapter, write a count() function, or work out offsets yourself. The Drizzle integration handles those details.

We'll fetch page 2 of a users table, see exactly what each input means, and examine the result.

1. Install

pnpm add @evensurface/paginate @evensurface/paginate-drizzle drizzle-orm

Or with npm:

npm install @evensurface/paginate @evensurface/paginate-drizzle drizzle-orm

The packages have different jobs:

PackageWhy you need it
@evensurface/paginateThe pagination engine: limits, page calculations, cursors, and metadata.
@evensurface/paginate-drizzleThe ready-made Drizzle integration. It connects the engine to Drizzle queries.
drizzle-ormDrizzle itself. You likely already have this in your app.

Before continuing: You need an existing Drizzle database connection (db) and a table (users). We'll import them from ./db and ./schema, which are example paths in your project, not files supplied by Paginate. If you're just starting with Drizzle, set up your database and table first.

2. Fetch your first page

import { asc } from "drizzle-orm"
import { paginate } from "@evensurface/paginate-drizzle"
import { db } from "./db"         // Your existing Drizzle database
import { users } from "./schema" // Your existing users table

const result = await paginate({
  query: () =>
    db
      .select({ id: users.id, name: users.name })
      .from(users)
      .orderBy(asc(users.id)),
  countQuery: db.$count(users),
  page: 2,
  limit: 2,
})

That's the whole pagination call. There is no adapter field. The paginate() function imported from @evensurface/paginate-drizzle already knows how to work with Drizzle.

What you're passing in

InputWhat it isWhat it does
queryA function that returns a Drizzle select queryDescribes the rows to fetch, and their order. Paginate calls it and adds the correct LIMIT and OFFSET.
countQuerydb.$count(users)Counts how many rows match, so Paginate can calculate the number of pages.
page2Requests the second page. Page numbering starts at 1.
limit2Requests up to two users per page.

The full input type is DrizzlePaginationOptions<TItem>. The optional pageSizeOptions lets you change the default and maximum page sizes.

Why write query: () => ... instead of query: db.select()...? The () => makes a function. Paginate calls that function when it's ready to fetch and gets a fresh Drizzle query each time. It then applies the page's limit and offset. You describe which users and how they're sorted; Paginate supplies how many to take and skip.

Why orderBy(asc(users.id))? A page must have a predictable order. Sorting by a unique ID means that page 2 follows page 1 consistently. If you prefer newest-first ordering, you can use a date and a unique ID as a tie-breaker.

3. See the output

Imagine your database contains five users, ordered by ID:

PositionIDName
11Ava
22Ben
33Grace
44Maya
55Noah

We requested page 2, with 2 users per page. So Paginate does this:

  1. Counts the users: total = 5.
  2. Works out the offset: (page - 1) × limit = (2 - 1) × 2 = 2.
  3. Fetches users in ID order, skipping the first 2 and returning up to 2 more.
  4. Returns those users plus information you can use for Next/Previous buttons.

For this example dataset, result is:

{
  items: [
    { id: 3, name: "Grace" },
    { id: 4, name: "Maya" },
  ],
  pagination: {
    page: 2,
    limit: 2,
    offset: 2,
    total: 5,
    totalPages: 3,
    hasNextPage: true,
    hasPreviousPage: true,
    firstItem: 3,
    lastItem: 4,
  },
}

items contains the selected database rows. pagination tells you where this batch is in the full list. firstItem and lastItem are positions in the list, not database IDs (they happen to match the IDs in this example).

The output type is PaginatedResult<TItem>, and every metadata field is documented in PaginationMetadata. The TypeScript type of each item is inferred from .select({ id, name })—you don't have to define it by hand.

4. Accept page and limit from a URL

A real API might receive /users?page=2&limit=2. Those URL values are strings, but Paginate accepts them directly:

async function handleRequest(request: Request): Promise<Response> {
  const url = new URL(request.url)

  const result = await paginate({
    query: () =>
      db
        .select({ id: users.id, name: users.name })
        .from(users)
        .orderBy(asc(users.id)),
    countQuery: db.$count(users),
    page: url.searchParams.get("page") ?? undefined,
    limit: url.searchParams.get("limit") ?? undefined,
  })

  return Response.json(result)
}

handleRequest() is an example HTTP handler, not a Paginate function. URLSearchParams.get() reads values like "2" from the URL. Response.json() sends the result as JSON.

Paginate handles common input problems for you:

  • Missing or invalid page → page 1.
  • Missing or invalid limit → limit 20.
  • A valid limit larger than the normal maximum → capped at 100.
  • A page beyond the last page → resolved to the last available page.

You can change the limit settings with PageSizeOptions.

5. Add filters without breaking totals

Suppose your users table also has an active boolean column and you want only active users. Apply the same filter to the select query and the count query:

import { eq } from "drizzle-orm"

const where = eq(users.active, true)

const result = await paginate({
  query: () =>
    db
      .select({ id: users.id, name: users.name })
      .from(users)
      .where(where)
      .orderBy(asc(users.id)),
  countQuery: db.$count(users, where),
  page: 1,
  limit: 10,
})

where tells Drizzle to include only users whose active column is true. Using it in both places ensures that the total counts the same users that appear in items. The active column is an assumption for this optional example; use the columns from your own schema.

What about adapters?

You may see an adapter mentioned in the core API or the other pagination guides. It's simply the bit of code that translates Paginate's instructions into data queries.

With Drizzle, that adapter is already built for you. You call paginate() from @evensurface/paginate-drizzle and pass Drizzle queries, as shown above. You do not create an adapter object.

Without Drizzle, the framework-independent @evensurface/paginate package lets you write a small adapter for Prisma, another ORM, an HTTP API, or even an array. That's an advanced integration choice, not a step everyone must complete. Read Adapters explained when you actually need it.

Next steps

On this page