skip to content

When a Laravel route returns a paginator directly, what JSON does paginate() produce, and how does it differ for simplePaginate() and cursorPaginate()?

level: juniorimportance: should knowfreq 42%

answer

  1. records under data
  2. total, last_page, links only on paginate()
  3. simple: next_page_url, no total
  4. cursor: next_cursor / prev_cursor
  5. through() maps items, keeps meta

basics

~20 s

Returning a paginator serialises it with the records under data plus metadata. paginate() adds total, last_page, last_page_url and a links array; simplePaginate() has page URLs but no total; cursorPaginate() has next_cursor, prev_cursor and their URLs instead of page numbers.

solid answer

~30 s

Paginators implement `Jsonable` and `Arrayable`, so returning one from a route or controller sends JSON with the items under `data`. `paginate()` (a `LengthAwarePaginator`) adds `current_page`, `from`, `to`, `per_page`, `total`, `last_page`, `first_page_url`, `last_page_url`, `next_page_url`, `prev_page_url`, `path` and a `links` array of `{url, label, page, active}` entries. `simplePaginate()` returns the same page fields plus `current_page_url`, but no `total`, `last_page`, `last_page_url` or `links`. `cursorPaginate()` returns only `data`, `path`, `per_page`, `next_cursor`, `next_page_url`, `prev_cursor` and `prev_page_url`; a client scrolls by following `next_page_url` or sending `next_cursor` as `?cursor=`. To reshape each record without losing the metadata, call `through(fn ($item) => ...)` on the paginator.

code

php · 13 lines
php
<?php

use App\Models\Activity;

Route::get('/api/feed', function () {
    return Activity::latest()
        ->orderByDesc('id')
        ->cursorPaginate(20)
        ->through(fn (Activity $a) => [
            'id' => $a->id,
            'km' => $a->distance_km,
        ]);
});

go deeper

for a junior

Recall that returning a paginator gives JSON with data plus metadata, and that only paginate() includes total and last_page.

for a middle

Explain which keys each paginator emits, what the links array holds, and how through() reshapes records without losing metadata.

for a senior

Treat the paginator's JSON as a client contract: changing the method changes the keys, so version or wrap the response deliberately.

for a principal

Decide whether public APIs expose the raw paginator shape or a stable wrapped envelope, trading convenience against long-term contract control.

## Paginators serialise themselves Every Laravel paginator implements `Arrayable`, `Jsonable` and `JsonSerializable`. When a route or controller returns one, the framework turns it into a JSON response through `toArray()`: the page's records under `data` and pagination metadata beside them. Eloquent models in `data` are serialised with their own `toArray()`, so hidden attributes stay hidden. ## paginate(): full metadata A `LengthAwarePaginator` emits these keys (at `laravel/framework` 13.34.0): - `current_page`, `per_page`, `from`, `to`: the position; `from` and `to` are the 1-based index of the first and last item on the page, or `null` when empty; - `total`, `last_page`: from the count query; - `first_page_url`, `last_page_url`, `next_page_url`, `prev_page_url`, `path`: navigation, with `next_page_url` / `prev_page_url` `null` at either end; - `links`: an array for building a numbered pager, each entry `{url, label, page, active}`, starting with a "previous" entry, then page numbers and `...` separators, then a "next" entry (the labels come from the `pagination.previous` / `pagination.next` translation keys); - `data`: the records. The Laravel 13 docs' sample response also shows a `current_page_url` key for `paginate()`, but `LengthAwarePaginator::toArray()` in the pinned source does not emit it; the simple paginator does. ## simplePaginate(): no totals A `Paginator` emits `current_page`, `current_page_url`, `data`, `first_page_url`, `from`, `next_page_url`, `path`, `per_page`, `prev_page_url` and `to`. There is **no** `total`, `last_page`, `last_page_url` or `links`, because the method never counts. A client knows it has reached the end when `next_page_url` is `null`. ## cursorPaginate(): cursors instead of pages A `CursorPaginator` emits the smallest shape: | Key | Meaning | |---|---| | `data` | the records | | `path` | the base URL | | `per_page` | page size | | `next_cursor` | encoded cursor for the next page, or `null` | | `next_page_url` | `path?cursor=...`, or `null` | | `prev_cursor` | encoded cursor for the previous page, or `null` | | `prev_page_url` | `path?cursor=...`, or `null` | For the infinite-scroll feed of a running app, the client keeps appending `data` and requests `next_page_url` until it is `null`. There is no page number to show and no total to display. ## Key presence at a glance | Key | `paginate()` | `simplePaginate()` | `cursorPaginate()` | |---|---|---|---| | `data`, `path`, `per_page` | yes | yes | yes | | `next_page_url`, `prev_page_url` | yes | yes | yes | | `current_page`, `from`, `to`, `first_page_url` | yes | yes | no | | `current_page_url` | no (source) | yes | no | | `total`, `last_page`, `last_page_url`, `links` | yes | no | no | | `next_cursor`, `prev_cursor` | no | no | yes | `from` and `to` are 1-based positions: on page 3 of 20 per page they are 41 and 60 (or less on a short last page), and both are `null` when the page is empty. A client can therefore render "41 to 60 of 312" from a `paginate()` response alone, "41 to 60" from a `simplePaginate()` response, and nothing positional from a cursor response. ## Shaping the records, not the envelope To change what each record looks like while keeping the envelope, call `through()`: `Activity::latest()->cursorPaginate(20)->through(fn ($a) => ['id' => $a->id, 'km' => $a->distance_km])`. It maps the items in place and returns the same paginator. The query string carried into the URLs follows the rules of `withQueryString()` and `appends()`. When the API needs a different envelope, for example `meta` and `links` objects, that is the job of API resource collections, which wrap a paginator and restructure its metadata; the raw paginator shape described here is what you get without them. ## Why interviewers ask A front-end or mobile client is written against this shape. Knowing which keys exist for which method tells you whether a "page 3 of 12" UI is even possible, why a cursor endpoint cannot report a total, and why switching an endpoint from `paginate()` to `simplePaginate()` is a breaking change for clients that read `total` or `links`.

  • Why is switching an API endpoint from paginate() to simplePaginate() a breaking change?
    The JSON loses `total`, `last_page`, `last_page_url` and the `links` array, because `simplePaginate()` never counts rows. Any client that shows a page count or builds numbered buttons from `links` stops working, even though `data` and the next/previous URLs remain.
  • How do you change each record's fields without losing the pagination metadata?
    Call `through()` with a callback on the paginator. It maps every item in the current page and returns the same paginator, so `toArray()` still emits the full metadata with the reshaped records under `data`.

saying these in an interview costs you the question

  • simplePaginate() JSON includes total but not last_page
  • cursorPaginate() JSON includes current_page and total
  • Returning a paginator sends only the records array
  • map() on the paginator keeps the metadata like through()
  • The links array exists on every paginator type