Laravel Routing and Controllers

Directing traffic and processing requests in your Laravel application

Introduction to Routing

Routing is the mechanism that connects incoming HTTP requests to the code that handles them. Think of routes as the receptionist of your application - they determine who (which controller) should handle each visitor (request) that arrives.

In Laravel, routes are defined in files located in the routes/ directory:

A real-world analogy for routing would be a postal sorting system. Each letter (request) has an address (URL) that determines where it should be delivered (which controller and method).

Basic Route Definition

Let's explore the fundamentals of defining routes in Laravel. Routes are registered using HTTP verb methods on the Route facade.

Basic Routes in web.php

// Basic GET route
Route::get('/welcome', function () {
    return 'Hello, World!';
});

// Route to a controller
Route::get('/users', [UserController::class, 'index']);

// Named route
Route::get('/profile', [ProfileController::class, 'show'])->name('profile');

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

// Optional parameters
Route::get('/posts/{slug?}', [PostController::class, 'show']);

// Route with constraints
Route::get('/user/{id}', [UserController::class, 'show'])
    ->where('id', '[0-9]+');

Each route definition includes:

Route Parameters

Route parameters allow you to capture segments of the URI within your routes. These parameters are passed to your route's handler as arguments.

Route Parameters Examples

// A simple parameter
Route::get('/users/{id}', function ($id) {
    return 'User ' . $id;
});

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

// Controller example with parameters
Route::get('/users/{id}', [UserController::class, 'show']);

// In UserController.php
public function show($id)
{
    $user = User::findOrFail($id);
    return view('users.show', ['user' => $user]);
}

Think of route parameters like form fields in a paper form. They collect specific information that's needed to process the request properly.

Parameter Constraints

You can restrict parameters to match specific patterns using constraints:

// Numeric constraint
Route::get('/user/{id}', [UserController::class, 'show'])
    ->where('id', '[0-9]+');

// Alpha constraint
Route::get('/category/{slug}', [CategoryController::class, 'show'])
    ->where('slug', '[A-Za-z]+');

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

// Multiple constraints
Route::get('/posts/{postId}/comments/{commentId}', [CommentController::class, 'show'])
    ->where(['postId' => '[0-9]+', 'commentId' => '[0-9]+']);
    
// Global constraint patterns (in RouteServiceProvider)
public function boot()
{
    Route::pattern('id', '[0-9]+');
    // Now all {id} parameters must be numeric
}

Constraints are like input validation for your URLs - they ensure the parameters match expected formats before processing the request.

Route Names

Named routes allow you to generate URLs or redirects without hardcoding paths. This is particularly useful when routes change, as you only need to update them in one place.

Defining and Using Named Routes

// Define a named route
Route::get('/user/profile', [ProfileController::class, 'show'])
    ->name('profile');

// Generate a URL to a named route
$url = route('profile'); // Returns: http://example.com/user/profile

// Redirect to a named route
return redirect()->route('profile');

// Named route with parameters
Route::get('/user/{id}/profile', [ProfileController::class, 'show'])
    ->name('user.profile');

// Generate URL with parameters
$url = route('user.profile', ['id' => 1]); // Returns: http://example.com/user/1/profile

Named routes are like having contacts in your phone - instead of remembering everyone's number (URL), you just reference them by name.

