Laravel API Resources, Cursor Pagination &amp; Rate Limiting | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com/public) - [Home](https://www.msaied.com/public)
- [Projects](https://www.msaied.com/public/projects)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [About](https://www.msaied.com/public#about)

           [  Contact](https://www.msaied.com/public#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com/public)
- [ProjectsCase studies](https://www.msaied.com/public/projects)
- [ArticlesEngineering notes](https://www.msaied.com/public/articles)
- [CertificatesCredentials](https://www.msaied.com/public/certificates)
- [AboutHow I work](https://www.msaied.com/public#about)
- [ContactGet in touch](https://www.msaied.com/public#contact)

  [Start a conversation](https://www.msaied.com/public#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com/public)
2. /
3. [Articles](https://www.msaied.com/public/articles)
4. /
5. Laravel API Resources: Sparse Fieldsets, Cursor Pagination, and Per-Route Rate Limiting

 Laravel API Resources: Sparse Fieldsets, Cursor Pagination, and Per-Route Rate Limiting
========================================================================================

 Go beyond basic JsonResource usage. This guide covers sparse fieldsets, cursor-based pagination for large datasets, and per-route rate limiting strategies that keep your Laravel API fast and predictable under load.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com/public#person) Published 29 Aug 2026 · Updated 29 Aug 2026 · 4 min read

ShareCopy linkCopied

 ![Laravel API Resources: Sparse Fieldsets, Cursor Pagination, and Per-Route Rate Limiting](https://cdn.msaied.com/606/93349b03f4527b9100157c6774bb4ce2.png) 

  On this page +1. [Beyond Basic JsonResource](#beyond-basic-jsonresource)
2. [Sparse Fieldsets Without a Package](#sparse-fieldsets-without-a-package)
3. [Cursor Pagination for Large Datasets](#cursor-pagination-for-large-datasets)
4. [What the Query Actually Looks Like](#what-the-query-actually-looks-like)
5. [Per-Route Rate Limiting with Named Limiters](#per-route-rate-limiting-with-named-limiters)
6. [Surfacing Limit Headers](#surfacing-limit-headers)
7. [Key Takeaways](#key-takeaways)

 Beyond Basic JsonResource
-------------------------

Most Laravel APIs start with a thin `JsonResource` wrapper and a `paginate()` call. That works until your payloads balloon, your cursors drift, and a single client hammers one endpoint. This article tackles three concrete improvements you can ship today.

---

Sparse Fieldsets Without a Package
----------------------------------

JSON:API defines sparse fieldsets (`?fields[resource]=id,name,email`). You can implement a lightweight version directly in a base resource.

```php
// app/Http/Resources/SparseResource.php
abstract class SparseResource extends JsonResource
{
    protected function sparse(array $fields): array
    {
        $requested = collect(
            explode(',', request()->query('fields', ''))
        )->filter()->values();

        if ($requested->isEmpty()) {
            return $fields;
        }

        return array_intersect_key($fields, array_flip($requested->all()));
    }
}

```

```php
// app/Http/Resources/UserResource.php
class UserResource extends SparseResource
{
    public function toArray(Request $request): array
    {
        return $this->sparse([
            'id'         => $this->id,
            'name'       => $this->name,
            'email'      => $this->email,
            'created_at' => $this->created_at->toISOString(),
        ]);
    }
}

```

A request to `GET /users?fields=id,name` now returns only those two keys. No extra package, no reflection magic — just an `array_intersect_key` on the resolved field map.

> **Tip:** Validate allowed fields in a Form Request to prevent leaking internal column names.

---

Cursor Pagination for Large Datasets
------------------------------------

`paginate()` uses `OFFSET`, which forces the database to scan all preceding rows. On a table with millions of records that becomes expensive fast. `cursorPaginate()` uses a keyset derived from the last seen row.

```php
// routes/api.php
Route::get('/events', function (Request $request) {
    return EventResource::collection(
        Event::query()
            ->orderBy('id')
            ->cursorPaginate(50)
    );
});

```

The response includes `next_cursor` and `prev_cursor` tokens. Clients pass `?cursor=` on subsequent requests.

### What the Query Actually Looks Like

With `orderBy('id')` and a cursor pointing at id `1000`, Laravel generates:

```sql
SELECT * FROM events WHERE id > 1000 ORDER BY id ASC LIMIT 51;

```

That `51` is intentional — Laravel fetches one extra row to determine whether a next page exists, then discards it. The query hits the primary key index regardless of table size.

**Caveats:**

- Cursor pagination requires a stable, unique sort column (or composite).
- You cannot jump to an arbitrary page — it is forward/backward only.
- Use `CursorPaginator::currentCursorName()` if you need a custom query-string key.

---

Per-Route Rate Limiting with Named Limiters
-------------------------------------------

The global `throttle:60,1` middleware is too blunt for a real API. Define named limiters in `AppServiceProvider` (or a dedicated `RateLimitServiceProvider`).

```php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

public function boot(): void
{
    RateLimiter::for('exports', function (Request $request) {
        return $request->user()
            ? Limit::perHour(10)->by($request->user()->id)
            : Limit::perHour(2)->by($request->ip());
    });

    RateLimiter::for('search', function (Request $request) {
        return [
            Limit::perMinute(30)->by($request->user()?->id ?? $request->ip()),
            Limit::perDay(5000)->by($request->user()?->id ?? $request->ip()),
        ];
    });
}

```

Attach them per route:

```php
Route::get('/reports/export', ExportController::class)
    ->middleware('throttle:exports');

Route::get('/search', SearchController::class)
    ->middleware('throttle:search');

```

Returning an **array** of `Limit` objects enforces multiple windows simultaneously — a burst guard (per-minute) and a daily budget in one declaration.

### Surfacing Limit Headers

Laravel automatically adds `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `Retry-After` headers when the limiter fires. Clients can back off gracefully without guessing.

---

Key Takeaways
-------------

- **Sparse fieldsets** reduce payload size with a single `array_intersect_key` — no package required.
- **`cursorPaginate()`** replaces `OFFSET` with a keyset query; always pair it with an indexed sort column.
- **Named rate limiters** let you apply different burst and daily budgets per endpoint, scoped to authenticated users or IP addresses.
- Returning an array of `Limit` objects from a limiter enforces multiple time windows at once.
- Laravel's built-in rate-limit headers give clients everything they need to implement polite retry logic.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [api](https://www.msaied.com/public/articles?search=api)
- [eloquent](https://www.msaied.com/public/articles?search=eloquent)
- [rate-limiting](https://www.msaied.com/public/articles?search=rate-limiting)

 Frequently asked questions 
---------------------------

  When should I prefer cursorPaginate() over paginate() in Laravel?Use cursorPaginate() whenever you are paginating large tables (hundreds of thousands of rows or more) and do not need random page access. It avoids the OFFSET scan by using a keyset derived from the last seen row, which keeps query time constant regardless of how deep into the dataset you are.

   Can I apply multiple rate limits to a single route in Laravel?Yes. Return an array of Limit objects from your named limiter closure. Laravel evaluates each limit independently, so you can enforce a per-minute burst cap and a per-day total cap simultaneously on the same route.

   Is the sparse fieldsets approach safe? Could clients request internal columns?The SparseResource pattern is safe because you define the allowed field map explicitly in toArray(). Clients can only request keys that already exist in that map — they cannot access raw database columns or relationships you have not exposed.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com/public#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com/public#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleCancel In-Flight Form Submissions in Inertia.js v3.7](https://www.msaied.com/public/articles/cancel-in-flight-form-submissions-in-inertiajs-v37) [Next articleRecursive CTEs and Hierarchical Data in Laravel with PostgreSQL](https://www.msaied.com/public/articles/recursive-ctes-and-hierarchical-data-in-laravel-with-postgresql)  

   On this page
-------------

1. [Beyond Basic JsonResource](#beyond-basic-jsonresource)
2. [Sparse Fieldsets Without a Package](#sparse-fieldsets-without-a-package)
3. [Cursor Pagination for Large Datasets](#cursor-pagination-for-large-datasets)
4. [What the Query Actually Looks Like](#what-the-query-actually-looks-like)
5. [Per-Route Rate Limiting with Named Limiters](#per-route-rate-limiting-with-named-limiters)
6. [Surfacing Limit Headers](#surfacing-limit-headers)
7. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com/public#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/740/cce86edc21eddcbdd2f2454fadaf9c70.png)  · 3 min read### The Pipeline Pattern in Laravel: Custom Pipelines Beyond Middleware

5 Oct 2026 ](https://www.msaied.com/public/articles/the-pipeline-pattern-in-laravel-custom-pipelines-beyond-middleware-1) [ ![](https://cdn.msaied.com/739/2d6897fdcdcf090613f96f72a64b8a78.png)  · 4 min read### MySQL Full-Text Search in Laravel: Indexes, Relevance Scoring, and Boolean Mode

4 Oct 2026 ](https://www.msaied.com/public/articles/mysql-full-text-search-in-laravel-indexes-relevance-scoring-and-boolean-mode) [ ![](https://cdn.msaied.com/738/073696a3fefe18bec825beec5ac658f5.png)  · 4 min read### Laravel Queue Rate-Limited Middleware: Throttling Jobs Without Losing Work

4 Oct 2026 ](https://www.msaied.com/public/articles/laravel-queue-rate-limited-middleware-throttling-jobs-without-losing-work) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com/public#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com/public)
- [Articles](https://www.msaied.com/public/articles)
- [Certificates](https://www.msaied.com/public/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/public/sitemap.xml)
