Every Laravel developer knows the drill: create a model, write a controller, define routes, add validation, implement filtering, pagination, sorting... rinse and repeat for every single entity in your application.
What if I told you there's a better way?
I've been working on Laravel Query Gate, a package that transforms how you build APIs. Instead of writing repetitive boilerplate code, you declare what you want — and Query Gate handles the rest.
Let me show you how I built a complete blog API with Posts, Comments, Categories, Tags, Authentication, Custom Actions, and API Versioning — all without writing a single controller.
The Problem We All Face
Here's what a typical Laravel API setup looks like:
// PostController.php - Just ONE of many controllers you'll write
class PostController extends Controller
{
public function index(Request $request)
{
$query = Post::query();
if ($request->has('status')) {
$query->where('status', $request->status);
}
if ($request->has('category_id')) {
$query->where('category_id', $request->category_id);
}
if ($request->has('author')) {
$query->whereHas('author', fn($q) => $q->where('name', 'like', "%{$request->author}%"));
}
// ... 20 more filters
if ($request->has('sort')) {
// parsing logic...
}
return $query->paginate($request->per_page ?? 15);
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
// ... more rules
]);
return Post::create($validated);
}
// update(), destroy(), show()... you get the idea
}
Now multiply this by every model in your application. It's exhausting.
The Query Gate Way
Here's the same functionality with Laravel Query Gate:
// app/Models/Post.php
class Post extends Model
{
use HasQueryGate;
public static function queryGate(): QueryGate
{
return QueryGate::make()
->alias('posts')
->middleware(['auth:sanctum'])
->filters([
'status' => ['string', 'in:draft,published,archived'],
'category_id' => 'integer',
'author.name' => ['string', 'max:100'],
'created_at' => 'date',
])
->allowedFilters([
'status' => ['eq', 'in', 'neq'],
'category_id' => ['eq', 'in'],
'author.name' => ['like'],
'created_at' => ['gte', 'lte', 'between'],
])
->select(['id', 'title', 'slug', 'status', 'created_at', 'author.name'])
->sorts(['created_at', 'title'])
->actions(fn ($actions) => $actions
->create(fn ($action) => $action
->validations([
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string'],
])
)
->update()
->delete()
);
}
}
That's it. No controller. No routes. No repetitive code.
Query Gate automatically gives you:
GET /query/posts → List with filters, sorting, pagination
GET /query/posts/{id} → Show single post
POST /query/posts → Create post
PATCH /query/posts/{id} → Update post
DELETE /query/posts/{id} → Delete post
Real-World Example: A Complete Blog API
I built a complete example project to showcase what Query Gate can do. Here's what's included:
1. Advanced Filtering
Filter by any field with operators like eq, neq, like, in, between, gt, gte, lt, lte:
# Posts published in 2024
GET /query/posts?filter[published_at][between]=2024-01-01,2024-12-31
# Posts by specific tags
GET /query/posts?filter[tag_slugs][in]=laravel,php
# Posts with 1000+ views, sorted by popularity
GET /query/posts?filter[views_count][gte]=1000&sort=views_count:desc
2. Relationship Filtering
Query through relationships using dot notation:
# Posts by author name
GET /query/posts?filter[author.name][like]=John
# Comments on published posts
GET /query/comments?filter[post.status][eq]=published
3. Custom Actions
Need more than CRUD? Define custom actions:
// app/Actions/QueryGate/Posts/PublishPost.php
class PublishPost extends AbstractQueryGateAction
{
public function action(): string
{
return 'publish';
}
public function handle($request, $model, array $payload)
{
$model->update([
'status' => 'published',
'published_at' => now(),
]);
return ['message' => 'Post published!', 'post' => $model];
}
public function authorize($request, $model): ?bool
{
return $request->user()->can('update', $model);
}
}
Now you have:
POST /query/posts/1/publish
The example includes actions for: Publish, Unpublish, Archive, Feature, Unfeature, Duplicate posts and Approve, Reject, Mark as Spam comments.
4. API Versioning (Yes, Built-In!)
Evolve your API without breaking existing clients:
QueryGate::make()
->version('2024-01-01', function (QueryGate $gate) {
$gate->filters(['title' => 'string', 'status' => 'string'])
->select(['id', 'title', 'status']);
})
->version('2025-01-01', function (QueryGate $gate) {
$gate->filters([
'title' => 'string',
'status' => 'string',
'views_count' => 'integer', // New in 2025!
'author.name' => 'string', // New in 2025!
])
->select(['id', 'title', 'status', 'views_count', 'author.name']);
});
Clients choose their version:
GET /query/posts -H "X-Query-Version: 2024-01-01"
And there's a changelog endpoint:
GET /query/posts/__changelog
5. Automatic OpenAPI Documentation
Query Gate generates OpenAPI/Swagger docs automatically:
GET /query/docs → Interactive documentation UI
GET /query/docs.json → OpenAPI JSON spec
No manual documentation. It's always in sync with your code.
Quick Start
Want to try it? Here's how to get started:
Install the Package
composer require behindsolution/laravel-query-gate
Add the Trait to Your Model
use BehindSolution\LaravelQueryGate\Traits\HasQueryGate;
use BehindSolution\LaravelQueryGate\Support\QueryGate;
class Post extends Model
{
use HasQueryGate;
public static function queryGate(): QueryGate
{
return QueryGate::make()
->alias('posts')
->filters(['title' => 'string', 'status' => 'string'])
->allowedFilters(['title' => ['like'], 'status' => ['eq']])
->actions(fn ($a) => $a->create()->update()->delete());
}
}
Register in Config
// config/query-gate.php
'models' => [
App\Models\Post::class,
],
Done. You now have a fully functional REST API.
Try the Example Project
Clone the complete example to see everything in action:
git clone git@github.com:behindSolution/LQG-example.git
cd LQG-example
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate --seed
php artisan serve
Import the included Postman collection and start exploring.
Why I Built This
I was tired of writing the same code over and over. Every project needed:
- Filtering logic
- Sorting logic
- Pagination
- Validation
- Authorization checks
- API versioning (if you're lucky)
- Documentation (if you have time)
Query Gate handles all of this declaratively. You focus on your business logic, not boilerplate.
What's Next?
- GitHub Repository - Star it if you find it useful!
- Example Project - Full blog API demo
- Documentation - Complete guide
Have questions or feedback? Drop a comment below or open an issue on GitHub. I'd love to hear how you're using Query Gate in your projects!
If this saved you some time, consider giving the repo a star. It helps more developers discover the project.
Top comments (0)