DEV3 Technologies home

API resource with sparse fieldsets

Let API clients request only the fields they need with a fields query parameter, applied inside a Laravel JSON resource using a small trait.

LaravelAPIsapi-resourcejsonfieldsetsperformance

Code

<?php

namespace App\Http\Resources\Concerns;

use Illuminate\Http\Request;

trait SparseFields
{
    /** Fields a client may ask for. Keep sensitive columns out of this list. */
    abstract protected function allowed(): array;

    /** Fields returned when the client does not ask for a subset. */
    protected function defaults(): array
    {
        return $this->allowed();
    }

    /**
     * Filter an array of field => value|Closure by the ?fields= parameter.
     * Closures run only for kept fields, so skipped fields cost nothing.
     */
    protected function only(array $attributes, ?Request $request = null): array
    {
        $request ??= request();

        $asked = array_filter(array_map('trim', explode(',', (string) $request->query('fields'))));
        $keep = $asked
            ? array_values(array_intersect($asked, $this->allowed()))
            : $this->defaults();

        // The id always travels with the payload so clients can key rows.
        $keep = array_unique(['id', ...$keep]);

        $out = [];
        foreach ($keep as $field) {
            if (array_key_exists($field, $attributes)) {
                $value = $attributes[$field];
                $out[$field] = $value instanceof \Closure ? $value() : $value;
            }
        }

        return $out;
    }
}

How it works

What it solves

Mobile apps and dashboards often need two or three fields from a resource that has twenty. Sending everything wastes bandwidth and forces you to ship a new endpoint for each screen. A ?fields=id,name,price parameter, as in JSON:API, lets the client trim the response without any new routes.

How it works

SparseFields is a trait for JsonResource classes. It reads the fields query parameter, splits it on commas, and intersects the request with a per-resource allow-list so clients cannot ask for hidden columns such as cost_price. Its only() method takes the full attribute array built by the resource and returns just the requested keys. When no parameter is given you get the default set.

ProductResource defines allowed() and builds its array lazily with closures. Closures matter: expensive values, like a computed price with tax, are only evaluated for fields that survive the filter, so a sparse request is also cheaper on the server.

Relationships still use whenLoaded, which keeps lazy loading out of the resource.

Adapting it

Add another resource by using the trait and defining allowed(). For nested resources prefix the parameter, for example fields[product]=name, and read that key instead.

Pitfalls

  • Always allow-list; never echo the client's list straight into the output.
  • Always include the id so clients can key their lists.
  • Cache keys for responses must include the fields value.

How to use it

  1. Copy the trait to app/Http/Resources/Concerns.
  2. Make your resource use SparseFields; and return $this->only([...closures...]) from toArray.
  3. Return ProductResource::collection($query->paginate()) from your controller as usual.
  4. Request GET /api/products?fields=id,name,price.
  5. Unknown field names are ignored silently; missing parameter returns the default set.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets