Laravel API Resources: Sparse Fieldsets, Conditional Relationships, and Versioning
#laravel #api #resources #versioning

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

3 min read Mohamed Said Mohamed Said

Beyond toArray: Making API Resources Do Real Work

Laravel's JsonResource class is deceptively simple. Most teams use it as a glorified array cast, but it carries enough power to handle sparse fieldsets, conditional relationship loading, and multi-version response contracts — without reaching for a dedicated API layer package.


Sparse Fieldsets Without a Package

JSON:API specifies that clients can request only the fields they need via ?fields[articles]=title,body. Implementing this in a resource is straightforward once you centralise the parsing.

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

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

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

        return array_intersect_key($fields, $keys);
    }
}
// app/Http/Resources/ArticleResource.php
class ArticleResource extends JsonResource
{
    use SparseFieldset;

    public function toArray(Request $request): array
    {
        return $this->allowed('articles', [
            'id'         => $this->id,
            'title'      => $this->title,
            'body'       => $this->body,
            'created_at' => $this->created_at,
        ]);
    }
}

The allowed() helper filters the field map to only what the client requested. No extra dependencies, no magic.


Conditional Relationship Loading

The built-in whenLoaded() is good, but it only checks whether the relation is already eager-loaded. For APIs that accept an ?include= parameter, you want to drive eager loading from the resource itself rather than the controller.

// app/Http/Resources/ArticleResource.php
public function with(Request $request): array
{
    return [];
}

public function toArray(Request $request): array
{
    return [
        'id'     => $this->id,
        'title'  => $this->title,
        'author' => $this->whenLoaded('author', fn () =>
            new UserResource($this->author)
        ),
        'tags'   => $this->whenLoaded('tags', fn () =>
            TagResource::collection($this->tags)
        ),
    ];
}

In the controller, parse ?include= once and eager-load accordingly:

// app/Http/Controllers/ArticleController.php
public function show(Article $article): ArticleResource
{
    $includes = array_intersect(
        explode(',', request()->input('include', '')),
        ['author', 'tags'] // allowlist
    );

    return new ArticleResource(
        $article->loadMissing($includes)
    );
}

This keeps the allowlist in the controller (where auth context lives) and the shape in the resource.


Clean API Versioning Without Route Duplication

The temptation is to create V1\ArticleResource and V2\ArticleResource as entirely separate files. That leads to drift. A better pattern: a single resource class that branches on a version header or route prefix.

// app/Http/Resources/ArticleResource.php
public function toArray(Request $request): array
{
    $version = $request->header('X-API-Version', 'v1');

    return match ($version) {
        'v2'    => $this->v2Fields(),
        default => $this->v1Fields(),
    };
}

private function v1Fields(): array
{
    return [
        'id'    => $this->id,
        'title' => $this->title,
    ];
}

private function v2Fields(): array
{
    return [
        'id'       => $this->id,
        'title'    => $this->title,
        'slug'     => $this->slug,
        'metadata' => $this->metadata,
    ];
}

When v1 is retired, delete v1Fields() and the match arm. No orphaned route files, no duplicated middleware stacks.

For larger APIs where versions diverge significantly, namespace the resource classes but share a base:

// app/Http/Resources/V2/ArticleResource.php
class ArticleResource extends \App\Http\Resources\ArticleResource
{
    public function toArray(Request $request): array
    {
        return array_merge(parent::toArray($request), [
            'slug' => $this->slug,
        ]);
    }
}

Inheritance is fine here — the relationship is genuinely "is-a".


Key Takeaways

  • Sparse fieldsets can be implemented with a single trait and array_intersect_key — no package needed.
  • Conditional includes belong in the controller (allowlist) and the resource (whenLoaded); never auto-load in the resource.
  • Version branching inside one resource class is maintainable for minor divergence; namespace inheritance works for major breaks.
  • Keep resources focused on shape, not business logic — computed values belong in model accessors or dedicated DTOs.
  • Always test resources with ArticleResource::make($model)->toArray(request()) in Pest; it's fast and catches regressions early.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Should I implement sparse fieldsets manually or use a JSON:API package like spatie/laravel-json-api-paginate?
For full JSON:API compliance (type objects, links, meta) a package is worth it. For a simpler REST API that just wants field filtering, the trait approach shown here avoids the overhead and keeps your resource shape under your control.
Q02 Is it safe to branch on a request header inside a resource's toArray method?
Yes, but inject the version via the constructor if you want the resource to be testable without a real HTTP request. Pass `$version` in and store it as a property — that makes unit testing each version branch trivial.
Q03 How do I handle pagination with sparse fieldsets in a collection resource?
The fieldset trait works per-item because each resource instance calls allowed() independently. ResourceCollection wraps the items, so pagination metadata is unaffected. Just ensure your collection class calls the item resource, not a raw array map.

Continue reading

More Articles

View all