← jordandalton.com

Laravel Pipelines: Transforming Your Code into a Flow of Efficiency

Laravel's pipelines are a powerful tool for simplifying complex processes and streamlining workflows. By breaking down operations into smaller, reusable stages, pipelines offer a more efficient, readable, and scalable approach to handling data transformations in your Laravel applications.

Laravel pipelines are a powerful feature that allows you to build complex workflows by breaking down operations into smaller, reusable stages. Pipelines offer a more efficient, readable, and scalable approach to handling data transformations in your Laravel applications. In this post, we'll explore the basics of pipelines, their benefits, and how to use them to build APIs.

Pipeline Basics

Laravel pipelines are a design pattern that allows you to create a sequence of stages, where each stage receives input data, performs some operation, and then passes the output to the next stage. This pattern is especially useful for processing large data sets, where you need to perform multiple operations on the data before returning the final result.

The benefits of using pipelines

By using pipelines, you can:

  • Break down complex operations into smaller, reusable stages.
  • Simplify your code by separating concerns.
  • Improve code readability and maintainability.
  • Scale your application more easily.
  • Enable parallel processing by running stages concurrently.
  • Allow stages to be swapped in and out, depending on your needs.

How to build a simple API in Laravel using pipelines

First, let's modify our users migration (this file is likely named 2014_10_12_000000_create_users_table.php in your repository). This modification will introduce the dob field which we will later reference in our API.

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->string('timezone')->index();
            $table->date('dob')->index();
            $table->rememberToken();
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('users');
    }
};

Now we need to migrate the table to the database:

php artisan migrate

From here we will update our UserFactory.php to include the dob field which we can generate a fake date for on demand:

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name'              => fake()->name(),
            'email'             => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'timezone'          => fake()->timezone,
            'password'          => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'dob'               => $this->faker->date,
            'remember_token'    => Str::random(10),
        ];
    }
}

Now we need to get data into your database. Let's fire up Tinker:

php artisan tinker

This will now place us in a tinker session. Type the following into the terminal, then press ENTER. This will generate 1000 user records into your database:

\App\Models\User::factory()->times(1000)->create();

Now that we have a decently sized data set, we'll close Tinker and create a controller:

php artisan make:controller UserSearchController -i

Let's add the route in our api.php:

Route::get('users/search', \App\Http\Controllers\UserSearchController::class);

We'll now take that controller and bootstrap the pipeline:

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Pipeline;

class UserSearchController extends Controller
{
    public function __invoke(Request $request)
    {
        $pipelines = [];

        return Pipeline::send(User::query())
            ->through($pipelines)
            ->thenReturn()
            ->paginate();
    }
}

Right now the code would return the same as if we did this:

User::paginate()

Before we start adding stages to our pipeline let's visit our endpoint in the browser:

http://yourdomain.test/api/users/search

Since UserFactory creates data at random, your data points will not look exactly like mine. Now we'll create a class called ByEmail which will serve as our filter allowing us to "filter by email."

namespace App\Filters;

use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;

class ByEmail
{
    public function __construct(public Request $request) {}

    public function handle(Builder $query, Closure $next)
    {
        return $next($query)
            ->when(
                $this->request->has('email'),
                fn ($query) => $query->where('email', 'regexp', $this->request->email)
            );
    }
}

So what exactly is going on here?

public function __construct(public Request $request) {}

We're asking the service container to return us the current request coming into our application and assign it to a public attribute called $request which we can reference anytime by using $this->request.

Now let's examine the handle() method:

public function handle(Builder $query, Closure $next)
{
    return $next($query)
        ->when(
            $this->request->has('email'),
            fn ($query) => $query->where('email', 'regexp', $this->request->email)
        );
}

The first parameter, Builder $query, serves as the payload being sent into our pipeline — in our case, the Eloquent Query Builder. The second parameter, Closure $next, is what is used to send our payload, along with any modifications made to it, onto the next stage in the pipeline.

The when() method is telling the query builder that "when" there is an email being provided in our request, we want to apply the query fn($query) => $query->where('email', 'regexp', $this->request->email), which will search for any email addresses that contain any character provided by the email portion of the request.

We now need to add this filter to our pipeline by adding the fully qualified name to the $pipelines array in our controller:

$pipelines = [
    \App\Filters\ByEmail::class,
];

With our pipeline aware of our filter, let's test this by visiting our API URL with the email reference in the query string:

http://yourdomain.test/api/users/search?email=.org

In this example we're telling our endpoint we want to filter by email addresses which contain .org. Our results now include only records with email addresses containing .org.

Now that we understand what's going on, we'll create two more filters to search by name and date of birth:

namespace App\Filters;

use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;

class ByName
{
    public function __construct(public Request $request) {}

    public function handle(Builder $query, Closure $next)
    {
        return $next($query)
            ->when(
                $this->request->has('name'),
                fn ($query) => $query->where('name', 'regexp', $this->request->name)
            );
    }
}
namespace App\Filters;

use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;

class ByDob
{
    public function __construct(public Request $request) {}

    public function handle(Builder $query, Closure $next)
    {
        return $next($query)
            ->when(
                $this->request->has('dob'),
                fn ($query) => $query->where('dob', 'regexp', $this->request->dob)
            );
    }
}

I bet you've spotted a recurring pattern in our approach. Challenge yourself by creating a way to make the code reusable — then share with me on Twitter what you did.

Let's incorporate them into our controller:

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Pipeline;

class UserSearchController extends Controller
{
    public function __invoke(Request $request)
    {
        $pipelines = [
            \App\Filters\ByEmail::class,
            \App\Filters\ByName::class,
            \App\Filters\ByDob::class,
        ];

        return Pipeline::send(User::query())
            ->through($pipelines)
            ->thenReturn()
            ->paginate();
    }
}

What's really cool now is that we can stack these filters in our request. For example, let's search for .org addresses where the name contains the letter R and the user was born in 1981:

http://yourdomain.test/api/users/search?email=.org&name=R&dob=1981

The results are filtered by all three criteria simultaneously — pipelines composing cleanly.

Conclusion

Laravel pipelines are a powerful tool that can simplify your code and streamline your workflow. By breaking down complex processes into small, manageable steps, pipelines make it easier to build scalable and maintainable applications.

Whether you're working with data transformations, API requests, or any other type of data processing, Laravel pipelines provide an elegant and efficient way to handle the flow of data through your application. By using the techniques outlined in this article, you can harness the power of pipelines to take your Laravel development to the next level.

So don't be afraid to give pipelines a try! With their flexible and customizable architecture, you can build pipelines that are tailored to your specific needs and help you build high-quality, performant applications with ease. Happy coding!


Source code is available on GitHub. Follow me on Twitter and YouTube.

Enjoyed this?

Book a build day.

Take the ideas in here from concept to working software in one focused day.

Book a build day →
jordandalton.com — © 2026 Jordan DaltonBuilt in a day, naturally.