Skip to main content

🚦 Routing and Controllers

Routes decide which code answers a URL; controllers are that code. Together they form the traffic system of a Laravel app — matching every incoming request to a handler, capturing the data it carries, and returning the right response.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Define routes with different HTTP verbs, parameters, and constraints
  • Use named routes and route groups to stay DRY and organised
  • Apply route model binding to load models automatically
  • Build RESTful resource controllers and validate incoming requests
  • Attach middleware to filter requests before they reach a controller

Estimated Time: 40–50 minutes  •  Difficulty: Intermediate

Hands-on: Build the routes and controllers for a small product catalogue.

In This Lesson

What Routing Does

Routing is the mechanism that connects an incoming HTTP request to the code that handles it. If a controller is a department manager, a route is the receptionist who reads each visitor's request and points them to the right desk.

Laravel keeps routes in the routes/ directory, split by purpose:

FilePurpose
web.phpBrowser-facing routes — sessions, cookies, and CSRF protection applied
api.phpStateless API routes returning JSON (add it with php artisan install:api)
console.phpCustom Artisan (CLI) commands
channels.phpWebSocket broadcasting channel authorisation

A useful mental model is a postal sorting office: every letter (request) carries an address (URL) and a class of service (HTTP verb), and the sorting rules (routes) decide where it is delivered.

Defining Routes

Routes are registered by calling a method named after the HTTP verb on the Route facade. The handler is either a closure or, far more commonly in real apps, a reference to a controller method.

<?php
use App\Http\Controllers\UserController;
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;

// A closure handler (fine for tiny things)
Route::get('/welcome', fn () => 'Hello, World!');

// The usual form: point a route at a controller method
Route::get('/users', [UserController::class, 'index']);

// A named route (we'll use the name to generate URLs later)
Route::get('/profile', [ProfileController::class, 'show'])->name('profile');

// Match other verbs
Route::post('/users', [UserController::class, 'store']);
Route::put('/users/{user}', [UserController::class, 'update']);
Route::delete('/users/{user}', [UserController::class, 'destroy']);

Every route definition combines four things:

  • HTTP verbget, post, put, patch, delete
  • URI pattern — the path to match, e.g. /users/{id}
  • Handler — a closure or [Controller::class, 'method']
  • Modifiers — optional extras chained on, like ->name(), ->middleware(), or ->where()

Parameters & Named Routes

Route parameters

Curly braces capture a segment of the URL and pass it to the handler as an argument. Think of parameters as the fields on a form — they collect the specific data a request needs.

<?php
// A single parameter
Route::get('/users/{id}', function (string $id) {
    return 'User ' . $id;
});

// Multiple parameters, in order
Route::get('/posts/{post}/comments/{comment}', function ($postId, $commentId) {
    return "Post {$postId}, Comment {$commentId}";
});

// An optional parameter (note the ? and the default value)
Route::get('/posts/{slug?}', function (?string $slug = null) {
    return $slug ?? 'All posts';
});

Constraints

Constraints restrict a parameter to a pattern, rejecting anything that does not match before your code runs — like input validation for URLs.

<?php
// Only numeric IDs
Route::get('/user/{id}', [UserController::class, 'show'])
    ->where('id', '[0-9]+');

// A slug of letters, numbers, hyphens, underscores
Route::get('/post/{slug}', [PostController::class, 'show'])
    ->where('slug', '[A-Za-z0-9\-_]+');

// Fluent helpers read even better
Route::get('/user/{id}', [UserController::class, 'show'])->whereNumber('id');
Route::get('/category/{name}', [CategoryController::class, 'show'])->whereAlpha('name');

Named routes

Naming a route lets you refer to it by name instead of hard-coding its URL. If the path ever changes, you update it in one place and every reference follows.

<?php
Route::get('/user/{id}/profile', [ProfileController::class, 'show'])
    ->name('user.profile');

// Generate a URL to it
$url = route('user.profile', ['id' => 1]);   // /user/1/profile

// Redirect to it
return redirect()->route('user.profile', ['id' => 1]);

✅ Why named routes matter

Imagine a "view product" link appearing in a dozen templates. Name the route products.show, reference it everywhere with route('products.show', $product), and you can later change the URL from /products/{id} to /shop/items/{id} by editing a single line.

How a named route becomes a URL A view calls route with the name profile, the route collection looks it up, and returns the concrete URL slash user slash profile. View / Controller route('profile') Route Collection name → pattern Generated URL /user/profile
Figure 1 — A named route decouples your code from the concrete URL: you reference the name, Laravel builds the path.

Route Groups