graph LR A[View/Controller] -->|route('profile')| B[Route Collection] B -->|Generates| C[http://example.com/user/profile] style B fill:#f9d77e

A practical example: imagine you have a 'view product' route that appears in multiple places in your e-commerce application. If you decide to change the URL pattern from /products/{id} to /shop/items/{id}, you'd only need to update the route definition once - all references to the named route would automatically use the new URL.

Route Groups

Route groups allow you to share route attributes (middleware, namespaces, prefixes, etc.) across multiple routes without repeating yourself.

Route Group Examples

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

// Route group with middleware
Route::middleware(['auth'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/settings', [SettingsController::class, 'index']);
    // Both routes will use auth middleware
});

// Multiple attributes
Route::prefix('admin')
    ->middleware(['auth', 'admin'])
    ->name('admin.')
    ->group(function () {
        Route::get('/users', [AdminController::class, 'users'])->name('users');
        // Route name will be 'admin.users'
        // URL will be '/admin/users'
        // Will use both 'auth' and 'admin' middleware
    });

Route groups are like organizing cables with cable ties - they bundle related routes together and apply common settings to all of them.

Nested Groups

Groups can be nested to create a hierarchy of routes:

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

Route Model Binding

Route model binding is a powerful feature that automatically resolves Eloquent models from route parameters, saving you from manually querying the database.

Implicit Binding

// Implicit binding (parameter name matches model variable name)
Route::get('/users/{user}', [UserController::class, 'show']);

// In UserController.php
public function show(User $user)
{
    // $user is already the User model instance with ID matching the route parameter
    return view('users.show', ['user' => $user]);
}

Explicit Binding

// In RouteServiceProvider.php
public function boot()
{
    Route::bind('user', function ($value) {
        return User::where('username', $value)->firstOrFail();
    });
}

// Now any {user} parameter will be resolved by username instead of ID
Route::get('/users/{user}', [UserController::class, 'show']);

Custom Keys

// In User.php model
public function getRouteKeyName()
{
    return 'username'; // Instead of 'id'
}

Route model binding is like having a personal assistant who brings you the complete file (model) whenever you mention a client's name (parameter) - you don't have to search for it yourself.

flowchart TD A[Route: /users/{user}] --> B{Model Binding} B --> C[Find User where id = parameter] C --> D[Inject User model to controller] D --> E[Controller processes request] style B fill:#f97e7e

A real-world application: when building a blog, route model binding lets you use clean URLs like /posts/laravel-routing-guide instead of /posts/42, while automatically loading the correct post model.

RESTful Resource Controllers

Laravel provides a convenient way to define RESTful resource routes that map to controller actions, following REST conventions.

Resource Route Definition

// Single resource controller registration
Route::resource('photos', PhotoController::class);

// This single line creates 7 routes:
// GET /photos - index
// GET /photos/create - create
// POST /photos - store
// GET /photos/{photo} - show
// GET /photos/{photo}/edit - edit
// PUT/PATCH /photos/{photo} - update
// DELETE /photos/{photo} - destroy

Generating a Resource Controller

// Terminal command to create a resource controller
php artisan make:controller PhotoController --resource
          
// The generated controller will have all the necessary methods
class PhotoController extends Controller
{
    public function index() { /* List all photos */ }
    public function create() { /* Show creation form */ }
    public function store(Request $request) { /* Store new photo */ }
    public function show(Photo $photo) { /* Show single photo */ }
    public function edit(Photo $photo) { /* Show edit form */ }
    public function update(Request $request, Photo $photo) { /* Update photo */ }
    public function destroy(Photo $photo) { /* Delete photo */ }
}
HTTP Verb URI Action Route Name Purpose
GET /photos index photos.index Display a list of resources
GET /photos/create create photos.create Show form to create new resource
POST /photos store photos.store Store a newly created resource
GET /photos/{photo} show photos.show Display a specific resource
GET /photos/{photo}/edit edit photos.edit Show form to edit resource
PUT/PATCH /photos/{photo} update photos.update Update a specific resource
DELETE /photos/{photo} destroy photos.destroy Delete a specific resource

Resource controllers are like standardized processes in a factory - they ensure consistent handling of resources with predictable URLs and actions.

Customizing Resource Routes

// Only certain actions
Route::resource('photos', PhotoController::class)->only([
    'index', 'show'
]);

// Exclude certain actions
Route::resource('photos', PhotoController::class)->except([
    'create', 'store', 'update', 'destroy'
]);

// API resources (no create/edit forms)
Route::apiResource('photos', PhotoApiController::class);
// This excludes the create and edit routes that return forms

Controllers Deep Dive

Controllers group related request handling logic into a single class. They are the C in MVC and coordinate the interaction between the user, the views, and the models.

Basic Controller

namespace App\Http\Controllers;

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

class UserController extends Controller
{
    public function index()
    {
        $users = User::all();
        return view('users.index', ['users' => $users]);
    }

    public function show($id)
    {
        $user = User::findOrFail($id);
        return view('users.show', ['user' => $user]);
    }
}

If routes are the receptionists of your application, controllers are like department managers who handle specific types of requests. A UserController handles all user-related actions, an OrderController manages orders, and so on.

Single Action Controllers

When a controller only needs to handle a single action, you can use the __invoke method:

namespace App\Http\Controllers;

use App\Models\User;

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

// Route registration
Route::get('/user/{id}', ShowProfile::class);

Single action controllers are like specialized workers who only perform one specific task, but do it very well.

Dependency Injection in Controllers

Laravel's service container automatically resolves dependencies in controller constructors and methods, allowing you to easily use services and objects.

Constructor Injection

namespace App\Http\Controllers;

use App\Repositories\UserRepository;
use App\Services\Logger;

class UserController extends Controller
{
    protected $users;
    protected $logger;

    public function __construct(UserRepository $users, Logger $logger)
    {
        $this->users = $users;
        $this->logger = $logger;
    }

    public function show($id)
    {
        $this->logger->log("User $id profile viewed");
        $user = $this->users->find($id);
        return view('users.show', ['user' => $user]);
    }
}

Method Injection

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;
use App\Services\UserStats;

class UserController extends Controller
{
    public function show(Request $request, UserStats $stats, $id)
    {
        // Request and UserStats are automatically injected
        $user = User::findOrFail($id);
        $userStats = $stats->gather($user);
        
        return view('users.show', [
            'user' => $user,
            'stats' => $userStats,
        ]);
    }
}

Dependency injection in controllers is like having specialized tools automatically handed to you when you start a specific job - everything you need is provided without you having to assemble it yourself.

This approach makes your controllers more testable, as you can easily mock dependencies when testing.

Request Handling

Laravel provides powerful tools for working with HTTP requests, including validation, file uploads, and input retrieval.

Accessing Request Data

public function store(Request $request)
{
    // All input data
    $allData = $request->all();
    
    // Specific input field
    $name = $request->input('name');
    
    // With default value
    $page = $request->input('page', 1);
    
    // Determine if input exists
    if ($request->has('name')) {
        // ...
    }
    
    // Retrieve from nested input
    $name = $request->input('user.name');
    
    // Only retrieve specific fields
    $credentials = $request->only(['email', 'password']);
    
    // Retrieve all except certain fields
    $data = $request->except(['credit_card']);
}

Request Validation

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

    // Only validated data is available in $validated
    // Create article using validated data
    $article = Article::create($validated);
    
    return redirect()->route('articles.show', $article);
}

