# Offset Pagination (/docs/guides/offset-pagination)



Use `paginateOffset()` when your API accepts **`offset`** and **`limit`** instead of a page number.

* `offset` = the number of matching items to **skip** (starting from zero).
* `limit` = the maximum number of matching items to **return**.

For example, `offset: 7` with `limit: 3` skips the first seven matching items and asks for items at positions 8, 9, and 10. Unlike numbered pagination, the offset doesn't need to be a multiple of the limit.

## Use it directly with Drizzle [#use-it-directly-with-drizzle]

```ts
import { asc } from "drizzle-orm"
import { paginateOffset } from "@evensurface/paginate-drizzle"
import { db } from "./db"
import { users } from "./schema"

const result = await paginateOffset({
  query: () => db.select().from(users).orderBy(asc(users.id)),
  countQuery: db.$count(users),
  offset: 40,
  limit: 20,
})
```

This asks Drizzle to count users, then **skip 40** and return **up to 20** more. The Drizzle integration takes care of the `count()` and `fetch()` methods internally. No custom adapter is needed. See the [Quick Start](/docs/getting-started/quick-start) to learn how the select factory and count query work.

## Framework-independent core example [#framework-independent-core-example]

If you use another ORM or a custom data source, the core package takes an adapter with `count()` and `fetch()` methods:

```ts
import { paginateOffset } from "@evensurface/paginate"

const result = await paginateOffset({
  adapter: userAdapter,
  query: { active: true },
  countQuery: { active: true },
  offset: 40,
  limit: 20,
})
```

`userAdapter` is an object with `count()` and `fetch()` methods; see the [custom adapter guide](/docs/adapters/create-your-own). Here `query` tells the adapter which users to return, and `countQuery` makes sure the total is based on the **same filter**.

If the adapter counts 95 matching users, the call:

1. Validates `offset: 40` and `limit: 20`.
2. Calls `count()` and gets `95`.
3. Calls `fetch()` with `{ limit: 20, offset: 40 }`.
4. Returns up to 20 matching users (positions 41–60) with the following metadata.

## Core API inputs [#core-api-inputs]

The table below describes the core `paginateOffset()` API used with custom adapters. With Drizzle, use [`DrizzleOffsetPaginationOptions`](/docs/api-reference/drizzle-types#drizzleoffsetpaginationoptions), which has `query`, `countQuery`, `offset`, `limit`, and optional `pageSizeOptions`—but **no `adapter` field**.

All inputs are defined in [`OffsetPaginationOptions<TQuery, TCountQuery, TItem>`](/docs/api-reference/types#offsetpaginationoptions).

| Input             | Type                                                                                           | Required? | Meaning                                                         |
| ----------------- | ---------------------------------------------------------------------------------------------- | --------- | --------------------------------------------------------------- |
| `adapter`         | [`PaginationAdapter<TQuery, TCountQuery, TItem>`](/docs/api-reference/types#paginationadapter) | Yes       | Counts matching rows and fetches the requested slice.           |
| `query`           | `TQuery`                                                                                       | Yes       | Sent to `adapter.fetch()`.                                      |
| `countQuery`      | `TCountQuery`                                                                                  | Yes       | Sent to `adapter.count()`.                                      |
| `offset`          | [`PaginationInput`](/docs/api-reference/types#paginationinput)                                 | No        | Number of matches to skip. Default `0`.                         |
| `limit`           | [`PaginationInput`](/docs/api-reference/types#paginationinput)                                 | No        | Maximum rows requested. Default `20`, normally capped at `100`. |
| `pageSizeOptions` | [`PageSizeOptions`](/docs/api-reference/types#pagesizeoptions)                                 | No        | Custom limit defaults and boundaries.                           |

`offset` may be `40` or `"40"`. Negative, fractional, missing, or otherwise invalid offsets become `0`. The limit follows the same normalization as page mode.

## Outputs [#outputs]

The return type is [`Promise<OffsetPaginatedResult<TItem>>`](/docs/api-reference/types#offsetpaginatedresult). With 95 matching users:

```ts
{
  items: [/* up to 20 User objects */],
  pagination: {
    total: 95,
    limit: 20,
    offset: 40,
    hasNextPage: true,
    hasPreviousPage: true,
  },
}
```

Its metadata type is [`OffsetPaginationMetadata`](/docs/api-reference/types#offsetpaginationmetadata).

| Output                       | Type      | Meaning here                                            |
| ---------------------------- | --------- | ------------------------------------------------------- |
| `items`                      | `TItem[]` | Returned users (up to 20).                              |
| `pagination.total`           | `number`  | `95`: all matching users.                               |
| `pagination.limit`           | `number`  | `20`: requested maximum.                                |
| `pagination.offset`          | `number`  | `40`: number skipped.                                   |
| `pagination.hasNextPage`     | `boolean` | `true`: matching items exist after the requested batch. |
| `pagination.hasPreviousPage` | `boolean` | `true`: offset is greater than zero.                    |

There is **no `page` or `totalPages` field** in this mode. Those fields belong to numbered pagination.

## Move forward or backward [#move-forward-or-backward]

You can calculate offsets for navigation using the returned metadata:

```ts
const { offset, limit } = result.pagination

const previousOffset = Math.max(0, offset - limit) // 20
const nextOffset = offset + limit                 // 60
```

Use `hasPreviousPage` and `hasNextPage` to decide whether to show the corresponding buttons. `nextOffset` is just a number you calculate for the *next request*; `paginateOffset()` does not automatically issue that request.

## If the offset is past the end [#if-the-offset-is-past-the-end]

Suppose there are **95** matching users, but the request is:

```ts
{ offset: 200, limit: 20 }
```

Paginate performs the count, sees the offset is beyond the total, and **doesn't call `fetch()`**:

```ts
{
  items: [],
  pagination: {
    total: 95,
    limit: 20,
    offset: 200,
    hasNextPage: false,
    hasPreviousPage: true,
  },
}
```

It keeps `offset: 200` exactly as requested instead of moving it back to the last possible offset. `hasPreviousPage` simply means the offset is greater than zero; it does not guarantee the previous offset contains results.

If `total` is `0`, the array is empty and the fetch is skipped too.

## Return the fields in your own API shape [#return-the-fields-in-your-own-api-shape]

You can keep the standard result:

```ts
return Response.json(result)
```

Or flatten the metadata when your API already uses top-level `total`, `limit`, and `offset` fields:

```ts
return Response.json({
  users: result.items,
  ...result.pagination,
})
```

Both options send the same information. The second only changes **how your API response is formatted**.

<Note>
  Page and offset modes both perform a count query. For large datasets, deep offsets and counts can be expensive. If users only need to load more items sequentially, see [Cursor Pagination](/docs/guides/cursor-pagination).
</Note>