Route groups share attributes — a URL prefix, middleware, a name prefix — across many routes so you do not repeat yourself. They are like cable ties: bundle related routes and apply common settings to all of them at once.

<?php
// Shared prefix: URLs become /admin/users and /admin/posts
Route::prefix('admin')->group(function () {
    Route::get('/users', [AdminController::class, 'users']);
    Route::get('/posts', [AdminController::class, 'posts']);
});

// Shared middleware: both routes require authentication
Route::middleware(['auth'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/settings', [SettingsController::class, 'index']);
});

// Combine several attributes fluently
Route::prefix('admin')
    ->middleware(['auth', 'can:admin'])
    ->name('admin.')
    ->group(function () {
        // name = admin.users, URL = /admin/users, guarded by both middleware
        Route::get('/users', [AdminController::class, 'users'])->name('users');
    });

Groups can nest, which is handy for API versioning:

<?php
Route::prefix('api')->group(function () {
    Route::prefix('v1')->group(function () {
        Route::apiResource('products', ProductApiController::class);
        // URL: /api/v1/products
    });
});

Route Model Binding

Route model binding automatically resolves an Eloquent model from a route parameter — no manual database query needed. It is like a personal assistant who hands you the whole client file the moment you mention the client's name.

flowchart LR A["Route: /users/{user}"] --> B{Model Binding} B --> C[Find User by route key] C --> D[Inject User into controller] D --> E[Method runs with the model]

Implicit binding

When the parameter name matches a type-hinted model variable, Laravel fetches it for you and returns a 404 if it is not found.

<?php
// The {user} segment is resolved to a User model automatically
Route::get('/users/{user}', [UserController::class, 'show']);

// In UserController — $user is already loaded
public function show(User $user)
{
    return view('users.show', ['user' => $user]);
}

Custom keys

By default binding uses the id column. To bind by a friendlier column such as a slug, name it in the route:

<?php
// Resolve the post by its slug column instead of id
Route::get('/posts/{post:slug}', [PostController::class, 'show']);

💡 Cleaner URLs, for free

Custom-key binding lets a blog serve /posts/laravel-routing-guide instead of /posts/42 while still loading the correct model automatically.

RESTful Resource Controllers

Most CRUD screens follow the same seven actions. Laravel's resource routes generate all of them from a single line, keeping your URLs and method names consistent — like a standardised process on a factory line.

<?php
// One line registers seven routes
Route::resource('photos', PhotoController::class);
VerbURIActionRoute namePurpose
GET/photosindexphotos.indexList all resources
GET/photos/createcreatephotos.createShow the "new" form
POST/photosstorephotos.storeSave a new resource
GET/photos/{photo}showphotos.showShow one resource
GET/photos/{photo}/editeditphotos.editShow the "edit" form
PUT/PATCH/photos/{photo}updatephotos.updateUpdate a resource
DELETE/photos/{photo}destroyphotos.destroyDelete a resource

Generate a matching controller with all seven methods stubbed out:

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

You can trim or reshape the set:

<?php
// Only a subset of actions
Route::resource('photos', PhotoController::class)->only(['index', 'show']);

// Everything except a few
Route::resource('photos', PhotoController::class)->except(['destroy']);

// API resources skip the create/edit form routes (JSON has no forms)
Route::apiResource('photos', PhotoApiController::class);

Single-action controllers

When a controller does exactly one thing, use the __invoke method and reference the class directly:

<?php
namespace App\Http\Controllers;

use App\Models\User;

class ShowProfile extends Controller
{
    public function __invoke(User $user)
    {
        return view('profile', ['user' => $user]);
    }
}

// Route registration — no method name needed
Route::get('/user/{user}', ShowProfile::class);

Request Validation & Responses

Controllers receive an Illuminate\Http\Request that carries all input. Validate it before you trust it — the receptionist not only directs visitors but checks their paperwork first.

<?php
public function store(Request $request)
{
    // Only validated data is returned; validation failures redirect back
    $validated = $request->validate([
        'title'      => 'required|max:255',
        'body'       => 'required',
        'publish_at' => 'nullable|date',
    ]);

    $article = Article::create($validated);

    return redirect()->route('articles.show', $article)
                     ->with('status', 'Article created!');
}

For complex rules, extract validation into a dedicated Form Request class. It keeps controllers thin and bundles authorisation with the rules:

<?php
// php artisan make:request StoreArticleRequest
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreArticleRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Article::class);
    }

    public function rules(): array
    {
        return [
            'title'       => 'required|max:255',
            'body'        => 'required',
            'category_id' => 'required|exists:categories,id',
        ];
    }
}

// The controller now stays tiny — validation happens before it runs
public function store(StoreArticleRequest $request)
{
    $article = Article::create($request->validated());
    return redirect()->route('articles.show', $article);
}

