Route::group
Applies shared attributes such as middleware, prefixes and names to a set of routes.
group(array $attributes, Closure|string $routes): voidParameters
| Parameter | Type | Description |
|---|---|---|
$$attributes | array | Shared attributes for every route in the group: middleware, prefix, as (name prefix), domain, controller, namespace, where, and the resource-style options. |
$$routes | Closure|string | A closure containing the route definitions, or the path to a route file to load — for example base_path('routes/webhooks.php'). |
Return value
voidNothing. The routes defined inside the closure or file are registered with the group's attributes merged in.
Errors
Nothing. Route definitions inside the closure are evaluated immediately, so a fatal error inside it surfaces at boot.
Route::group shares attributes across many routes so you do not repeat them on each definition. The fluent form — Route::middleware('auth')->prefix('admin')->name('admin.')->group(fn () => ...) — is what almost all Laravel code uses; the array form above is what the fluent builders ultimately call.
The merge rules
These are the part worth memorising. Quoting the docs: middleware and where conditions are merged while names and prefixes are appended. So nesting a group named reports. inside one named admin. gives admin.reports., and both groups' middleware apply. Namespace delimiters and URI slashes are inserted automatically; name segments are not, which is why name prefixes end in a dot.
What trips people up
- The closure runs at boot, every request. Anything expensive inside it — a database query to build routes — runs on every request that is not served from the route cache.
- A group cannot un-apply middleware except with
withoutMiddleware(), which only removes middleware the group itself added. - Subdomain groups must be registered before root-domain routes with the same path, or the root routes win.
- Passing a file path instead of a closure is how
bootstrap/app.phploads extra route files.
Example
<?php
use App\Http\Controllers\Admin\ReportController;
use Illuminate\Support\Facades\Route;
Route::middleware(['auth', 'can:access-admin'])
->prefix('admin')
->name('admin.')
->group(function () {
Route::get('/', [DashboardController::class, 'index'])->name('dashboard');
// Nested: middleware merges, prefix and name append.
Route::prefix('reports')->name('reports.')->group(function () {
Route::get('/{year}', [ReportController::class, 'show'])
->whereNumber('year')
->name('show');
});
});
echo route('admin.dashboard');
echo PHP_EOL;
echo route('admin.reports.show', ['year' => 2026]);
// http://example.com/admin
// http://example.com/admin/reports/2026When to use it
- Put every admin screen behind auth and a role middleware with one wrapper.
- Version an API by prefixing a group with v1 and naming it v1..
- Load routes/webhooks.php with its own middleware and name prefix from bootstrap/app.php.
- Capture a tenant subdomain as a route parameter available to every route in the group.
Frequently asked questions
How do nested route groups combine their attributes?
Middleware and where constraints are merged, while name prefixes and URI prefixes are appended in order. Namespace separators and URI slashes are inserted for you; the dot in a name prefix is not.
What is the array key for a group's route name prefix?
It is 'as', not 'name' — Route::group(['as' => 'admin.'], ...). The fluent equivalent Route::name('admin.') and Route::as('admin.') both do the same thing.
Can a route group load an external route file?
Yes. Pass a path instead of a closure: Route::middleware('api')->prefix('webhooks')->group(base_path('routes/webhooks.php')). This is how bootstrap/app.php registers additional route files.
More examples
The array form, and loading a route file
The array keys differ from the fluent method names in one place that costs people time: the name prefix is 'as', not 'name'. The second half shows the string form of the second argument — every route defined in routes/webhooks.php inherits the api middleware, the webhooks prefix and the webhooks. name prefix.
<?php
use Illuminate\Support\Facades\Route;
// The array form: 'as' is the name prefix, not 'name'.
Route::group([
'middleware' => ['auth', 'verified'],
'prefix' => 'account',
'as' => 'account.',
], function () {
Route::get('/billing', [BillingController::class, 'show'])->name('billing');
});
// bootstrap/app.php — a group can load a whole file instead of a closure.
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
then: function () {
Route::middleware('api')
->prefix('webhooks')
->name('webhooks.')
->group(base_path('routes/webhooks.php'));
},
)
echo route('account.billing');
// http://example.com/account/billingSubdomain group with a captured parameter
The subdomain placeholder becomes the first argument to the closure, ahead of the URI parameters, and route() needs a value for it when generating the URL. Registering the root-domain /user/{id} first would shadow the tenant route, since the router stops at the first match.
<?php
use Illuminate\Support\Facades\Route;
// Register subdomain routes BEFORE root-domain routes with the same path.
Route::domain('{account}.example.com')->group(function () {
Route::get('/user/{id}', function (string $account, string $id) {
return $account.':'.$id;
})->name('tenant.user');
});
Route::get('/user/{id}', [UserController::class, 'show'])->name('user.show');
echo route('tenant.user', ['account' => 'acme', 'id' => 4]);
// http://acme.example.com/user/4
Discussion