How Pagination Works in JSON Server with _page and _per_page Parameters

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, applies limits in src/service.ts, and calculates bounds in src/paginate.ts.

Parsing Query Parameters in src/app.ts

The pagination workflow begins in 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.

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)

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.

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

(source: app.ts L21‑L28)

Service Layer Pagination Trigger in src/service.ts

Inside 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.

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

(source: 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 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.

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)

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:

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

Response:

{
  "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:

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:

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:

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:

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

Response:

[
  { "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 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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →