PHP Architecture 6 min read Aug 5, 2026

Laravel Controllers Explained: Types, Examples & Best Practices (2026 Guide)

Learn every type of Laravel controller — resource, invokable, single-action & API — with real code examples and when to use each

Post
Mastering Laravel Controllers: A Beginner's Guide to Organized Application Logic

1. Introduction: The Role of the Controller

In the modern Laravel 13.x ecosystem, a controller serves as the vital intermediary between your application's routes and its core business logic. Based on fundamental architecture concepts, the controller's primary responsibility is to receive an incoming HTTP request, coordinate the necessary logic, and return a response.

While Laravel allows you to define logic directly in route closures, moving that logic into dedicated controller classes provides several professional benefits:

  • Code Cleanliness: Keeps your code routes/web.php or routes/api.php files from becoming cluttered "junk drawers".
  • Separation of Concerns: Ensures that the routing layer only handles traffic, while the controller handles the "how" of the request.
  • Scalability: Organised classes are significantly easier to navigate as your application grows from ten routes to hundreds.
  • Testability: Dedicated classes are much simpler to target with automated unit and feature tests.

By embracing this organisation early, you move away from chaotic scripting toward the "thin controller" philosophy—a hallmark of elite Laravel development.

2. The Golden Rule: Keeping Controllers "Thin"

One of the most frequent mistakes I see in production environments is the "Fat Controller". These are classes overstuffed with database queries, complex calculations, and third-party API calls. To build a maintainable app, your controllers must remain thin, acting only as a conductor for an orchestra of other services.

The Dangers of "Fat Controllers"

  • Hard-to-Test Logic: When business rules are trapped inside a controller method, you cannot easily test them in isolation.
  • Repeated Code: Logic defined in a controller is difficult to reuse elsewhere, leading to the same code being copied across multiple classes.
  • Endless Growth: Without boundaries, a single controller can easily balloon into thousands of lines, becoming a technical debt nightmare.

The "Fix-it" Table: Logic Distribution

Use this matrix to decide if a piece of code belongs in your controller or needs a new home:

Task Type

What stays in the Controller

Where to move Business Logic

Primary Goal

Coordination (Request/Response)

Execution (The "Work")

Data Logic

Validating the request shape

Repositories or Eloquent Models

Complex Rules

Triggering a service call

Service Classes or Actions

Side Effects

Returning a View or JSON

Jobs (for background tasks)

Laravel 13.x provides specialised controller types to help you enforce this focused organisation from the start.

3. Type 1: Basic Controllers (Manual Organization)

Basic controllers are the bread and butter of Laravel organisation. They allow you to group related logic—like everything involving a "User"—into one place.

To generate a basic controller, use the Artisan command:

php artisan make:controller UserController

Example: Defining and Routing

In a basic controller, you define public methods to handle specific actions. Here is a UserController response to a profile request:

namespace App\Http\Controllers;

use App\Models\User;
use Illuminate\View\View;

class UserController extends Controller
{
    public function show(string $id): View
    {
        return view('user.profile', [
            'user' => User::findOrFail($id)
        ]);
    }
}

You then map this method to the following:

use App\Http\Controllers\UserController;

Route::get('/user/{id}', [UserController::class, 'show']);

Senior Developer Tip (Laravel 13.x Middleware): In previous versions, middleware was often assigned in the constructor. In Laravel 13.x, the professional standard is to implement the HasMiddleware interface and use the static middleware() method or use attributes directly on the class or methods.

While basic controllers are flexible, some actions are so specialised they deserve their own dedicated class.

4. Type 2: Single Action (Invokable) Controllers

Sometimes you have a task that is particularly complex, such as a multi-step ProvisionServer process or an action. If you find yourself struggling to name the method (should it be handle()? execute()? doIt()?), you should use an Invokable Controller.

These controllers use the PHP magic method __invoke() to handle a single responsibility. This solves the "naming struggle" and follows the Single Responsibility Principle.

Routing Tip: When routing to an invokable controller, omit the method name entirely. Laravel automatically looks for the __invoke method.

Example Route:

Route::post('/server/provision', ProvisionServer::class);

Artisan Command:

php artisan make:controller ProvisionServer --invokable

Transitioning from these specialised single actions to standardised data management brings us to the CRUD powerhouse: resource controllers.

5. Type 3: Resource Controllers (The CRUD Powerhouse)

Resource controllers treat Eloquent models as "resources" that require standard actions. Instead of manually defining seven different routes for a blog, you can generate them all with one line of code.

Expert Pro-tip: When generating a controller for a specific model, use the --model flag to have Laravel automatically type-hint your methods:

php artisan make:controller PhotoController --model=Photo --resource

The 7 Standard Resource Actions

Using Route::resource('photos', PhotoController::class) maps, the following:

HTTP Verb

URI

Action (Method)

Route Name

Purpose

GET

/photos

index

photos.index

Display a list of all photos.

GET

/photos/create

create

photos.create

Show the form to upload a new photo.

POST

/photos

store

photos.store

Save the new photo to the database.

GET

/photos/{photo}

show

photos.show

Display a specific photo.

GET

/photos/{photo}/edit

edit

photos.edit

Show the edit form for a photo.

PUT/PATCH

/photos/{photo}

update

photos.update

Update the photo in the database.

DELETE

/photos/{photo}

destroy

photos.destroy

Delete the photo.

Advanced Resource Features

  • API Resources: Use Route::apiResource() to exclude the create and edit routes (APIs don't need HTML forms).
  • Nested Resources: Use dot notation (e.g., photos.comments) to handle child items.
  • Singleton Resources: Use Route::singleton() for items like a user's profile. You can use the .creatable() method nuance (e.g., ->creatable()) if the singleton should allow creation and deletion routes as well.

Even the best routes require robust professional patterns like dependency injection and validation to remain "thin."

6. Professional Patterns: Injection and Validation

To keep your controllers focused on orchestration, you must master how the Laravel Service Container provides the tools your methods need.

Dependency Injection (DI)

Laravel automatically "injects" objects into your controller.

Injection Type

Description

Best Use Case

Constructor Injection

Defined in the __construct method.

Services or repositories used by all actions.

Method Injection

Type-hinted directly in the method.

Request-specific needs (e.g., Request or PostService).

The Practical DI Checklist

When working with dependency injection, follow these four rules from the "Half Shell" best practices:

  1. Inject, Don't Instantiate: Always type-hint your services and the Request object; never use the new keyword inside a controller method.
  2. Order Matters: Always place your injected dependencies before your route parameters in the method signature.
  3. Bind Interfaces: If a concrete class cannot be auto-resolved, bind the interface in a Service Provider.
  4. Stay Orchestration-Focused: Ensure the injected collaborator does the heavy lifting while the controller merely calls its methods.

Form Request Validation

Professional developers move validation out of the controller entirely. Use the command php artisan make:request StorePostRequest to create a dedicated validation class.

3 Keys to Human-Readable Errors

Within your Form Request class, you can override default error messages to make them user-friendly:

  1. The messages() Method: Return an array of custom text (e.g., 'title.required' => 'A title is needed to save a post').
  2. The attributes() Method: Swap technical field names for human-friendly ones (e.g., 'user_id' => 'author').
  3. The :attribute Placeholder: Use this to dynamically inject the human-friendly name from your attributes() method.

7. Decision Matrix: Which Controller Should I Use?

Scenario

Recommended Type

Reasoning

Standard CRUD (Blog, Products)

Resource

Follows RESTful conventions; predictable and standard.

Complex Action (Deployment, Payment)

Invokable

Solves the "naming struggle" for single, complex tasks.

Simple Contact Form / Static Pages

Basic

Best for grouping 2-3 non-resourceful actions together.

Settings / User Profile

Singleton

Ideal for "one-of-a-kind" items where no ID is needed in the URI.

Final Summary

Mastering controllers in Laravel 13.x is about discipline rather than just syntax. While the framework doesn't strictly "enforce" a specific architecture, following these patterns—keeping controllers thin, centralising validation in form requests, and leveraging dependency injection—leads to cleaner code and much happier development teams. Choose the right tool for your specific scenario, and your application will remain scalable, testable, and maintainable for years to come.

Related reading