// With custom error messages
public function store(Request $request)
{
    $validated = $request->validate([
        'email' => 'required|email|unique:users',
        'password' => 'required|min:8|confirmed',
    ], [
        'email.unique' => 'This email is already registered!',
        'password.min' => 'Password must be at least 8 characters.',
    ]);
    
    // Process valid data...
}

Request handling in Laravel is like having a receptionist who not only directs visitors but also checks their credentials and ensures all forms are filled out correctly before passing them along.

Form Request Classes

For complex validation scenarios, you can create dedicated Form Request classes:

// Generate a form request
php artisan make:request StoreArticleRequest

// In app/Http/Requests/StoreArticleRequest.php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreArticleRequest extends FormRequest
{
    public function authorize()
    {
        // Check if user is authorized to make this request
        return $this->user()->can('create', Article::class);
    }

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

// In controller
public function store(StoreArticleRequest $request)
{
    // Request is already validated!
    $article = Article::create($request->validated());
    
    return redirect()->route('articles.show', $article);
}

Form Request classes are like having specialized forms with built-in validation rules that different departments use to process specific types of information.

Response Types

Laravel controllers can return various types of responses to suit different needs:

Different Response Types

// Simple Response
public function simple()
{
    return 'Hello World';
}

// View Response
public function showProfile($id)
{
    return view('profile', ['user' => User::findOrFail($id)]);
}

// JSON Response
public function users()
{
    return response()->json([
        'users' => User::all()
    ]);
}

// File Download
public function download($id)
{
    $document = Document::findOrFail($id);
    return response()->download($document->path, $document->name);
}

// File Stream
public function stream($id)
{
    $video = Video::findOrFail($id);
    return response()->file($video->path);
}

// Redirect
public function store(Request $request)
{
    // Process the data...
    
    return redirect()->route('dashboard')
                     ->with('status', 'Profile created!');
}

// Custom Response
public function custom()
{
    return response($content, 200)
                  ->header('Content-Type', 'text/plain')
                  ->cookie('name', 'value', $minutes);
}

The variety of response types in Laravel is like having different ways to package your products - whether as a digital download, physical product, or subscription service, you can choose the format that best suits your customer's needs.

Middleware in Routes

Middleware provides a convenient mechanism for filtering HTTP requests entering your application. They act as layers that requests must pass through before reaching your controllers.

graph LR A[HTTP Request] --> B[Auth Middleware] B --> C[CSRF Middleware] C --> D[Custom Middleware] D --> E[Controller] E --> F[HTTP Response] style B fill:#f9d77e style C fill:#7ef9a5 style D fill:#7ecbf9 style E fill:#f97e7e

Applying Middleware to Routes

// Single middleware
Route::get('/profile', [ProfileController::class, 'show'])
     ->middleware('auth');

// Multiple middleware
Route::get('/admin/dashboard', [AdminController::class, 'dashboard'])
     ->middleware(['auth', 'admin']);

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

// Controller middleware in constructor
class AdminController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
        $this->middleware('admin')->only(['settings', 'users']);
        $this->middleware('log')->except('dashboard');
    }
    
    // Controller methods...
}

