We just released v7 of laravel-query-builder, our package that makes it easy to build flexible API endpoints. If you’re building an API with Laravel, you’ll almost certainly need to let consumers filter results, sort them, include relationships and select specific fields. Writing that logic by hand for every endpoint gets repetitive fast, and it’s easy to accidentally expose columns or relationships you didn’t intend to.

Our query builder takes care of all of that. It reads query parameters from the URL, translates them into the right Eloquent queries, and makes sure only the things you’ve explicitly allowed can be queried.

// GET /users?filter[name]=John&include=posts&sort=-created_at

$users = QueryBuilder::for(User::class)
    ->allowedFilters('name')
    ->allowedIncludes('posts')
    ->allowedSorts('created_at')
    ->get();

// select * from users where name = 'John' order by created_at desc

This major version requires PHP 8.3+ and Laravel 12 or higher, and brings a cleaner API along with some features we’ve been wanting to add for a while.

Let me walk you through how the package works and what’s new.

Using the package

The idea is simple: your API consumers pass query parameters in the URL, and the package translates those into the right Eloquent query. You just define what’s allowed.

Say you have a User model and you want to let API consumers filter by name. Here’s all you need:

use SpatieQueryBuilderQueryBuilder;

$users = QueryBuilder::for(User::class)
    ->allowedFilters('name')
    ->get();

Now when someone requests /users?filter[name]=John, the package adds the appropriate WHERE clause to the query:

select * from users where name = 'John'

Only the filters you’ve explicitly allowed will work. If someone tries /users?filter[secret_column]=something, the package throws an InvalidFilterQuery exception. Your database schema stays hidden from API consumers.

You can allow multiple filters at once and combine them with sorting:

$users = QueryBuilder::for(User::class)
    ->allowedFilters('name', 'email')
    ->allowedSorts('name', 'created_at')
    ->get();

A request to /users?filter[name]=John&sort=-created_at now filters by name and sorts by created_at descending (the - prefix means descending).

Including relationships works the same way. If you want consumers to be able to eager-load a user’s posts:

$users = QueryBuilder::for(User::class)
    ->allowedFilters('name', 'email')
    ->allowedIncludes('posts', 'permissions')
    ->allowedSorts('name', 'created_at')
    ->get();

A request to /users?include=posts&filter[name]=John&sort=-created_at now returns users named John, sorted by creation date, with their posts eager-loaded.

You can also select specific fields to keep your responses lean:

$users = QueryBuilder::for(User::class)
    ->allowedFields('id', 'name', 'email')
    ->allowedIncludes('posts')
    ->get();

With /users?fields=id,email&include=posts, only the id and email columns are selected.

The QueryBuilder extends Laravel’s default Eloquent builder, so all your favorite methods still work. You can combine it with existing queries:

$query = User::where('active', true);

$users = QueryBuilder::for($query)
    ->withTrashed()
    ->allowedFilters('name')
    ->allowedIncludes('posts', 'permissions')
    ->where('score', '>', 42)
    ->get();

The query parameter names follow the JSON API specification as closely as possible. This means you get a consistent, well-documented API surface without having to think about naming conventions.

What’s new in v7

Variadic parameters

All the allowed* methods now accept variadic arguments instead of arrays.

// Before (v6)
QueryBuilder::for(User::class)
    ->allowedFilters(['name', 'email'])
    ->allowedSorts(['name'])
    ->allowedIncludes(['posts']);

// After (v7)
QueryBuilder::for(User::class)
    ->allowedFilters('name', 'email')
    ->allowedSorts('name')
    ->allowedIncludes('posts');

If you have a dynamic list, use the spread operator:

$filters = ['name', 'email'];
QueryBuilder::for(User::class)->allowedFilters(...$filters);

Aggregate includes

This is the biggest new feature. You can now include aggregate values for related models using AllowedInclude::min(), AllowedInclude::max(), AllowedInclude::sum(), and AllowedInclude::avg(). Under the hood, these map to Laravel’s withMin(), withMax(), withSum() and withAvg() methods.

use SpatieQueryBuilderAllowedInclude;

$users = QueryBuilder::for(User::class)
    ->allowedIncludes(
        'posts',
        AllowedInclude::count('postsCount'),
        AllowedInclude::sum('postsViewsSum', 'posts', 'views'),
        AllowedInclude::avg('postsViewsAvg', 'posts', 'views'),
    )
    ->get();

A request to /users?include=posts,postsCount,postsViewsSum now returns users with their posts, the post count, and the total views across all posts.

You can constrain these aggregates too. For example, to only count published posts:

use SpatieQueryBuilderAllowedInclude;
use IlluminateDatabaseEloquentBuilder;

$users = QueryBuilder::for(User::class)
    ->allowedIncludes(
        AllowedInclude::count(
            'publishedPostsCount',
            'posts',
            fn (Builder $query) => $query->where('published', true)
        ),
        AllowedInclude::sum(
            'publishedPostsViewsSum',
            'posts',
            'views',
            constraint: fn (Builder $query) => $query->where('published', true)
        ),
    )
    ->get();

All four aggregate types support these constraint closures, making it possible to build endpoints that return computed data alongside your models without writing custom query logic.

A perfect match for Laravel’s JSON:API resources

Laravel 13 is adding built-in support for JSON:API resources. These new JsonApiResource classes handle the serialization side: they produce responses compliant with the JSON:API specification.

You create one by adding the --json-api flag:

php artisan make:resource PostResource --json-api

This generates a resource class where you define attributes and relationships:

use IlluminateHttpResourcesJsonApiJsonApiResource;

class PostResource extends JsonApiResource
{
    public $attributes = [
        'title',
        'body',
        'created_at',
    ];

    public $relationships = [
        'author',
        'comments',
    ];
}

Return it from your controller, and Laravel produces a fully compliant JSON:API response:

{
    "data": {
        "id": "1",
        "type": "posts",
        "attributes": {
            "title": "Hello World",
            "body": "This is my first post."
        },
        "relationships": {
            "author": {
                "data": { "id": "1", "type": "users" }
            }
        }
    },
    "included": [
        {
            "id": "1",
            "type": "users",
            "attributes": { "name": "Taylor Otwell" }
        }
    ]
}

Clients can request specific fields and includes via query parameters like /api/posts?fields[posts]=title&include=author. Laravel’s JSON:API resources handle all of that on the response side.

The Laravel docs explicitly mention our package as a companion:

Laravel’s JSON:API resources handle the serialization of your responses. If you also need to parse incoming JSON:API query parameters such as filters and sorts, Spatie’s Laravel Query Builder is a great companion package.

So while Laravel’s new JSON:API resources take care of the output format, our query builder handles the input side: parsing filter, sort, include and fields parameters from the request and translating them into the right Eloquent queries. Together they give you a full JSON:API implementation with very little boilerplate.

In closing

To upgrade from v6, check the upgrade guide. The changes are mostly mechanical. Check the guide for the full list.

You can find the full source code and documentation on GitHub. We also have extensive documentation on the Spatie website.

This is one of the many packages we’ve created at Spatie. If you want to support our open source work, consider picking up one of our paid products.

Categories: PHP