Response types

A controller can return many kinds of response — choose the format that fits the client:

<?php
return view('profile', ['user' => $user]);          // rendered HTML
return response()->json(['users' => User::all()]);  // JSON (APIs)
return redirect()->route('dashboard');              // redirect
return response()->download($path, 'invoice.pdf');  // file download
return response('OK', 200)->header('X-App', 'demo');// custom response

Middleware

Middleware are layers a request must pass through before reaching a controller (and that the response passes back through). Authentication, CSRF protection, and rate limiting are all middleware. Picture airport security checkpoints: every passenger clears them before reaching the gate.

flowchart LR A[HTTP Request] --> B[auth] B --> C[verified] C --> D[Custom middleware] D --> E[Controller] E --> F[HTTP Response]
<?php
// One middleware
Route::get('/profile', [ProfileController::class, 'show'])
    ->middleware('auth');

// Several at once
Route::get('/admin', [AdminController::class, 'index'])
    ->middleware(['auth', 'can:admin']);

// Applied to a whole group
Route::middleware(['auth'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/settings', [SettingsController::class, 'edit']);
});

Create your own to enforce custom rules — here, requiring an active subscription:

<?php
// php artisan make:middleware EnsureSubscribed
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureSubscribed
{
    public function handle(Request $request, Closure $next): Response
    {
        if ($request->user() && ! $request->user()->subscribed) {
            return redirect()->route('billing');
        }

        return $next($request);   // pass control to the next layer
    }
}

⚠️ Registering middleware in Laravel 11+

Recent Laravel no longer uses an app/Http/Kernel.php. Register middleware aliases and groups in bootstrap/app.php inside the ->withMiddleware(...) callback (for example, $middleware->alias(['subscribed' => EnsureSubscribed::class]);). Older tutorials that edit the Kernel are out of date.

Hands-on Exercise

🏋️ Build a Product Catalogue

Objective: Wire up public and admin routes plus controllers for a simple product catalogue, practising resource routes, groups, and model binding.

Instructions:

  1. Generate a model with a migration: php artisan make:model Product -m (fields: name, slug, description, price, category_id).
  2. Generate resource controllers for the public and admin sides.
  3. In routes/web.php, add public routes for browsing products (bind by slug) and an admin group, guarded by auth, exposing full CRUD.
  4. Implement index (list, filterable by category) and show (single product).
💡 Hint

Use Route::prefix('admin')->middleware('auth')->name('admin.')->group(...) for the admin side, and Route::get('/products/{product:slug}', ...) for clean public URLs. A resource controller can be created with php artisan make:controller Admin/ProductController --resource --model=Product.

✅ Example solution
<?php
// routes/web.php
use App\Http\Controllers\ProductController;
use App\Http\Controllers\Admin\ProductController as AdminProductController;
use Illuminate\Support\Facades\Route;

// Public — clean slug URLs
Route::get('/products', [ProductController::class, 'index'])->name('products.index');
Route::get('/products/{product:slug}', [ProductController::class, 'show'])->name('products.show');

// Admin — grouped, guarded, name-prefixed
Route::prefix('admin')
    ->middleware(['auth', 'can:admin'])
    ->name('admin.')
    ->group(function () {
        Route::resource('products', AdminProductController::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(Request $request)
    {
        $products = Product::query()
            ->when($request->query('category'), function ($q, $slug) {
                $q->whereHas('category', fn ($c) => $c->where('slug', $slug));
            })
            ->latest()
            ->paginate(12);

        return view('products.index', ['products' => $products]);
    }

    public function show(Product $product)   // bound by slug from the route
    {
        return view('products.show', ['product' => $product]);
    }
}

🎯 Quick Quiz

Question 1: How many routes does Route::resource('photos', PhotoController::class) register?

Question 2: What does route model binding do?

Question 3: Where do you register a custom middleware alias in Laravel 11+?

Summary & Quiz

🎉 Key Takeaways

  • Routes map a verb + URI to a handler; controllers group related request logic.
  • Parameters capture URL segments; constraints restrict them; named routes decouple code from URLs.
  • Route groups share prefixes, middleware, and name prefixes to keep routes DRY.
  • Route model binding loads models automatically — even by a custom key like a slug.
  • Resource controllers generate the seven RESTful routes; validate input (ideally with Form Requests) and guard routes with middleware.

📚 Further Reading

🚀 What's Next?

Your controllers keep returning views. Next we open up those views with the Blade template engine — how to render data, reuse layouts, build components, and handle forms safely.

🎉 Well done!

You can now route any request to the right code and hand back the right response.