Advanced Request Filtering and Sorting in Laravel APIs
Master advanced filtering, sorting, and pagination in Laravel to build scalable APIs. Learn to parse query params and implement dynamic Eloquent queries today.
Previously in this course, we explored Advanced Eloquent Scopes and Accessors: Cleaner Laravel Models to keep our database logic expressive. In this lesson, we are taking that foundation and applying it to our API layer, specifically addressing how to handle filtering, sorting, and pagination dynamically.
When building a project board API, you eventually reach a point where clients need to search for tasks by status, sort them by priority, or fetch them in manageable chunks. Hard-coding these paths is a maintenance nightmare. Instead, we’ll build a dynamic query pipeline.
The Problem: Controller Bloat
If you’ve ever seen a controller method with a dozen if ($request->has(...)) statements, you know the pain. Not only is it hard to read, but it’s also impossible to reuse.
To solve this, we will use a "Pipeline" approach. We want our controller to look like this:
PHPpublic function index(Request $request) { $tasks = $this->taskService->search($request->all()); return TaskResource::collection($tasks); }
By delegating the logic to the TaskService (following our work in Implementing the Service Layer), we keep our API endpoints thin and maintainable.
Building the Query Pipeline
1. Handling Filtering
We’ll leverage the Eloquent scopes we defined earlier. To make this dynamic, we map incoming request keys to specific scope methods.
PHP#6A9955">// Inside TaskRepository.php public function applyFilters(Builder $query, array $filters) { #6A9955">// Map request keys to our model scopes if (isset($filters['status'])) { $query->whereStatus($filters['status']); } if (isset($filters['priority'])) { $query->wherePriority($filters['priority']); } return $query; }
2. Implementing Dynamic Sorting
Sorting is often dangerous if you simply pass user input directly into orderBy(), as it opens the door to SQL injection or exposing sensitive columns. Always use an allow-list.
PHPpublic function applySorting(Builder $query, string $sort = null) { $allowedSorts = ['created_at', 'priority', 'due_date']; #6A9955">// Default to created_at if not provided or invalid $column = in_array($sort, $allowedSorts) ? $sort : 'created_at'; return $query->orderBy($column, 'desc'); }
3. Combining with Pagination
Laravel’s paginate() method is the standard for API responses. By combining our filters and sorting before calling paginate, we ensure the query is executed efficiently by the database rather than in memory.
PHP#6A9955">// TaskRepository.php public function getFilteredTasks(array $params) { $query = Task::query(); $this->applyFilters($query, $params); $this->applySorting($query, $params['sort'] ?? null); return $query->paginate($params['per_page'] ?? 15); }
Worked Example: Connecting the Layers
In our running project board, let's update the TaskService to handle these incoming parameters.
PHP#6A9955">// TaskService.php public function listTasks(array $data) { #6A9955">// We pass the data directly to the repository #6A9955">// The repository handles the heavy lifting of Eloquent composition return $this->taskRepository->getFilteredTasks($data); }
This approach allows your API to handle requests like GET /api/tasks?status=in-progress&sort=priority&per_page=50 effortlessly.
Hands-on Exercise
- Define an allow-list: In your
TaskRepository, create a constantALLOWED_SORT_COLUMNSto hold the fields users are permitted to sort by. - Refactor: Move your current
TaskControllerlogic into theTaskRepositoryusing the pattern shown above. - Test: Write a feature test that sends a request with
?status=completed&sort=due_dateand asserts that the returned JSON collection is correctly filtered and sorted.
Common Pitfalls
- Ignoring Pagination: Always set a default
per_pagevalue. Allowing users to fetch thousands of records without limit will eventually crash your memory or slow your database to a crawl. - Trusting User Input: Never pass a raw string from
$request->get('sort')directly into anorderBy()call. Always validate it against an allow-list. - N+1 Queries: Since we are returning a
JsonResourcecollection, ensure you are eager loading relationships (likeuserorproject) within your repository using$query->with(['user'])before callingpaginate().
Recap
We’ve evolved from manual request parsing to a clean, service-oriented approach. By centralizing filtering, sorting, and pagination in the repository layer, we’ve made our API more resilient and significantly easier to test. Remember: keep your controllers thin, validate your sorting keys, and always paginate your results.
Up next: We will implement Job Chaining and Batching to handle complex, multi-step background processes for our project tasks.
Work with me

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.

FilamentPHP Admin Panel & Dashboard Development
A powerful admin panel for your Laravel app — built with FilamentPHP so you can manage everything without touching the database.