Use Actions to Keep Your Laravel Controllers Thin

Use Actions to Keep Your Laravel Controllers Thin

Controllers tend to grow fast. It always starts clean, but then requirements stack up. A few conditionals, a call to an external service, maybe a notification or a queue dispatch, and suddenly your controller is a maze.

I've run into this enough times that I started looking for a cleaner way to handle business logic. That's when I found the Laravel Action pattern. I picked it up from Nuno Maduro, and it immediately made sense. Since then, I've been using it in most of my Laravel projects, and it's become a core part of how I structure my apps.

What is the Laravel Action pattern?

An Action is a single-purpose class that handles one operation. Create a post. Update a profile. Delete a record.

You give it a clear name, like CreatePostAction, and define a handle() method inside. That method receives the needed input and performs the operation. No more, no less.

All the logic for that action lives in one place. Your controller just delegates to it. This keeps your controllers thin and your business logic easier to test, reason about, and reuse.

Why it’s worth using

  • Keeps controllers focused – Your controller methods shrink to just handling requests and returning responses
  • Reusable logic – Call the same Action from a controller, console command, or queued job
  • Easier to test – You can test an Action like any other class, without touching the HTTP layer
  • Clear organizationCreatePostAction, UpdatePostAction, DeletePostAction tell you exactly what they do
  • Safe by default – If the operation involves multiple steps, you can wrap it in a transaction to ensure consistency
  • Centralized side effects – If an action triggers events or jobs, it does so inside the class itself

This pattern creates clear boundaries between different parts of your app and encourages good habits by default.

Example: Blog Post Actions

Let’s say we’re building a simple blog. You’ve got a Post model and want to create, update, and delete posts. Here’s how that looks using Actions.

Note: You don’t need to use DB::transaction() unless multiple writes are happening inside the action. For example, creating a post and logging an audit trail or dispatching a sync operation to another system. Don’t wrap a single write in a transaction—it adds overhead with no benefit.

CreatePostAction

final readonly class CreatePostAction
{
    public function handle(array $data): Post
    {
        return Post::create([
            'title'   => $data['title'],
            'content' => $data['content'],
            'user_id' => $data['user_id'],
        ]);
    }
}

UpdatePostAction

final readonly class UpdatePostAction
{
    public function handle(Post $post, array $data): Post
    {
        $post->update($data);

        return $post->refresh();
    }
}

DeletePostAction

final readonly class DeletePostAction
{
    public function handle(Post $post): void
    {
        $post->delete();
    }
}

The Controller

Your controller stays clean and easy to read:

class PostController extends Controller
{
    public function store(
        StorePostRequest $request,
        CreatePostAction $create
    ): RedirectResponse {
        $post = $create->handle(
            $request->validated() + ['user_id' => $request->user()->id]
        );

        return redirect()->route('posts.show', $post);
    }

    public function update(
        UpdatePostRequest $request,
        Post $post,
        UpdatePostAction $update
    ): RedirectResponse {
        $update->handle($post, $request->validated());

        return back()->with('status', 'Post updated.');
    }

    public function destroy(
        Post $post,
        DeletePostAction $delete
    ): RedirectResponse {
        $delete->handle($post);

        return redirect()->route('posts.index')->with('status', 'Post deleted.');
    }
}

There’s no guesswork here. Each method delegates to an Action. The Action handles everything else. This makes it easier to read, maintain, and update later.

Important clarification

Actions are for mutations. They should be used for operations that change application state: create, update, delete, or trigger a side effect.

You should not use Actions for read-only operations like index() or show(). If you're just retrieving data, keep that logic inside the controller or move it to a dedicated query class or ViewModel. Actions are designed for writes and side effects, not reads.