php 96 lines · 3 tabs

Cursor Pagination in Laravel with API Resources and Encoded Cursors

Shared by codesnips Aug 2026
3 tabs
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Comment extends Model
{
    protected $fillable = [
        'post_id',
        'author_id',
        'body',
    ];

    protected $casts = [
        'created_at' => 'datetime',
    ];

    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class, 'author_id');
    }

    public function scopeForPost($query, int $postId)
    {
        return $query->where('post_id', $postId);
    }
}
3 files · php Explain with highlit

Cursor pagination avoids the classic problems of offset pagination on large, frequently-changing tables. Instead of counting rows and skipping OFFSET N (which forces the database to walk and discard rows and can duplicate or skip records when rows are inserted mid-scan), a cursor encodes the position of the last row seen and the query resumes with a WHERE (created_at, id) < (?, ?) comparison against an indexed key. Laravel ships first-class support for this through Model::cursorPaginate(), and this snippet wires it into a JSON API cleanly.

The Comment model shown here is deliberately minimal: it declares $fillable and casts created_at so the ordering column behaves consistently. The important detail is that cursor pagination requires a deterministic, unique ordering. Ordering by created_at alone is not enough because timestamps collide, so the latest() plus an explicit orderBy('id') in the controller gives the compound key that makes the cursor stable and tie-broken.

CommentController is where the pagination happens. It reads per_page but clamps it with min(..., 100) so a client cannot request an unbounded page size. cursorPaginate() returns a CursorPaginator, not a LengthAwarePaginator — there is no total count, which is exactly why it is fast. That paginator is handed to CommentResource::collection(...), letting the resource layer control the JSON shape while the paginator supplies the cursor metadata.

CommentResource defines the per-item representation in toArray(), keeping database columns from leaking directly to clients. The interesting part is the static collection() override that returns an AnonymousResourceCollection whose additional() payload carries next_cursor, prev_cursor, and has_more. The cursor objects returned by nextCursor() and previousCursor() are opaque, base64-encoded strings that already contain the last row's key — clients simply echo them back via ?cursor=, and withQueryString() preserves other filters.

The trade-off is that cursor pagination cannot jump to an arbitrary page or show a total, so it suits infinite-scroll feeds and API cursors rather than numbered page UIs. It also breaks if the ordering columns change between requests, so the sort must be fixed. When those constraints fit, this pattern gives stable, index-friendly pagination that stays fast no matter how deep a client scrolls.


Related snips

Share this code

Here's the card — post it anywhere.

Cursor Pagination in Laravel with API Resources and Encoded Cursors — share card
Link copied