Skip to main content
Technology & EngineeringPhp Laravel270 lines

Routing Middleware

Laravel routing, route groups, resource controllers, and middleware for request filtering and authentication

Quick Summary18 lines
You are an expert in Laravel routing and middleware for building Laravel applications. You design route structures that are RESTful, grouped logically, and protected by focused middleware that each handle a single cross-cutting concern.

## Key Points

- Use `Route::resource()` or `Route::apiResource()` to follow RESTful conventions and reduce boilerplate.
- Name every route so URL generation uses `route()` instead of hardcoded paths.
- Keep middleware focused on a single responsibility; compose multiple middleware for complex policies.
- Register global middleware (CORS, trimming) in `bootstrap/app.php`; apply feature-specific middleware per route or group.
- Use Form Request classes for validation rather than validating inside controllers.
- Prefer implicit route model binding over manual `findOrFail()` calls.
- Use `scopeBindings()` on nested resource routes to enforce parent-child ownership.
- **Route ordering**: More specific routes must come before catch-all or wildcard routes; Laravel matches top-down.
- **Forgetting `name()`**: Without named routes, refactoring URLs requires find-and-replace everywhere.
- **Middleware stacking conflicts**: Applying `auth` and `guest` middleware on overlapping routes causes redirect loops.
- **Not using HTTPS in production**: Use `URL::forceScheme('https')` or the `TrustProxies` middleware behind a load balancer.
- **Fat controllers**: Move business logic to service classes or actions; controllers should only handle HTTP concerns.
skilldb get php-laravel-skills/Routing MiddlewareFull skill: 270 lines
Paste into your CLAUDE.md or agent config

Routing & Middleware — PHP/Laravel

You are an expert in Laravel routing and middleware for building Laravel applications. You design route structures that are RESTful, grouped logically, and protected by focused middleware that each handle a single cross-cutting concern.

Core Philosophy

Routes are the public API of the application, and their design communicates intent to both developers and consumers. A well-structured route file reads like a table of contents: resource routes for CRUD operations, named routes for URL generation, grouped routes for shared middleware and prefixes. When routes are scattered, unnamed, and inconsistently structured, every URL change requires a codebase-wide search-and-replace, and new developers cannot understand the application's surface area by reading the route file. Treat routes as a first-class design artifact, not an implementation detail.

Middleware embodies the principle of separation of concerns for HTTP request handling. Each middleware should handle exactly one responsibility -- authentication, rate limiting, CORS, input transformation, logging -- and compose with others through the middleware stack. A middleware that checks authentication, validates a subscription, and logs the request is doing three jobs and is impossible to reuse for any of them individually. Small, focused middleware can be mixed and matched across route groups, providing flexible security and request processing without duplication.

Route model binding and Form Requests eliminate entire categories of boilerplate and bugs. Implicit route model binding replaces manual findOrFail() calls with automatic resolution, ensuring consistent 404 behavior across every endpoint. Form Requests centralize validation rules so controllers never contain validation logic. Together, these features mean that by the time a controller method executes, it can trust that the model exists and the input is valid. Controllers become thin orchestrators that delegate to services, and the HTTP layer's responsibilities are clearly bounded.

Overview

Laravel's router maps HTTP requests to controller actions or closures. Middleware provides a mechanism for filtering HTTP requests entering the application, handling concerns like authentication, CORS, rate limiting, and request transformation.

Core Concepts

Route Definitions

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PostController;

// Basic routes
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');

// Resource controller (generates index, create, store, show, edit, update, destroy)
Route::resource('posts', PostController::class);

// API resource (omits create and edit views)
Route::apiResource('posts', PostController::class);

// Single-action controller
Route::post('/newsletter/subscribe', SubscribeToNewsletterController::class);

Route Groups and Prefixes

Route::prefix('admin')
    ->name('admin.')
    ->middleware(['auth', 'verified', 'role:admin'])
    ->group(function () {
        Route::resource('users', Admin\UserController::class);
        Route::resource('posts', Admin\PostController::class);
        Route::get('dashboard', Admin\DashboardController::class)->name('dashboard');
    });

// API versioning
Route::prefix('api/v1')
    ->name('api.v1.')
    ->middleware('api')
    ->group(base_path('routes/api_v1.php'));

Route Model Binding

// Implicit binding (resolves by ID automatically)
Route::get('/posts/{post}', function (Post $post) {
    return $post;
});

// Custom key for implicit binding
Route::get('/posts/{post:slug}', function (Post $post) {
    return $post;
});

// Scoped binding (comment must belong to post)
Route::get('/posts/{post}/comments/{comment:id}', function (Post $post, Comment $comment) {
    return $comment;
})->scopeBindings();

// Explicit binding in RouteServiceProvider
public function boot(): void
{
    Route::model('post', Post::class);

    Route::bind('user', function (string $value) {
        return User::where('username', $value)->firstOrFail();
    });
}

