spatie/laravel-query-builder: turning API query strings into Eloquent queries
Easily build Eloquent queries from API requests
At a glance
- What is it?
- A PHP package that maps filter, sort, include and field parameters from an HTTP request onto an Eloquent query, with an explicit allowlist that decides what a client may ask for. It fits Laravel JSON APIs; it is not a general SQL builder.
- Who is it for?
- Adopt it if you are exposing a Laravel model over HTTP and want the accepted filter, sort, include and field names declared in code rather than parsed from raw input. Do not adopt it as a general SQL builder: it builds on Eloquent, so joins, inserts, updates and deletes are not its subject, and the documentation covers filtering, including relationships, sorting and selecting fields instead.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 27 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 27, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem: request parameters that reach the database unchecked
A typical Laravel JSON endpoint receives something like /users?filter[name]=John&sort=id&include=posts. Without a layer in between, the controller has to inspect each parameter, decide whether it is legitimate, and translate it into the right Eloquent call. That code is repetitive, and the failure mode is quiet: a parameter that is not validated can end up selecting a column or a relation the caller was never meant to touch.
spatie/laravel-query-builder addresses that by inverting the decision. The developer declares which filters, sorts, includes and fields are acceptable, and the package only acts on parameters that match those declarations. Everything else in the query string is ignored rather than executed. The audience is Laravel developers writing public or internal APIs where the front end, a mobile client or a third party needs to shape the result set without a bespoke endpoint for every combination.
How QueryBuilder::for() maps a request onto an Eloquent builder
The entry point is the static for() method, which accepts either a model class name or an existing query builder instance. From there the chain mirrors what the request is allowed to do: allowedFilters(), allowedIncludes(), allowedSorts() and allowedFields(). The README's first example is QueryBuilder::for(User::class)->allowedFilters('name')->get(), which returns every User whose name contains the string "John".
Because for() also takes a builder, the package composes with code you already have. The README shows starting from User::where('active', true), passing that into QueryBuilder::for(), then chaining withTrashed() and where('score', '>', 42) alongside allowedIncludes('posts', 'permissions'). The query builder methods you already use keep working; the package adds a request-driven layer on top rather than replacing the builder.
Filtering has more depth than the single-string form suggests. AllowedFilter::partial() matches on a substring, AllowedFilter::exact() is available as a type, and the README documents scope filters, custom filters, ignored values and default filter values in the v7 filtering documentation. The grouping feature is the most interesting recent addition: AllowedFilter::groupOr('q', [...]) and AllowedFilter::groupAnd() let several filters share one request parameter. The README's example, /users?filter[q]=John, produces WHERE (name LIKE '%John%' OR full_name LIKE '%John%'), and combining it with filter[name]=Doe produces WHERE name LIKE '%Doe%' AND (name LIKE '%John%' OR full_name LIKE '%John%'). Members can be any AllowedFilter type and the shorthand value is broadcast to every member, which is what keeps the grouped form from needing one parameter per column.
Installing spatie/laravel-query-builder and a first endpoint
The README gives a single installation step: run Composer in the project root. The package is published on Packagist as spatie/laravel-query-builder.
composer require spatie/laravel-query-builderThe README then points to the installation notes on the docs site at https://spatie.be/docs/laravel-query-builder/v7/installation-setup for anything beyond the Composer step. The repository layout includes a config/ directory and a database/ directory, so there is more to configure than the README shows, but the installation page is where that is documented.
A first real use is a route that returns users and accepts a filter. The README's basic example is the shape to copy: declare the model, declare the one filter you accept, and call get().
use Spatie\QueryBuilder\QueryBuilder;
$users = QueryBuilder::for(User::class)
->allowedFilters('name')
->get();With that in place, GET /users?filter[name]=John returns the users whose name contains "John". A request with filter[email]=... would return the unfiltered list, because email was never declared as an allowed filter. That behaviour is the point of the package, and it is also the thing to verify in your own test suite before shipping.
Includes, sorts and field selection are separate allowlists
Each capability is opted into independently, which means a route can permit sorting but not relation loading, or relation loading but not arbitrary field selection. For includes, the README's example is allowedIncludes('posts') with a request of /users?include=posts, producing all users with their posts loaded. Nested relationships, relationship counts and custom includes are covered in the v7 including-relationships documentation rather than in the README.
Sorting follows the same pattern: allowedSorts('id') with /users?sort=id returns users sorted by ascending id, and custom sorts and sort direction are documented separately. Field selection uses a different parameter shape, fields[users]=id,email, matched by allowedFields('id', 'email'), and the README states that the fetched users will only have their id and email set.
That last detail is worth pausing on. Selecting a subset of columns changes what the model instance contains, so any accessor, cast or serialization step that expects the full row can behave differently. The package does what the allowlist says; it does not warn you that your transformer assumed a column would be present.
Where it stops being the right tool
The package is built around Eloquent models and relations. If your endpoint is a reporting query that joins five tables and returns aggregates, QueryBuilder::for() is not the abstraction you want; you are better off writing the query directly. The related searches that pair the project name with join, insert, update, delete, distinct or to-sql describe Laravel's own query builder, not this package. Those verbs are outside what the README documents here.
There is a second boundary around the allowlist itself. The package decides what a parameter may do, not whether the caller is entitled to do it. If a route allows filtering on a tenant column, the package will apply that filter for any authenticated caller who sends it; scoping the query to the current tenant is still your job, whether through an existing builder passed into for() or through a global scope. The README shows starting from an existing builder, which is the natural place to put that scoping, but it does not present it as an authorization mechanism.
Finally, the README does not document rollback or removal. If you adopt the package and later want to drop it, the UPGRADING.md file covers version upgrades, not uninstalling, and the README is silent on the subject. Plan for the calls to be spread across controllers rather than concentrated in one place.
Compared with hand-written request parsing
The obvious alternative is doing this yourself in the controller: read $request->query('filter'), validate the keys against an array, and call where() for each one. That approach has no dependency and no version-upgrade cost, and for one or two endpoints it is less code than the package plus its configuration.
The difference appears as the surface grows. A hand-rolled version tends to accumulate special cases: one filter needs a LIKE, another needs an exact match, a third needs to search two columns at once. The package names those cases instead. AllowedFilter::partial() and AllowedFilter::exact() cover the first two, and AllowedFilter::groupOr() covers the third without inventing a new parameter convention. The README explicitly ties the grouping behaviour to the JSON:API Fancy Filters recommendation, which is a useful signal: if your API already follows that convention, the package's parameter shapes will look familiar to clients rather than bespoke.
The trade-off is the dependency and the learning surface. The README links out to separate documentation pages for filtering, including relationships, sorting and selecting fields, so the feature set is larger than a single page suggests, and the config/ directory in the repository indicates configuration that the README does not walk through. A team that only ever needs one exact-match filter is paying for capability it will not use.
Version 7, maintenance and the MIT licence
The most recent releases are 7.3.5 on 2026-09-03, 7.3.4 on 2026-09-01 and 7.3.3 on 2026-08-07, and the last push to the default branch was on 2026-09-03. That is recent activity, and the repository is not archived. Version 7 is the line the README and the documentation links target, so the installation and feature pages referenced here are v7 pages.
Upgrade cost lives in UPGRADING.md, which the README points to for details rather than summarising. A major version bump is the moment to read it; patch releases within 7.3.x are the normal case. The repository also carries a CHANGELOG.md, so the per-release detail is available without reading commit history.
The licence is MIT, stated in the README and present as LICENSE.md at the repository root. MIT is permissive: it allows commercial use and modification, and it requires the copyright notice and licence text to be preserved. That is a description of the licence text, not legal advice; if your organisation has a policy on third-party dependencies, route it through whoever handles that.
Editorial conclusion
Adopt it if you are exposing a Laravel model over HTTP and want the accepted filter, sort, include and field names declared in code rather than parsed from raw input. Do not adopt it as a general SQL builder: it builds on Eloquent, so joins, inserts, updates and deletes are not its subject, and the documentation covers filtering, including relationships, sorting and selecting fields instead. Before rolling it out, read the installation notes at spatie.be/docs/laravel-query-builder/v7/installation-setup and check the 7.x entries in UPGRADING.md against your Laravel version.
Frequently asked questions
What does spatie/laravel-query-builder do?
It builds Eloquent queries from API request parameters. You declare which filters, sorts, includes and fields are allowed, and the package applies only those to the query.
What is the difference between an ORM and a query builder like spatie/laravel-query-builder?
This package is not an ORM. It sits on top of Eloquent, Laravel's ORM layer, and translates request parameters into calls on an existing query builder, so it does not map tables to objects itself.
Can spatie/laravel-query-builder handle joins, inserts or updates?
Those are not what the README documents. The package covers filtering, including relationships, sorting and selecting fields on an Eloquent query; joins, inserts, updates and deletes belong to Laravel's own query builder.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/spatie-laravel-query-builder)