🚦 Routing and Controllers
Routes decide where a request goes; controllers decide what happens when it gets there. Together they are the traffic system of your application. This lesson takes you from a one-line route to resource controllers, route model binding, validation, and the full range of responses Laravel can return.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Define routes for each HTTP verb and map them to controllers or closures
- Capture route parameters, apply constraints, and use named routes
- Organize routes with groups (prefixes and middleware) and use route model binding
- Build resource and single-action controllers and validate incoming requests
- Return the right response type — view, JSON, redirect, or file
Estimated Time: 40–50 minutes • Difficulty: Intermediate
Hands-on: Build a small product catalog with resource routes, a controller, and validation.
In This Lesson
What Routing Is
Routing maps an incoming URL and HTTP verb to the code that should handle it. Routes are the switchboard operators of your app: they take each request and connect it to the right controller action, closure, or view.
A useful real-world picture is a post office. Each letter (request) carries an address (URL) and an instruction (verb — deliver, return, forward). The sorting system (router) reads both and sends it to the correct destination (controller action).
📖 Where routes live
In Laravel 11, web routes go in routes/web.php (session, cookies, CSRF protection). API routes live in routes/api.php — added with php artisan install:api — are automatically prefixed with /api and use the stateless api middleware group. Custom Artisan commands are defined in routes/console.php.
Basic Routes & HTTP Verbs
Laravel gives you an expressive method per HTTP verb. The simplest route returns a value directly; most real routes point to a controller action using the [Controller::class, 'method'] array syntax.
<?php
use App\Http\Controllers\ContactController;
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
// A closure route that returns a string
Route::get('/hello', function () {
return 'Hello World';
});
// A route pointing at a controller action, given a name
Route::get('/profile', [ProfileController::class, 'show'])->name('profile');
// Multiple verbs on one URL
Route::match(['get', 'post'], '/contact', [ContactController::class, 'handle']);
Each verb corresponds to an intent, and they line up neatly with CRUD operations:
| Verb | Intent | CRUD | Library analogy |
|---|---|---|---|
| GET | Retrieve data | Read | "Can I see this book?" |
| POST | Create a resource | Create | "Here's a new book to add." |
| PUT / PATCH | Update a resource | Update | "Update this book's details." |
| DELETE | Remove a resource | Delete | "Remove this book." |
💡 See every route at a glance
Run php artisan route:list to print a table of every registered route — its verb, URI, name, and the action it maps to. It is the fastest way to understand or debug an app's routing.
Parameters, Constraints & Names
Route parameters
Wrap a URL segment in braces to capture it. Add a ? for an optional parameter (give the matching argument a default).
<?php
// Required parameter
Route::get('/users/{id}', function (string $id) {
return "User with ID: {$id}";
});
// Optional parameter
Route::get('/users/{id?}', function (?string $id = null) {
return $id ? "User {$id}" : 'All users';
});
Constraints
Use where() to restrict a parameter to a pattern, or the built-in helpers like whereNumber(). A non-matching URL simply won't match the route (yielding a 404).
<?php
// Only match a numeric id
Route::get('/users/{id}', [UserController::class, 'show'])
->whereNumber('id');
// Multiple constraints via a pattern
Route::get('/posts/{slug}/{id}', [PostController::class, 'show'])
->where(['slug' => '[A-Za-z0-9\-]+', 'id' => '[0-9]+']);
Named routes
Naming a route lets you refer to it by name instead of hardcoding its URL. Change the URL later and every reference keeps working.
<?php
Route::get('/user/{id}/profile', [ProfileController::class, 'show'])
->name('profile.show');
// Generate the URL by name (with parameters)
$url = route('profile.show', ['id' => 1]); // "/user/1/profile"
// Redirect to a named route
return redirect()->route('profile.show', ['id' => 1]);
💡 Analogy: Named routes are contacts in your phone. You don't memorize the number (URL) — you tap the name. If the person changes numbers, your contact still reaches them.
Route Groups
Route groups share attributes — a URL prefix, middleware, or a name prefix — across many routes, so you write them once instead of repeating them on every line.
<?php
// Prefix + middleware + name prefix, all shared
Route::prefix('admin')
->middleware(['auth', 'can:access-admin'])
->name('admin.')
->group(function () {
Route::get('/users', [AdminController::class, 'users'])->name('users');
Route::get('/settings', [AdminController::class, 'settings'])->name('settings');
// URLs: /admin/users and /admin/settings
// Names: admin.users and admin.settings
});
Groups can nest, which is perfect for versioned APIs:
<?php
Route::prefix('api')->group(function () {
Route::prefix('v1')->group(function () {
Route::get('/users', [Api\V1\UserController::class, 'index']);
});
Route::prefix('v2')->group(function () {
Route::get('/users', [Api\V2\UserController::class, 'index']);
});
});
// Produces /api/v1/users and /api/v2/users
✅ Why group routes
Groups keep related routes together and apply cross-cutting policies (like authentication) in one place. Forget to protect one admin route and you have a security hole — a group closes that gap by applying the middleware to every route inside it.
Route Model Binding
Instead of receiving an id and looking the record up yourself, you can type-hint the model and Laravel fetches it automatically — returning a 404 if nothing matches. This is route model binding, and it eliminates a huge amount of repetitive code.
Implicit binding
<?php
// The {user} segment name matches the $user type-hint
Route::get('/users/{user}', [UserController::class, 'show']);
class UserController extends Controller
{
public function show(User $user)
{
// Laravel already fetched the User by primary key (or 404'd)
return view('users.show', ['user' => $user]);
}
}
Binding by a different column
Want pretty URLs like /posts/my-article instead of /posts/42? Specify the column right in the route:
<?php
// Resolve the Post by its "slug" column instead of the id
Route::get('/posts/{post:slug}', [PostController::class, 'show']);
💡 Analogy: Binding is an assistant who hears a name and instantly places the right file on your desk — no rummaging through the cabinet by ID number.
⚠️ The parameter name must match
Implicit binding works because the route segment ({user}) matches the variable name ($user). Rename one without the other and Laravel can't connect them — you'll get a plain string instead of a model.
Controllers
A controller groups related request-handling logic into one class. It is the conductor: it takes the request, coordinates models and services, and returns a response. Generate one with Artisan:
php artisan make:controller UserController # empty controller
php artisan make:controller PhotoController --resource # 7 CRUD methods
php artisan make:controller ShowDashboard --invokable # single-action
Resource controllers
A single line registers all seven conventional CRUD routes and maps them to matching methods. This is the standard way to expose a resource.
<?php
// One line = seven routes
Route::resource('photos', PhotoController::class);
// Or a subset
Route::resource('photos', PhotoController::class)->only(['index', 'show']);
| Verb | URI | Method | Route name |
|---|---|---|---|
| GET | /photos | index | photos.index |
| GET | /photos/create | create | photos.create |
| POST | /photos | store | photos.store |
| GET | /photos/{photo} | show | photos.show |
| GET | /photos/{photo}/edit | edit | photos.edit |
| PUT/PATCH | /photos/{photo} | update | photos.update |
| DELETE | /photos/{photo} | destroy | photos.destroy |
For JSON-only APIs, use apiResource(), which omits the create and edit methods (those exist only to serve HTML forms).
<?php
Route::apiResource('products', ProductController::class);
Single-action controllers
When a controller does exactly one thing, give it an __invoke method and reference the class directly.
<?php
namespace App\Http\Controllers;
class ShowDashboard extends Controller
{
public function __invoke()
{
return view('dashboard');
}
}
// Route — no method name needed
Route::get('/dashboard', ShowDashboard::class);
💡 Keep controllers thin
A controller should coordinate, not carry heavy business logic. Push complex rules into models, services, or form request classes. A thin controller is easy to read and easy to test.
Validation & Responses
Validating input
Never trust incoming data. The validate() method checks the request against rules; on failure it automatically redirects back with errors (or returns a 422 JSON response for API requests). On success it returns the validated data.
<?php
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'body' => 'required',
'published_at' => 'nullable|date',
]);
Article::create($validated);
return redirect()->route('articles.index')
->with('status', 'Article created!');
}
Validation is a quality-control checkpoint — data must pass inspection before it reaches your database.
Response types
Controllers return whatever the situation calls for:
<?php
// A rendered Blade view
return view('users.profile', ['user' => $user]);
// JSON (Content-Type set automatically), with a status code
return response()->json(['error' => 'Unauthorized'], 401);
// A file download with a custom name
return response()->download($pathToFile, 'report.pdf');
// A redirect to a named route
return redirect()->route('login');
// Redirect back to the previous page, keeping the old input
return back()->withInput();
📖 Form Requests for bigger rules
When validation grows complex, move it into a dedicated class with php artisan make:request StoreArticleRequest. Type-hint that class in your controller method and Laravel validates automatically before your code runs — keeping the controller clean.
Hands-on Exercise
🏋️ Build a Product Catalog
Objective: Combine routes, a resource controller, model binding, and validation.
Instructions:
- Generate a resource controller:
php artisan make:controller ProductController --resource --model=Product. - Register it with
Route::resource('products', ProductController::class)inroutes/web.php. - In
index(), return a paginated list of products. Inshow(Product $product), rely on route model binding. - In
store(), validatename(required, max 255) andprice_cents(required, integer, min 0), then create the product. - Run
php artisan route:listand confirm all seven product routes appear.
💡 Hint
Passing --model=Product to make:controller type-hints the model in the generated show, edit, update, and destroy methods for you, so implicit binding just works. Use Product::paginate(12) for the list.
✅ Example solution
<?php
// routes/web.php
use App\Http\Controllers\ProductController;
Route::resource('products', ProductController::class);
<?php
// app/Http/Controllers/ProductController.php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index()
{
$products = Product::paginate(12);
return view('products.index', compact('products'));
}
public function show(Product $product) // route model binding
{
return view('products.show', compact('product'));
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'price_cents' => 'required|integer|min:0',
]);
Product::create($validated);
return redirect()->route('products.index')
->with('status', 'Product added!');
}
}
Remember to add $fillable = ['name', 'price_cents'] (or $guarded = []) to the Product model so create() accepts those fields.
🎯 Quick Quiz
Question 1: How many routes does Route::resource('photos', PhotoController::class) register?
Question 2: With Route::get('/users/{user}', ...) and a show(User $user) method, what makes Laravel fetch the record automatically?
Question 3: When $request->validate() fails on a normal web form submission, what happens?
Summary & Quiz
🎉 Key Takeaways
- Routes map a URL + verb to a controller action, closure, or view; web routes live in
routes/web.php, API routes inroutes/api.php. - Parameters capture URL segments; constraints restrict them; named routes decouple your code from URL strings.
- Groups share prefixes, middleware, and name prefixes across many routes in one place.
- Route model binding fetches models automatically from route parameters — by id or any column.
- Resource controllers give you seven CRUD routes in one line; single-action controllers use
__invoke. - Validate every request, then return the appropriate response — view, JSON, redirect, or file.
📚 Further Reading
- Laravel Docs — Routing
- Laravel Docs — Controllers
- Laravel Docs — Route Model Binding
- Laravel Docs — Validation
🚀 What's Next?
Your controllers can now handle requests and return views — but those views are still plain HTML. Next we meet Blade, Laravel's template engine, which brings layouts, components, loops, and conditionals to your presentation layer.
🎉 Nice work!
You can now route any request to the right controller and return the right response. That's the backbone of every Laravel feature you'll build from here.