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:
web.php- Routes for web interface (with session, CSRF protection, etc.)api.php- Routes for API endpoints (stateless, often returning JSON)console.php- Routes for custom Artisan commandschannels.php- Routes for broadcasting channels
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:
- HTTP Verb - GET, POST, PUT, PATCH, DELETE, etc.
- URI Pattern - The URL pattern to match
- Handler - Closure function or controller reference
- Optional Modifiers - Route names, middleware, constraints
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.
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.
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.
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
- Create a new Laravel project or use an existing one
-
Generate a Product model with migration:
php artisan make:model Product -m- Add fields: name, slug, description, price, category_id
-
Generate a Category model with migration:
php artisan make:model Category -m- Add fields: name, slug
-
Create resource controllers:
php artisan make:controller ProductController --resourcephp artisan make:controller CategoryController --resourcephp artisan make:controller Admin\\ProductController --resource
-
Define routes in
routes/web.php:- Public routes for browsing products and categories
- Admin routes for managing products and categories
-
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
- Routes connect URLs to controller actions or closure functions
- Controllers organize related request handling logic in classes
- Route parameters can be captured and passed to controllers
- Named routes make URL generation easier and more maintainable
- Route model binding automatically resolves models from parameters
- Resource controllers follow RESTful conventions for CRUD operations
- Middleware can filter requests before they reach controllers
- Request validation ensures data integrity before processing
- Controllers can return various response types (views, JSON, downloads)
In our next session, we'll explore Blade templating, which brings views to life in Laravel applications.
Further Resources
- Laravel Routing Documentation
- Laravel Controllers Documentation
- Laracasts: Laravel From Scratch
- "Laravel Up & Running" by Matt Stauffer (Chapter on Routing)
- "Mastering Laravel" by Christopher John Pecoraro