Back to Blog
Lesson 21 of the Intermediate Laravel: Real-World Application Patterns course
LaravelJune 26, 20263 min read

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.

LaravelAPIEloquentPaginationRefactoringphpbackend

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:

PHP
public 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.

PHP
public 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

  1. Define an allow-list: In your TaskRepository, create a constant ALLOWED_SORT_COLUMNS to hold the fields users are permitted to sort by.
  2. Refactor: Move your current TaskController logic into the TaskRepository using the pattern shown above.
  3. Test: Write a feature test that sends a request with ?status=completed&sort=due_date and asserts that the returned JSON collection is correctly filtered and sorted.

Common Pitfalls

  • Ignoring Pagination: Always set a default per_page value. 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 an orderBy() call. Always validate it against an allow-list.
  • N+1 Queries: Since we are returning a JsonResource collection, ensure you are eager loading relationships (like user or project) within your repository using $query->with(['user']) before calling paginate().

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.

Similar Posts