Table of Contents
Laravel controllers are where your application’s logic lives. Laravel controllers are where your application’s logic lives. Understanding Laravel controllers is the step that makes everything click – routes decide where a request goes, views decide what the user sees, and controllers handle everything in between.
If you’re still writing logic directly inside route closures, controllers are the next step. This guide covers everything you need – creating controllers, connecting them to routes, passing data to views, resource controllers, and the best practices that keep Laravel projects maintainable as they grow.
Prerequisites
- Laravel 10 or higher installed
- Basic PHP knowledge
- Familiarity with Laravel routing basics
All examples use a fresh Laravel installation. Run php artisan serve and follow along.
For official documentation see the Laravel Controllers documentation.
What is a Laravel Controller?
A controller is a PHP class that groups related request handling logic. Instead of this:
// routes/web.php - logic in routes (don't do this)
Route::get('/users', function() {
$users = User::all();
return view('users.index', compact('users'));
});
Route::post('/users', function(Request $request) {
// validate, create user, redirect...
});
Route::delete('/users/{id}', function($id) {
// find user, delete, redirect...
});
You get this:
// routes/web.php - clean routes pointing to controller
Route::get('/users', [UserController::class, 'index']);
Route::post('/users', [UserController::class, 'store']);
Route::delete('/users/{id}', [UserController::class, 'destroy']);
All the logic moves into UserController where it’s organized, testable, and reusable. Routes become a clean map of your application rather than a wall of anonymous functions.
MVC in Laravel – How Controllers Fit
Laravel follows the MVC (Model-View-Controller) pattern:
- Model – represents database tables and handles data. Example:
User,Product,Order - View – Blade templates that display data to the user
- Controller – receives requests, talks to models, passes data to views, returns responses
The request flow:
Browser request
→ Route (routes/web.php)
→ Controller method
→ Model (database query)
→ View (Blade template)
→ Response to browser
Creating Your First Controller
Use Artisan to generate a controller – don’t create the file manually:
php artisan make:controller UserController
Output:
INFO Controller [app/Http/Controllers/UserController.php] created successfully.
Open the generated file:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
// Your methods go here
}
Every controller extends the base Controller class and lives in the App\Http\Controllers namespace. Artisan handles this automatically – manually created files often miss the namespace and fail silently.
Adding Methods and Connecting to Routes
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function index()
{
$users = [
['name' => 'Kapil Verma', 'email' => 'kapil@example.com'],
['name' => 'John Smith', 'email' => 'john@example.com'],
];
return view('users.index', compact('users'));
}
public function show($id)
{
// In a real app: $user = User::findOrFail($id);
return view('users.show', ['userId' => $id]);
}
}
Connect to routes in routes/web.php:
<?php
use App\Http\Controllers\UserController;
use Illuminate\Support\Facades\Route;
Route::get('/users', [UserController::class, 'index']);
Route::get('/users/{id}', [UserController::class, 'show']);
Visiting /users calls UserController@index. Visiting /users/5 calls UserController@show with $id = 5.
Passing Data to Blade Views
Three ways to pass data from a controller to a view:
<?php
class ProductController extends Controller
{
public function index()
{
$products = [
['name' => 'Laptop', 'price' => 999],
['name' => 'Mouse', 'price' => 29],
];
// Method 1: compact() - most common
return view('products.index', compact('products'));
// Method 2: with() - readable for single variables
// return view('products.index')->with('products', $products);
// Method 3: array - explicit
// return view('products.index', ['products' => $products]);
}
}
In the Blade view (resources/views/products/index.blade.php):
@foreach($products as $product)
<div>
<strong>{{ $product['name'] }}</strong>
&mdash; ${{ $product['price'] }}
</div>
@endforeach
compact('products') is shorthand for ['products' => $products]. Use it when passing multiple variables – it keeps the return statement clean.
Resource Controllers
For standard CRUD operations, resource controllers generate all seven methods automatically:
php artisan make:controller PostController --resource
This generates a controller with seven methods already stubbed out:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PostController extends Controller
{
// GET /posts - list all posts
public function index() {}
// GET /posts/create - show create form
public function create() {}
// POST /posts - store new post
public function store(Request $request) {}
// GET /posts/{post} - show single post
public function show(string $id) {}
// GET /posts/{post}/edit - show edit form
public function edit(string $id) {}
// PUT/PATCH /posts/{post} - update post
public function update(Request $request, string $id) {}
// DELETE /posts/{post} - delete post
public function destroy(string $id) {}
}
Register all seven routes with one line:
// routes/web.php
Route::resource('posts', PostController::class);
Verify the routes were created:
php artisan route:list --name=posts
Output:
GET|HEAD posts ............. posts.index › PostController@index
POST posts ............. posts.store › PostController@store
GET|HEAD posts/create ...... posts.create › PostController@create
GET|HEAD posts/{post} ...... posts.show › PostController@show
PUT|PATCH posts/{post} ...... posts.update › PostController@update
DELETE posts/{post} ...... posts.destroy › PostController@destroy
GET|HEAD posts/{post}/edit . posts.edit › PostController@edit
If you only need some of these routes use only() or except():
// Only index and show
Route::resource('posts', PostController::class)
->only(['index', 'show']);
// Everything except destroy
Route::resource('posts', PostController::class)
->except(['destroy']);
Handling Form Input With Request
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ContactController extends Controller
{
public function store(Request $request)
{
// Validate input
$validated = $request->validate([
'name' => 'required|string|max:100',
'email' => 'required|email',
'message' => 'required|string|min:10',
]);
// $validated only contains the fields that passed
// Safe to use - no raw $_POST needed
// In a real app: ContactMessage::create($validated);
return redirect()
->back()
->with('success', 'Message sent successfully.');
}
}
Never access $_POST directly in Laravel. The Request object handles input sanitization, CSRF verification, and validation in one place. The validate() method automatically redirects back with errors if validation fails – no if/else needed.
Returning Different Response Types
<?php
class ApiController extends Controller
{
// Return a view
public function index()
{
return view('home');
}
// Return JSON - for API endpoints
public function data()
{
return response()->json([
'status' => 'ok',
'data' => ['key' => 'value'],
]);
}
// Redirect to another route
public function store(Request $request)
{
// ... save data ...
return redirect()->route('home')->with('success', 'Saved.');
}
// Return a string (useful for debugging)
public function debug()
{
return 'Controller is working.';
}
// Return with HTTP status code
public function notFound()
{
return response()->json(['error' => 'Not found'], 404);
}
}
Debugging Controllers
When a controller method isn’t behaving as expected, these tools diagnose the problem quickly:
<?php
public function index()
{
$users = User::all();
// dd() - dump and die, stops execution and shows the value
dd($users);
// ddd() - same but with a nicer UI
ddd($users);
// dump() - shows the value but continues execution
dump($users);
return view('users.index', compact('users'));
}
Check which routes are registered when a URL returns 404:
php artisan route:list
Clear cached routes when route changes don’t take effect:
php artisan route:clear
php artisan config:clear
Best Practices
- One responsibility per method – each controller method does one thing. If a method is over 30 lines it probably needs to be split or moved to a service class.
- Keep controllers thin – business logic belongs in service classes or models, not controllers. Controllers receive requests and return responses. That’s it.
- Use resource controllers for CRUD – don’t invent custom method names when the standard seven cover your use case.
- Validate in controllers – or in Form Request classes for complex validation. Never trust raw input.
- Return typed responses –
view()for web,response()->json()for APIs,redirect()after form submissions.
Frequently Asked Questions
What is the difference between a controller and a route in Laravel?
A route maps a URL and HTTP method to a handler. A controller is the handler. Routes are defined in routes/web.php and point to controller methods. The route decides which controller method runs – the controller method decides what happens when it runs.
Should I use resource controllers or regular controllers?
Use resource controllers for standard CRUD operations – anything with create, read, update, delete on a model. Use regular controllers for anything that doesn’t map cleanly to CRUD – authentication flows, API endpoints with custom logic, or single-purpose controllers with one or two methods.
Why does my controller return a blank page?
Most likely the view path is wrong. return view('users.index') looks for resources/views/users/index.blade.php. Dots map to directory separators. Check that the file exists at exactly that path and has the .blade.php extension. Also check that the variable names passed with compact() match what the Blade template expects.
What is the difference between a controller and a middleware?
A controller handles what happens when a request is processed. Middleware handles what happens before or after the controller runs – authentication checks, logging, rate limiting, CORS headers. Middleware filters requests. Controllers process them.
How do I share data across all controller views?
Use View::share() in a service provider or use view composers. In AppServiceProvider::boot():
<?php
use Illuminate\Support\Facades\View;
public function boot(): void
{
View::share('siteName', config('app.name'));
}
Now $siteName is available in every Blade template without passing it from each controller method.
Summary: Laravel Controllers Best Practices
Laravel controllers organize your application’s request handling logic into clean, reusable classes. The key points:
- Always use Artisan to create controllers –
php artisan make:controllerhandles namespace and boilerplate automatically - Resource controllers give you seven standard CRUD methods and routes in one command
- Keep controllers thin – receive input, call services or models, return responses
- Use Request validation – never access
$_POSTdirectly - Debug with dd() and
php artisan route:listwhen things don’t work as expected
The next logical step after controllers is understanding middleware – how to protect routes, authenticate users, and run logic before controllers execute. The Laravel middleware guide covers all of that. For putting controllers into practice with a complete working application, the Laravel CRUD guide builds a full create, read, update, delete feature using resource controllers from start to finish.
