Laravel API Resources: Fieldsets, Relations &amp; Versioning | 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 Versioning

 Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Versioning
===================================================================================

 Go beyond basic transformers. Learn how to implement sparse fieldsets, conditionally load relationships, and version your API resources without duplicating transformation logic across your Laravel application.

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

ShareCopy linkCopied

 ![Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Versioning](https://cdn.msaied.com/400/67fe9d3c6c505a6f67092e2761690696.png) 

  On this page +1. [Beyond Basic toArray: Production-Grade API Resources](#beyond-basic-codetoarraycode-production-grade-api-resources)
2. [Sparse Fieldsets](#sparse-fieldsets)
3. [Conditional Relationships Without N+1](#conditional-relationships-without-n1)
4. [Versioning Without Class Duplication](#versioning-without-class-duplication)
5. [Takeaways](#takeaways)

 Beyond Basic `toArray`: Production-Grade API Resources
------------------------------------------------------

Most Laravel tutorials stop at `$this->merge([...])`. In a real SaaS API you need three things most examples skip: sparse fieldsets so clients fetch only what they need, conditional relationship loading that doesn't trigger N+1 queries, and a versioning strategy that doesn't mean copy-pasting resource classes.

---

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

The JSON:API spec defines `?fields[users]=id,name,email`. Implementing it cleanly inside a resource keeps the controller unaware of field filtering.

```php
// app/Http/Resources/Concerns/SparseFieldset.php
trait SparseFieldset
{
    protected function sparseFields(string $type, array $fields): array
    {
        $requested = request()
            ->input("fields.{$type}");

        if (! $requested) {
            return $fields;
        }

        $allowed = array_flip(explode(',', $requested));

        return array_filter(
            $fields,
            fn ($key) => isset($allowed[$key]),
            ARRAY_FILTER_USE_KEY
        );
    }
}

```

```php
// app/Http/Resources/UserResource.php
class UserResource extends JsonResource
{
    use SparseFieldset;

    public function toArray(Request $request): array
    {
        return $this->sparseFields('users', [
            'id'         => $this->id,
            'name'       => $this->name,
            'email'      => $this->email,
            'created_at' => $this->created_at->toIso8601String(),
        ]);
    }
}

```

A request to `GET /users?fields[users]=id,name` now returns only those two keys. The trait is reusable across every resource type.

---

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

Laravel's `whenLoaded` is well-known, but the pattern breaks down when you want to include a relationship only when the client explicitly requests it via `?include=posts`.

```php
// app/Http/Resources/UserResource.php
public function toArray(Request $request): array
{
    $includes = array_flip(
        explode(',', $request->input('include', ''))
    );

    return $this->sparseFields('users', [
        'id'    => $this->id,
        'name'  => $this->name,
        'posts' => $this->when(
            isset($includes['posts']) && $this->relationLoaded('posts'),
            fn () => PostResource::collection($this->posts)
        ),
    ]);
}

```

The controller is responsible for eager-loading based on the same `include` parameter:

```php
// app/Http/Controllers/UserController.php
public function index(Request $request): AnonymousResourceCollection
{
    $allowed = ['posts', 'team'];
    $includes = array_intersect(
        explode(',', $request->input('include', '')),
        $allowed
    );

    $users = User::with($includes)
        ->paginate(25);

    return UserResource::collection($users);
}

```

This pattern keeps the resource honest: it will never serialize a relationship that wasn't loaded, and the controller never leaks transformation logic.

---

Versioning Without Class Duplication
------------------------------------

The naive approach is `V1\UserResource` and `V2\UserResource`. That quickly becomes a maintenance nightmare. A cleaner approach uses a version-aware base resource and a small resolver.

```php
// app/Http/Resources/UserResource.php
class UserResource extends JsonResource
{
    use SparseFieldset;

    public function toArray(Request $request): array
    {
        $base = [
             'id'   => $this->id,
             'name' => $this->name,
        ];

        return match ($this->apiVersion($request)) {
            2       => array_merge($base, $this->v2Fields()),
            default => $base,
        };
    }

    private function v2Fields(): array
    {
        return [
            'email'      => $this->email,
            'created_at' => $this->created_at->toIso8601String(),
        ];
    }

    private function apiVersion(Request $request): int
    {
        // Accept: application/vnd.api+json; version=2
        preg_match('/version=(\d+)/', $request->header('Accept', ''), $m);
        return isset($m[1]) ? (int) $m[1] : 1;
    }
}

```

Version detection lives in one place. Adding V3 means adding a `v3Fields()` method and a new `match` arm — not a new file tree.

---

Takeaways
---------

- **Sparse fieldsets** belong in a reusable trait, not in controllers or query scopes.
- **`whenLoaded` + explicit include parsing** prevents accidental N+1 while keeping resources declarative.
- **Version branching inside a single resource class** is maintainable up to ~3 versions; beyond that, extract a versioned transformer layer.
- Always whitelist `include` values in the controller — never pass user input directly to `with()`.
- Pair resources with `JsonResource::withoutWrapping()` only at the outermost collection level to avoid breaking nested serialization.

- [laravel](https://www.msaied.com/public/articles?search=laravel)
- [api](https://www.msaied.com/public/articles?search=api)
- [resources](https://www.msaied.com/public/articles?search=resources)
- [json-api](https://www.msaied.com/public/articles?search=json-api)

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

  Does sparse fieldset filtering affect database queries?Not automatically. The trait filters the PHP array after the model is hydrated. To push field selection to the database you would need to parse the fields parameter in the controller and pass a select() clause — useful for wide tables but adds coupling between HTTP and query layers.

   When should I move to separate versioned resource classes instead of branching inside one class?Once a version introduces structural changes — renamed keys, removed relationships, or different pagination envelopes — a shared class becomes hard to reason about. A practical rule: branch inside one class for additive changes (new fields), split into separate classes when you need to remove or rename existing keys.

   ![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 articleLaravel Eloquent Global Scopes: Bootable Traits, Scope Removal, and Testing Pitfalls](https://www.msaied.com/public/articles/laravel-eloquent-global-scopes-bootable-traits-scope-removal-and-testing-pitfalls) [Next articleLaravel New in 12: First-Class Typed Config, Slim Skeletons, and Upgrade Notes](https://www.msaied.com/public/articles/laravel-new-in-12-first-class-typed-config-slim-skeletons-and-upgrade-notes)  

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

1. [Beyond Basic toArray: Production-Grade API Resources](#beyond-basic-codetoarraycode-production-grade-api-resources)
2. [Sparse Fieldsets](#sparse-fieldsets)
3. [Conditional Relationships Without N+1](#conditional-relationships-without-n1)
4. [Versioning Without Class Duplication](#versioning-without-class-duplication)
5. [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)
