0

Route groups — middleware, prefix, name prefix, domain

Intermediate5 min read·lv-06-004
interview

Concept

Route groups are the primary tool for applying shared configuration — middleware, URI prefixes, name prefixes, domain constraints, and namespace hints — to a set of routes without repeating those options on every individual registration. Route::group(array $attributes, Closure $routes) merges the attribute array onto a stack; nested groups inherit and merge from their parent automatically.

When you call Route::middleware('auth'), Route::prefix('/admin'), or Route::name('admin.') and then chain ->group(...), Laravel pushes the current group context onto Router::$groupStack. Every route registered inside the closure reads the top of the stack and merges it into its own attribute set. Middleware arrays are merged (parent and child middleware both apply). URI prefixes are concatenated. Name prefixes are concatenated. This makes arbitrarily deep nesting work correctly.

Middleware groups are a related but distinct concept. In app/Http/Kernel.php you define named bundles under $middlewareGroups'web' and 'api' are the defaults. Route::middleware('web') applies the entire bundle as one logical unit. Individual middleware and group middleware can be combined: ->middleware(['web', 'throttle:60,1']).

Domain routing is a first-class group feature: Route::domain('{account}.example.com')->group(...) creates a tenant-aware group where {account} is a route parameter available to every route inside. This is how SaaS applications implement subdomain-based multi-tenancy without any middleware hacks.

A practical gotcha: ->prefix() does not automatically add a leading slash. Laravel normalizes the URI, but double-slashes in deeply nested groups can cause subtle mismatches when route:cache is used. Always keep prefixes without a leading slash when they are nested, and always start the outermost prefix with / or omit it entirely (Laravel adds it). Testing your route list with php artisan route:list after any group restructure is a mandatory habit.

Code Example

php
<?php

use App\Http\Controllers\Admin;
use App\Http\Controllers\Api;
use Illuminate\Support\Facades\Route;

// Basic middleware group
Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
    Route::get('/profile', [ProfileController::class, 'show'])->name('profile.show');
});

// Prefix + middleware + name prefix combined
Route::prefix('/admin')
    ->middleware(['auth', 'role:admin'])
    ->name('admin.')
    ->group(function () {
        Route::get('/users', [Admin\UserController::class, 'index'])->name('users.index');
        // URI: /admin/users   Name: admin.users.index

        Route::get('/users/{id}', [Admin\UserController::class, 'show'])->name('users.show');
        // URI: /admin/users/{id}   Name: admin.users.show

        // Nested group — adds further prefix and middleware
        Route::prefix('/settings')
            ->middleware('superadmin')
            ->name('settings.')
            ->group(function () {
                Route::get('/', [Admin\SettingsController::class, 'index'])->name('index');
                // URI: /admin/settings   Name: admin.settings.index
            });
    });

// Domain routing for multi-tenant SaaS
Route::domain('{account}.example.com')
    ->middleware('tenant')
    ->group(function () {
        Route::get('/', [TenantController::class, 'dashboard']);
        Route::get('/users', [TenantController::class, 'users']);
    });

// API versioning with prefix groups
Route::prefix('/api')
    ->middleware('api')
    ->name('api.')
    ->group(function () {
        Route::prefix('/v1')->name('v1.')->group(function () {
            Route::get('/users', [Api\V1\UserController::class, 'index'])->name('users.index');
        });

        Route::prefix('/v2')->name('v2.')->group(function () {
            Route::get('/users', [Api\V2\UserController::class, 'index'])->name('users.index');
        });
    });

Interview Q&A

Q: How does Laravel merge middleware when route groups are nested three levels deep — does the innermost middleware override the outer, or do all levels apply?

All levels apply. Router::mergeWithLastGroup() retrieves the top frame of $groupStack and merges its middleware array with the current group's middleware array by concatenating them (array union, not replacement). The resulting Route object accumulates middleware from every ancestor group plus its own. When the middleware pipeline runs at dispatch time, duplicates are resolved by Route::gatherMiddleware() — if the same middleware string appears more than once, it runs only once. This means you can safely re-apply auth at multiple group levels without double-execution.


Q: What is the difference between defining middleware in a route group versus registering it in the $middlewareGroups array in Kernel.php?

$middlewareGroups in Kernel.php defines a named bundle — 'web' or 'api' — that can be referenced by a single string alias wherever middleware is listed. Route groups using ->middleware('web') reference that bundle; the kernel resolves it to the actual class list at dispatch time. Inline middleware on a route group (->middleware([\App\Http\Middleware\MyMiddleware::class])) are class references applied directly. Both end up in the route's middleware array; the distinction is organizational — kernel groups are global reusable bundles, inline arrays are route-specific stacks.


Q: How do route group name prefixes interact with Route::resource() inside a group?

Route::resource() generates seven named routes following the {resource}.{action} pattern. When inside a group with ->name('admin.'), each generated name is prefixed: admin.users.index, admin.users.create, admin.users.store, and so on. The prefix is prepended by Router::mergeWithLastGroup() before the resource routes are registered. You can further customize or exclude specific resource actions using ->only([...]) or ->except([...]) chained on the resource call, and all generated names respect the group prefix correctly.