# How Pagination Works in JSON Server with _page and _per_page Parameters

> Learn how pagination works in JSON Server using _page and _per_page parameters to control API response and page size. Get navigation links.

- Repository: [typicode/json-server](https://github.com/typicode/json-server)
- Tags: how-to-guide
- Published: 2026-03-01

---

**JSON Server automatically paginates API responses when you include the `_page` query parameter, using `_per_page` to control page size (defaulting to 10 items) and returning a metadata object with navigation links instead of a raw array.**

JSON Server provides zero-config pagination for your REST API using reserved query parameters. When you append `_page` to any collection endpoint, the server switches from returning a raw JSON array to a structured pagination object that includes navigation metadata and the current page's data. This feature is implemented in the `typicode/json-server` repository through a dedicated pagination pipeline that processes requests in [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts), applies limits in [`src/service.ts`](https://github.com/typicode/json-server/blob/main/src/service.ts), and calculates bounds in [`src/paginate.ts`](https://github.com/typicode/json-server/blob/main/src/paginate.ts).

## Parsing Query Parameters in src/app.ts

The pagination workflow begins in [`src/app.ts`](https://github.com/typicode/json-server/blob/main/src/app.ts) where the application layer extracts the reserved query keys from the request URL. The code retrieves **`_page`** and **`_per_page`** from the search parameters, converts them from strings to integers, and validates the results.

```typescript
const pageRaw = params.get('_page')
const perPageRaw = params.get('_per_page')
const page = pageRaw === null ? undefined : Number.parseInt(pageRaw, 10)
const perPage = perPageRaw === null ? undefined : Number.parseInt(perPageRaw, 10)
…
page: Number.isNaN(page) ? undefined : page,
perPage: Number.isNaN(perPage) ? undefined : perPage,

```

*(source: [app.ts L54‑L64](https://github.com/typicode/json-server/blob/main/src/app.ts#L54-L64))*

Invalid numeric strings are discarded and treated as `undefined`, ensuring only valid integers reach the service layer. These parsed values are then passed to the data service via the `Service.find` method.

```typescript
service.find(name, { where, sort, page, perPage, embed })

```

*(source: [app.ts L21‑L28](https://github.com/typicode/json-server/blob/main/src/app.ts#L21-L28))*

## Service Layer Pagination Trigger in src/service.ts

Inside [`src/service.ts`](https://github.com/typicode/json-server/blob/main/src/service.ts), the presence of a defined `page` option determines whether the response should be paginated. When `opts.page` is not `undefined`, the service calls the core pagination utility and supplies a default page size if **`_per_page`** was omitted.

```typescript
if (opts.page !== undefined) {
  return paginate(results, opts.page, opts.perPage ?? 10)
}

```

*(source: [service.ts L38‑L40](https://github.com/typicode/json-server/blob/main/src/service.ts#L38-L40))*

If the request does not specify `_per_page`, the fallback defaults to **10 items per page**. When pagination is not requested, `Service.find` returns the raw results array directly without wrapping it in a pagination object.

## Core Pagination Algorithm in src/paginate.ts

The `paginate<T>` function in [`src/paginate.ts`](https://github.com/typicode/json-server/blob/main/src/paginate.ts) contains the mathematical logic for slicing the result set and generating navigation metadata. It accepts the full array of items, the requested page number, and the items-per-page limit, then returns a `PaginationResult<T>` object.

```typescript
export function paginate<T>(items: T[], page: number, perPage: number): PaginationResult<T> {
  const totalItems = items.length
  const safePerPage = Number.isFinite(perPage) && perPage > 0 ? Math.floor(perPage) : 1
  const pages = Math.max(1, Math.ceil(totalItems / safePerPage))
  const safePage = Number.isFinite(page) ? Math.floor(page) : 1
  const currentPage = Math.max(1, Math.min(safePage, pages))
  …
  const data = items.slice(start, end)
  return { first, prev, next, last, pages, items: totalItems, data }
}

```

*(source: [paginate.ts L11‑L38](https://github.com/typicode/json-server/blob/main/src/paginate.ts#L11-L38))*

The algorithm performs several safety operations:
- It coerces non-finite **`perPage`** values to `1` and non-finite **`page`** values to `1`.
- It calculates total pages as `Math.ceil(totalItems / safePerPage)`.
- It **clamps** the requested page to the valid range `[1, pages]`, so requests for page `0` or page `99` on a 7-page dataset automatically adjust to valid boundaries.
- It extracts the page slice using `items.slice(start, end)`.

## Response Format and Navigation Metadata

When pagination is active, JSON Server returns an object containing navigation links and the current data slice rather than a plain array. The response includes:

- **`first`** – always `1`
- **`prev`** – the previous page number or `null` if on the first page
- **`next`** – the next page number or `null` if on the last page
- **`last`** – the final page number
- **`pages`** – total count of available pages
- **`items`** – total count of records across all pages
- **`data`** – the array of items for the current page

If you omit the `_page` parameter entirely, the server responds with a raw JSON array of all matching resources, preserving backward compatibility for clients that do not implement pagination.

## Practical Usage Examples

### Basic Pagination Request

Request page 2 with 5 items per page:

```bash
curl "http://localhost:3000/posts?_page=2&_per_page=5"

```

**Response:**

```json
{
  "first": 1,
  "prev": 1,
  "next": 3,
  "last": 7,
  "pages": 7,
  "items": 34,
  "data": [
    { "id": 6, "title": "…" },
    { "id": 7, "title": "…" },
    { "id": 8, "title": "…" },
    { "id": 9, "title": "…" },
    { "id": 10, "title": "…" }
  ]
}

```

### Default Page Size

Omitting `_per_page` uses the default value of 10:

```bash
curl "http://localhost:3000/comments?_page=3"

```

The response contains items 21–30 in the `data` array.

### Handling Out-of-Range Pages

Requesting page `0` automatically clamps to the first page:

```bash
curl "http://localhost:3000/users?_page=0&_per_page=3"

```

The response returns page 1 with `prev: null` and `next: 2`.

Requesting a page beyond the dataset length clamps to the last page:

```bash
curl "http://localhost:3000/users?_page=99&_per_page=5"

```

The response returns the final page with `next: null` and `prev` pointing to the penultimate page.

### Non-Paginated Response

Requests without `_page` return a raw array:

```bash
curl "http://localhost:3000/todos"

```

**Response:**

```json
[
  { "id": 1, "title": "Todo 1" },
  { "id": 2, "title": "Todo 2" }
]

```

## Summary

- Appending **`_page`** to any collection endpoint activates pagination mode and changes the response format from a raw array to a metadata object.
- The **`_per_page`** parameter controls page size and defaults to **10** items when omitted.
- The `paginate<T>` function in [`src/paginate.ts`](https://github.com/typicode/json-server/blob/main/src/paginate.ts) clamps out-of-range page numbers to the valid bounds of `[1, pages]`.
- The response includes navigation links (`first`, `prev`, `next`, `last`) and total counts to facilitate UI rendering without additional calculations.
- Pagination integrates seamlessly with filtering and sorting because `Service.find` applies `where` and `sort` conditions before invoking the pagination logic.

## Frequently Asked Questions

### What is the default page size if I omit `_per_page`?

JSON Server defaults to **10 items per page** when the `_per_page` parameter is not provided. This fallback value is hardcoded in [`src/service.ts`](https://github.com/typicode/json-server/blob/main/src/service.ts) at the call site to the `paginate` function: `opts.perPage ?? 10`.

### How does JSON Server handle out-of-range page numbers?

The pagination algorithm **clamps** invalid page numbers to the nearest valid boundary. Page numbers less than `1` are treated as `1`, while page numbers exceeding the total available pages are reduced to the last page number. This ensures every request returns a valid result set rather than an empty error response.

### Can I combine pagination with filtering and sorting?

Yes. The `Service.find` method in [`src/service.ts`](https://github.com/typicode/json-server/blob/main/src/service.ts) applies `where` filters and `sort` ordering to the dataset before passing the reduced result array to the `paginate` function. This means pagination always operates on the filtered and sorted subset of your data.

### Why does my response format change when I add `_page`?

JSON Server uses the presence of the `_page` parameter as a toggle between two response formats. Without `_page`, the server returns a raw JSON array for backward compatibility. With `_page`, it wraps the data in a `PaginationResult` object containing navigation metadata to support modern frontend pagination components.