Middleware are like security checkpoints at an airport. Each passenger (request) must go through security (middleware) before reaching their gate (controller). Some checkpoints are mandatory for everyone, while others are only for specific destinations.

Creating Custom Middleware

You can create your own middleware to handle custom verification or preprocessing:

// Generate a middleware
php artisan make:middleware CheckSubscription

// In app/Http/Middleware/CheckSubscription.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class CheckSubscription
{
    public function handle(Request $request, Closure $next)
    {
        if ($request->user() && !$request->user()->subscribed) {
            // Redirect to subscription page if user is not subscribed
            return redirect()->route('billing');
        }

        return $next($request);
    }
}

// Register in app/Http/Kernel.php
protected $routeMiddleware = [
    // ... other middleware
    'subscribed' => \App\Http\Middleware\CheckSubscription::class,
];

// Use in routes
Route::get('/premium-content', [ContentController::class, 'premium'])
     ->middleware(['auth', 'subscribed']);

Custom middleware is like having specialized security personnel who check for specific credentials beyond the standard ID check.

Practical Example: Building a Blog System

Let's tie everything together with a real-world example of routes and controllers for a blog system:

Routes Definition (routes/web.php)

// Public routes
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/blog', [BlogController::class, 'index'])->name('blog.index');
Route::get('/blog/{post:slug}', [BlogController::class, 'show'])->name('blog.show');
Route::get('/categories/{category:slug}', [CategoryController::class, 'show'])->name('categories.show');

// Authentication routes
Route::get('/login', [AuthController::class, 'showLoginForm'])->name('login');
Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');

// Admin routes
Route::prefix('admin')->middleware(['auth', 'admin'])->name('admin.')->group(function () {
    Route::get('/', [AdminController::class, 'dashboard'])->name('dashboard');
    
    // Blog post management
    Route::resource('posts', AdminPostController::class);
    
    // Category management
    Route::resource('categories', AdminCategoryController::class);
    
    // User management
    Route::resource('users', AdminUserController::class)->middleware('super-admin');
});

BlogController Implementation

namespace App\Http\Controllers;

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

class BlogController extends Controller
{
    public function index(Request $request)
    {
        // Get query parameters
        $category = $request->query('category');
        $search = $request->query('search');
        
        // Start query builder
        $query = Post::where('published', true)
                     ->orderBy('published_at', 'desc');
        
        // Apply category filter if provided
        if ($category) {
            $query->whereHas('category', function ($q) use ($category) {
                $q->where('slug', $category);
            });
        }
        
        // Apply search if provided
        if ($search) {
            $query->where(function ($q) use ($search) {
                $q->where('title', 'like', "%{$search}%")
                  ->orWhere('excerpt', 'like', "%{$search}%")
                  ->orWhere('body', 'like', "%{$search}%");
            });
        }
        
        // Paginate results
        $posts = $query->paginate(10);
        
        // Get all categories for the sidebar
        $categories = Category::withCount('posts')->get();
        
        return view('blog.index', [
            'posts' => $posts,
            'categories' => $categories,
            'currentCategory' => $category,
            'search' => $search
        ]);
    }
    
