Laravel API Resources: Sparse Fieldsets &amp; Cursor Pagination | 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, Conditional Relationships, and Cursor Pagination

 Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Cursor Pagination
==========================================================================================

 Go beyond basic JsonResource usage. Learn how to implement sparse fieldsets, conditionally load relationships without N+1 issues, and wire up cursor pagination for high-throughput Laravel APIs.

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

ShareCopy linkCopied

 ![Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Cursor Pagination](https://cdn.msaied.com/710/4f310773d50e6b3a7ab53e64d3e76921.png) 

  On this page +1. [Beyond Basic JsonResource](#beyond-basic-jsonresource)
2. [Sparse Fieldsets](#sparse-fieldsets)
3. [Conditional Relationships Without N+1](#conditional-relationships-without-n1)
4. [Nested Conditional Data](#nested-conditional-data)
5. [Cursor Pagination at Scale](#cursor-pagination-at-scale)
6. [Exposing Pagination Links in Resources](#exposing-pagination-links-in-resources)
7. [Takeaways](#takeaways)

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

Most Laravel APIs start with a `JsonResource` that dumps every column and calls it a day. That works until clients complain about payload size, mobile teams beg for field filtering, and your DBA notices the missing indexes on `OFFSET`-based pagination. This article tackles all three with concrete, production-ready patterns.

---

Sparse Fieldsets
----------------

Sparse fieldsets let clients request only the columns they need — a pattern borrowed from JSON:API. Implement it cleanly by reading a `fields` query parameter inside your resource.

```php
// app/Http/Resources/PostResource.php
class PostResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        $fields = $this->requestedFields($request, 'posts');

        $all = [
            'id'         => $this->id,
            'title'      => $this->title,
            'body'       => $this->body,
            'created_at' => $this->created_at->toIso8601String(),
            'author'     => new UserResource($this->whenLoaded('author')),
        ];

        return $fields ? array_intersect_key($all, array_flip($fields)) : $all;
    }

    private function requestedFields(Request $request, string $type): array
    {
        $raw = $request->query('fields', []);
        if (is_string($raw)) {
            return [];
        }
        return array_map('trim', explode(',', $raw[$type] ?? ''));
    }
}

```

A request like `GET /posts?fields[posts]=id,title` now returns only those two keys. Keep the logic in a `HasSparseFieldsets` trait if you need it across many resources.

---

Conditional Relationships Without N+1
-------------------------------------

`whenLoaded()` is the correct primitive, but the real trap is forgetting to eager-load based on what the client actually requested.

```php
// app/Http/Controllers/PostController.php
public function index(Request $request): AnonymousResourceCollection
{
    $includes = array_filter(
        explode(',', $request->query('include', '')),
        fn(string $rel) => in_array($rel, ['author', 'tags', 'comments'], true)
    );

    $posts = Post::query()
        ->with($includes)          // only load what was asked for
        ->cursorPaginate(25);

    return PostResource::collection($posts);
}

```

Whitelisting allowed includes server-side prevents clients from triggering arbitrary eager loads. Pair this with `whenLoaded()` in the resource and you get zero N+1 queries regardless of which includes the client sends.

### Nested Conditional Data

For attributes that are expensive to compute, use `when()`:

```php
'read_time' => $this->when(
    $request->boolean('meta'),
    fn() => $this->computeReadTime()
),

```

The closure is only evaluated when the condition is truthy, so the computation never runs for clients that don't need it.

---

Cursor Pagination at Scale
--------------------------

`OFFSET` pagination degrades as page numbers grow because the database must scan and discard all preceding rows. Cursor pagination solves this by encoding the last-seen position in an opaque token.

```php
$posts = Post::query()
    ->orderBy('id')
    ->cursorPaginate(25);

return PostResource::collection($posts);
// Response includes:
// "next_cursor": "eyJpZCI6MTAwfQ"
// "prev_cursor": "eyJpZCI6NzZ9"

```

Laravel's `cursorPaginate()` automatically adds a `WHERE id > ?` clause using the decoded cursor, keeping the query O(1) relative to dataset size.

**One constraint:** cursor pagination requires a stable, unique sort column (or composite). Sorting by `created_at` alone breaks when two rows share the same timestamp. Add `id` as a tiebreaker:

```php
->orderBy('created_at')
->orderBy('id')
->cursorPaginate(25);

```

Laravel encodes both columns in the cursor automatically.

### Exposing Pagination Links in Resources

```php
return PostResource::collection($posts)
    ->additional([
        'links' => [
            'next' => $posts->nextPageUrl(),
            'prev' => $posts->previousPageUrl(),
        ],
    ]);

```

---

Takeaways
---------

- **Sparse fieldsets** reduce payload size and keep serialization logic in one place — use a per-type `fields` query parameter.
- **Whitelist includes** server-side before passing them to `with()`; never trust raw client input for eager loading.
- **`whenLoaded()` + `when()`** are your tools for zero-cost conditional data — lean on closures to defer expensive computation.
- **Cursor pagination** is always preferable to offset for large, append-heavy tables; ensure your sort key is unique or add a tiebreaker.
- Keep resource classes thin: move field-filtering and include-parsing logic into traits or dedicated resolver classes as the API grows.

- [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)
- [performance](https://www.msaied.com/public/articles?search=performance)

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

  Can sparse fieldsets break API consumers that expect a fixed schema?Yes, if consumers rely on every field always being present. Document the default (all fields) clearly and treat sparse fieldsets as an opt-in optimization. Consider versioning your API contract separately from the fieldset feature.

   When should I prefer offset pagination over cursor pagination?Offset pagination is acceptable for small, rarely-updated datasets where clients need random page access (e.g., jump to page 10). For large or frequently-inserted tables, cursor pagination is almost always the right choice.

   How do I test that N+1 queries are not introduced when new includes are added?Use `DB::enableQueryLog()` in a Pest test or the `assertQueryCount()` helper from the `laravel-query-detector` package. Assert the exact query count for a known set of includes so regressions surface immediately in CI.

   ![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 articleClean Architecture Testing with Pest: Actions, Fakes, and Architectural Assertions](https://www.msaied.com/public/articles/clean-architecture-testing-with-pest-actions-fakes-and-architectural-assertions-1) [Next articleTashil: Add Subscription Plans and Feature Usage Limits to Laravel](https://www.msaied.com/public/articles/tashil-add-subscription-plans-and-feature-usage-limits-to-laravel)  

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

1. [Beyond Basic JsonResource](#beyond-basic-jsonresource)
2. [Sparse Fieldsets](#sparse-fieldsets)
3. [Conditional Relationships Without N+1](#conditional-relationships-without-n1)
4. [Nested Conditional Data](#nested-conditional-data)
5. [Cursor Pagination at Scale](#cursor-pagination-at-scale)
6. [Exposing Pagination Links in Resources](#exposing-pagination-links-in-resources)
7. [Takeaways](#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/743/8998fac3a41451ab3fe1588194e17a43.png) Filament · 3 min read### Securing Filament Plugins with Plumb: Automated Security Scoring for PHP Packages

5 Oct 2026 ](https://www.msaied.com/public/articles/securing-filament-plugins-with-plumb-automated-security-scoring-for-php-packages) [ ![](https://cdn.msaied.com/742/2d02018669cdeedccb5de2efb898f0ee.png) Filament · 3 min read### Filament v3.3.56 Released: File Hash Names and Livewire Upload Fix

5 Oct 2026 ](https://www.msaied.com/public/articles/filament-v3356-released-file-hash-names-and-livewire-upload-fix) [ ![](https://cdn.msaied.com/741/5b55c123ad08e4d34e1f4b99ad6a428b.png)  · 3 min read### Filament v4 Schema-Based Forms, Infolists, and the Unified Schema API

5 Oct 2026 ](https://www.msaied.com/public/articles/filament-v4-schema-based-forms-infolists-and-the-unified-schema-api-5) 

  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)
