spatie/laravel-json-api-paginate
A Laravel package that adds a jsonPaginate method to the Eloquent query builder, so pagination reads page[number] and page[size] request parameters and returns the pagination links required by the JSON:API spec.
Architecture fit. The package is a thin, single-purpose extension: it registers a jsonPaginate() macro on Eloquent's query builder (and relation builders) that reads page[number] / page[size] (and optionally page[cursor]) instead of Laravel's native page, and emits JSON:API-shaped pagination links. Architecturally it is a convention adapter, not a JSON:API implementation. It does not touch serialization, content negotiation, application/vnd.api+json media types, relationships/includes, sparse fieldsets, sorting, or error objects. If our roadmap is "become JSON:API compliant," this package covers roughly the pagination slice and nothing else; a full framework (e.g. Laravel JSON:API) would be the strategic bet. If our need is "our clients expect page[number]/page[size] and a links object," this is a near-perfect fit with almost zero architectural surface area.
Integration feasibility. Very high. Composer install, Laravel auto-discovery, optional published config (max_results, default_size, parameter names, method_name, simple/cursor/fast toggles), then a mechanical swap of ->paginate() for ->jsonPaginate() at the endpoints we choose. It composes with existing where clauses and relation queries, and accepts per-call overrides for max/default size and even a pre-computed total. Because the method name is configurable, collision risk with existing macros is controllable. Estimated effort: hours for a pilot endpoint, days for a full rollout plus contract tests.
Technical risk. Low-to-moderate, and mostly bounded:
max_results default 30) but not what happens on page[size]=1000 or page[number]=0 — clamping vs. a JSON:API-required 400. We must pin this down with tests before publishing a client-facing contract.base_url config as a design flaw (it overwrote the path, collapsing /api/users and /api/posts to the same URL). The replacement is URL::forceScheme('https') / URL::forceRootUrl(...) in a service provider — which means correct links depend on our proxy/TLS-termination configuration, not the package. This is the single most likely production surprise.base_url; we should adopt v4 directly and skip the legacy path.Key questions.
links object, and do any depend on last/prev semantics that cursor mode changes?forceRootUrl/forceScheme already configured?DISTINCT or expensive joins that make the total count a bottleneck?Stack fit. Requires a Laravel application with Eloquent; PHP 8.x and Laravel 10/11-class versions are supported per the changelog. No database-specific assumptions beyond Laravel's paginators. Cursor pagination requires an ordered, unique-ish column set (Laravel's constraint, not the package's). Fast pagination is strictly optional and pulls in spatie/laravel-fast-paginate (or recognizes the hammerstone/aaronfrancis variants) — treat that as a separate, later decision.
Migration path. Adopt v4+ directly to avoid the base_url trap. Steps: (1) install and publish config with an explicit max_results/default_size we can defend to clients; (2) configure URL forcing in AppServiceProvider and validate generated links in staging behind the same proxy topology as production; (3) replace paginate() with jsonPaginate() endpoint-by-endpoint, starting with a single low-traffic read endpoint; (4) add contract tests asserting request parameter names and the links shape; (5) only then broaden.
Compatibility. The macro approach is additive, so existing page=-style consumers keep working on untouched endpoints — this enables a genuinely incremental, per-endpoint cutover rather than a flag day. Watch for: custom paginator subclasses or response transformers that assume numeric page, existing macros registered under the same name, and any tests asserting on meta/links keys. Cursor mode is a global config switch (use_cursor_pagination overrides use_simple_pagination), which is a real constraint — it cannot be scoped per endpoint via config alone, so mixing cursor and offset endpoints likely requires per-call handling or a deliberate single-strategy decision.
Sequencing.
use_fast_pagination where the DB is the bottleneck.Maintenance. Very low. MIT license, Spatie-maintained, small dependency surface, semver releases and a conventional changelog. The recurring cost is Laravel major-version cadence: the changelog shows compatibility PRs landing promptly for Laravel 10 and 11 (including automated Laravel Shift PRs), which is a good signal but also means we should expect at least an annual dependency bump. Budget a few hours per Laravel upgrade. Pin the version and let Renovate/Dependabot surface releases.
Support. Upstream support is community/issue-tracker with a security disclosure email; there is no vendor SLA. For a package this small, that is acceptable — the code path is shallow and we can fork or vendor it if upstream stalls. Internally, ownership should sit with the API platform team, with the pagination contract documented in our public API reference so client teams have a single source of truth rather than reading the Spatie README.
Scaling. Two distinct concerns. Volume is fine — this is a query-shaping macro with negligible overhead. Depth is not: offset pagination degrades linearly with page number, and the package's default 30-row cap will silently limit large page-size requests. For high-traffic or deep-paging endpoints, plan on cursor pagination (best DB performance, but changes link semantics — no numeric last) or fast pagination (deferred join, transparent to clients, lowest migration cost). Note the documented limitation that the manual $total argument has no effect under cursor, simple, or fast pagination — a footgun if we adopt it as a workaround for expensive counts.
Failure modes.
page[size]=200 receives 30 rows with no error, reads it as a short page, and stops paging. Detect via contract tests and explicit docs.base_url bug class. Detect with a staging smoke test that asserts full URL paths per endpoint.DISTINCT/join-heavy queries make the count the slowest part of the request. Mitigate with simple pagination (no total) where the UI doesn't need a page count.jsonPaginate is silently overridden or overrides. Verify in a boot test.Ramp-up. Essentially zero for Laravel developers — the mental model is "paginate() but spec-shaped." Engineering onboarding is a paragraph in the API guide. The real ramp-up is on the consumer side: client teams need the parameter names, the page-size ceiling, and the paging-termination rule documented. Allocate a short enablement note plus updated OpenAPI examples as part of the rollout, not after.
How can I help you explore Laravel packages today?