    public function show(Post $post)
    {
        // Check if post is published
        if (!$post->published && !auth()->user()?->isAdmin()) {
            abort(404);
        }
        
        // Increment view count
        $post->increment('views');
        
        // Get related posts
        $relatedPosts = Post::where('category_id', $post->category_id)
                            ->where('id', '!=', $post->id)
                            ->where('published', true)
                            ->limit(3)
                            ->get();
                            
        return view('blog.show', [
            'post' => $post,
            'relatedPosts' => $relatedPosts
        ]);
    }
}

AdminPostController (for the admin section)

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\Post;
use App\Models\Category;
use App\Http\Requests\StorePostRequest;
use App\Http\Requests\UpdatePostRequest;
use Illuminate\Support\Str;

class AdminPostController extends Controller
{
    public function index()
    {
        $posts = Post::with('category', 'author')
                     ->latest()
                     ->paginate(15);
                     
        return view('admin.posts.index', [
            'posts' => $posts
        ]);
    }
    
    public function create()
    {
        $categories = Category::all();
        
        return view('admin.posts.create', [
            'categories' => $categories
        ]);
    }
    
    public function store(StorePostRequest $request)
    {
        // Get validated data
        $data = $request->validated();
        
        // Add author ID
        $data['user_id'] = auth()->id();
        
        // Generate slug from title
        $data['slug'] = Str::slug($data['title']);
        
        // Handle file upload if present
        if ($request->hasFile('featured_image')) {
            $data['featured_image'] = $request->file('featured_image')
                                              ->store('posts', 'public');
        }
        
        // Create the post
        $post = Post::create($data);
        
        // Attach tags if present
        if (isset($data['tags'])) {
            $post->tags()->attach($data['tags']);
        }
        
        return redirect()->route('admin.posts.index')
                         ->with('success', 'Post created successfully');
    }
    
    // Other resource methods (show, edit, update, destroy)...
}

This example demonstrates a practical implementation of routes and controllers for a real-world blog system. It shows how routes are organized, how controllers handle different aspects of the application, and how various Laravel features come together.

Best Practices

Following best practices ensures your routes and controllers remain maintainable as your application grows:

Route Organization

  • Group related routes together
  • Use meaningful route names that reflect their purpose
  • Keep route files clean by using route groups
  • Consider creating separate route files for large modules

Controller Best Practices

  • Follow SOLID principles, especially Single Responsibility
  • Keep controllers thin – move business logic to services or models
  • Use Form Request classes for complex validation
  • Use type hints for dependencies and route model binding
  • Return appropriate status codes in API responses

Security Considerations

  • Always validate input data
  • Use middleware for authentication and authorization
  • Be careful with route parameters and user input
  • Use CSRF protection for all forms (web middleware group provides this)
  • Consider rate limiting for public-facing routes

Think of these best practices like a well-designed road system - proper signs (route names), logical layouts (organization), and safety features (security) make the journey smooth for everyone.

Practical Activity: Building a Product Catalog

Let's solidify your understanding with a hands-on activity. You'll create routes and controllers for a simple product catalog:

Activity: Product Catalog Routes and Controllers

  1. Create a new Laravel project or use an existing one
  2. Generate a Product model with migration: php artisan make:model Product -m
    • Add fields: name, slug, description, price, category_id
  3. Generate a Category model with migration: php artisan make:model Category -m
    • Add fields: name, slug
  4. Create resource controllers:
    • php artisan make:controller ProductController --resource
    • php artisan make:controller CategoryController --resource
    • php artisan make:controller Admin\\ProductController --resource
  5. Define routes in routes/web.php:
    • Public routes for browsing products and categories
    • Admin routes for managing products and categories
  6. Implement the controller methods for:
    • Displaying all products with filtering by category
    • Showing single product details
    • Admin CRUD operations for products

Extension: Add search functionality, product image uploads, and pagination to practice more controller techniques.

Summary and Key Takeaways

In our next session, we'll explore Blade templating, which brings views to life in Laravel applications.

Further Resources