Route Parameters and Constraints

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

Route::get('/posts/{slug}', [PostController::class, 'show'])
    ->where('slug', '[a-z0-9\-]+');

// Global constraints in RouteServiceProvider
Route::pattern('id', '[0-9]+');

Implementation Patterns

Middleware Definition

namespace App\Http\Middleware;

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

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

        return $next($request);
    }
}

Middleware with Parameters

class RoleMiddleware
{
    public function handle(Request $request, Closure $next, string ...$roles): Response
    {
        if (! $request->user() || ! $request->user()->hasAnyRole($roles)) {
            abort(403, 'Unauthorized.');
        }

        return $next($request);
    }
}

// Registration in bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'role' => \App\Http\Middleware\RoleMiddleware::class,
        'subscribed' => \App\Http\Middleware\EnsureUserIsSubscribed::class,
    ]);
})

// Usage
Route::get('/admin', AdminController::class)->middleware('role:admin,editor');

After-Middleware (Terminable)

class LogResponseTime
{
    public function handle(Request $request, Closure $next): Response
    {
        $request->attributes->set('start_time', microtime(true));

        return $next($request);
    }

    public function terminate(Request $request, Response $response): void
    {
        $duration = microtime(true) - $request->attributes->get('start_time');
        Log::info('Request duration', [
            'url'      => $request->fullUrl(),
            'duration' => round($duration * 1000, 2) . 'ms',
        ]);
    }
}

Rate Limiting

// In AppServiceProvider or RouteServiceProvider
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

RateLimiter::for('uploads', function (Request $request) {
    return $request->user()->isPremium()
        ? Limit::none()
        : Limit::perMinute(5)->by($request->user()->id);
});

// Apply to routes
Route::middleware('throttle:api')->group(function () {
    Route::apiResource('posts', PostController::class);
});

Controller Structure

namespace App\Http\Controllers;

use App\Http\Requests\StorePostRequest;
use App\Http\Resources\PostResource;
use App\Models\Post;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::published()
            ->with('author')
            ->latest()
            ->paginate(15);

        return PostResource::collection($posts);
    }

    public function store(StorePostRequest $request)
    {
        $post = $request->user()->posts()->create($request->validated());

        return new PostResource($post);
    }

    public function show(Post $post)
    {
        $post->load(['author', 'comments.user', 'tags']);

        return new PostResource($post);
    }
}

Best Practices

  • Use Route::resource() or Route::apiResource() to follow RESTful conventions and reduce boilerplate.
  • Name every route so URL generation uses route() instead of hardcoded paths.
  • Keep middleware focused on a single responsibility; compose multiple middleware for complex policies.
  • Register global middleware (CORS, trimming) in bootstrap/app.php; apply feature-specific middleware per route or group.
  • Use Form Request classes for validation rather than validating inside controllers.
  • Prefer implicit route model binding over manual findOrFail() calls.
  • Use scopeBindings() on nested resource routes to enforce parent-child ownership.

Common Pitfalls

  • Route ordering: More specific routes must come before catch-all or wildcard routes; Laravel matches top-down.
  • Forgetting name(): Without named routes, refactoring URLs requires find-and-replace everywhere.
  • Middleware stacking conflicts: Applying auth and guest middleware on overlapping routes causes redirect loops.
  • Not using HTTPS in production: Use URL::forceScheme('https') or the TrustProxies middleware behind a load balancer.
  • Fat controllers: Move business logic to service classes or actions; controllers should only handle HTTP concerns.
  • Ignoring rate limiting on public APIs: Always apply throttle middleware to prevent abuse.

Anti-Patterns

  • The catch-all route — defining Route::any('/{path}', ...) early in the route file and wondering why more specific routes are never matched. Laravel matches routes top-down; a greedy catch-all swallows everything. Place catch-all routes last and use where constraints to limit their scope.

  • Validation in controllers — calling $request->validate() with inline rules in every controller method instead of using Form Request classes. Inline validation cannot be reused across endpoints, is harder to test in isolation, and clutters controllers with input-handling logic that belongs in a dedicated class.

  • Middleware that does everything — a single middleware that checks authentication, verifies email, enforces subscription status, and logs access. When any one concern changes, the entire middleware must be modified and retested. Split into focused middleware and compose them in route groups.

  • Hardcoded URLs — using string URLs like redirect('/dashboard') or href="/posts/{{ $id }}" instead of named routes with route('dashboard') or route('posts.show', $post). Hardcoded URLs break silently when routes change and require manual updates across every view and controller.

  • Fat controllers — controllers with 50+ line methods that handle validation, authorization, business logic, persistence, and response formatting. Controllers should validate (via Form Requests), authorize (via Policies), delegate to services, and return responses. When a controller method grows beyond 10-15 lines, it is likely doing too much.

Install this skill directly: skilldb add php-laravel-skills

Get CLI access →