Laravel Routing and Controllers

Defining routes, route parameters, controllers, route model binding, and resource controllers.

Defining routes

Routes map an incoming URL and HTTP method to the code that handles it. Browser-facing routes live in routes/web.php; routes meant for an API live in routes/api.php (in a fresh Laravel 11+ project, this file doesn't exist until you run php artisan install:api, which also sets up Sanctum for API token authentication).

PHP
<?php
// routes/web.php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return 'Welcome home!';
});

Route::get('/about', function () {
    return view('about');
});

The Route facade exposes one method per HTTP verb — Route::get(), Route::post(), Route::put(), Route::patch(), Route::delete() — each taking a URI pattern and a handler (a closure, or, far more commonly in real applications, a controller method).

Route parameters

A segment wrapped in curly braces captures part of the URL and passes it to the handler:

PHP
<?php

Route::get('/posts/{id}', function (string $id) {
    return "Showing post #{$id}";
});

// Optional parameter — note the `?` and a default value in the closure signature
Route::get('/posts/{id}/comments/{page?}', function (string $id, string $page = '1') {
    return "Post {$id}, comments page {$page}";
});

Add a where() constraint to restrict what a parameter matches (here, digits only), so a non-numeric id falls through to a 404 instead of reaching the handler at all:

PHP
<?php

Route::get('/posts/{id}', function (string $id) {
    return "Showing post #{$id}";
})->where('id', '[0-9]+');

Controllers

For anything beyond a trivial closure, logic belongs in a controller — a class that groups related request-handling methods together. Generate one with Artisan:

Bash
php artisan make:controller PostController
PHP
<?php
// app/Http/Controllers/PostController.php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::latest()->get();
        return view('posts.index', ['posts' => $posts]);
    }

    public function show(Post $post)
    {
        return view('posts.show', ['post' => $post]);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'body'  => 'required|string',
        ]);

        $post = Post::create($validated);

        return redirect()->route('posts.show', $post);
    }
}
PHP
<?php
// routes/web.php

use App\Http\Controllers\PostController;

Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
Route::get('/posts/{post}', [PostController::class, 'show'])->name('posts.show');
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');

show(Post $post) above is using route model binding — because the route parameter name ({post}) matches the type-hinted variable name, Laravel automatically looks up the Post by its route-segment ID and injects the actual model instance (or a 404 if none is found), with no manual Post::findOrFail($id) needed.

Resource controllers

Standard CRUD (index, create, store, show, edit, update, destroy) is common enough that Laravel generates and routes it in one step:

Bash
php artisan make:controller PostController --resource
PHP
<?php
// routes/web.php

use App\Http\Controllers\PostController;

Route::resource('posts', PostController::class);

That single Route::resource() call registers all seven conventional routes (GET /posts, GET /posts/create, POST /posts, GET /posts/{post}, GET /posts/{post}/edit, PUT/PATCH /posts/{post}, DELETE /posts/{post}), each named and pointed at the matching controller method. Run php artisan route:list at any time to see every registered route, its verb, its name, and which controller/method handles it.

Common mistakes

  • Putting real application logic directly inside route closures in routes/web.php — it works, but it's untestable in isolation and doesn't scale past a handful of trivial routes; controllers are the standard home for real logic.
  • Forgetting that route order matters when patterns could overlap — Laravel matches the first route that fits, so a broad pattern defined before a more specific one can shadow it.
  • Not using route model binding, and instead manually writing Post::findOrFail($id) in every controller method that receives an ID — more code, and it loses the automatic 404 handling model binding provides for free.

Interview questions

Q: What is route model binding, and what problem does it remove? When a route parameter's name matches a type-hinted Eloquent model parameter in the controller method (or closure), Laravel automatically resolves the model instance from the database using that route segment as its key, injecting the fully loaded model instead of just the raw ID string. It removes the repetitive Model::findOrFail($id) (and its manual 404 handling) that would otherwise appear at the top of nearly every controller method that operates on a single record.

Q: What does Route::resource() actually generate? It registers the seven conventional RESTful routes for a resource in one call — index, create, store, show, edit, update, and destroy — each mapped to the correspondingly named method on the given controller, and each given a predictable route name (like posts.show). It's a shorthand for what would otherwise be seven separate Route::get/post/put/delete calls written by hand.