Laravel Nested Route Groups: Organizing Complex Routing Structures
As Laravel applications grow in complexity, organizing routes becomes increasingly important. Nested route groups provide a powerful way to structure your routing with layered configurations, making your routes more maintainable and logical.
Understanding Nested Route Groups
Nested route groups allow you to apply different configurations to subsets of routes within a parent group. This creates a hierarchical structure where inner groups inherit and extend the configuration of outer groups.
Basic Nested Structure
Route::group(['prefix' => 'account', 'as' => 'account.'], function() {
// Public account routes
Route::get('login', [AccountController::class, 'login'])->name('login');
Route::get('register', [AccountController::class, 'register'])->name('register');
Route::post('password/reset', [AccountController::class, 'resetPassword'])->name('password.reset');
// Protected account routes (nested group)
Route::group(['middleware' => 'auth'], function() {
Route::get('dashboard', [AccountController::class, 'dashboard'])->name('dashboard');
Route::get('edit', [AccountController::class, 'edit'])->name('edit');
Route::put('update', [AccountController::class, 'update'])->name('update');
Route::delete('delete', [AccountController::class, 'destroy'])->name('delete');
});
});
In this example:
- All routes have the
account.
name prefix and/account
URL prefix - Only the inner group routes require authentication
- The resulting URLs:
/account/login
,/account/dashboard
, etc. - The resulting route names:
account.login
,account.dashboard
, etc.
Advanced Nested Routing Patterns
Multi-Level Administration Panel
Route::group(['prefix' => 'admin', 'as' => 'admin.', 'middleware' => 'auth'], function() {
// Basic admin routes
Route::get('dashboard', [AdminController::class, 'dashboard'])->name('dashboard');
// User management (requires admin role)
Route::group(['prefix' => 'users', 'as' => 'users.', 'middleware' => 'role:admin'], function() {
Route::get('/', [UserController::class, 'index'])->name('index');
Route::get('create', [UserController::class, 'create'])->name('create');
Route::post('/', [UserController::class, 'store'])->name('store');
// Individual user management (requires specific permissions)
Route::group(['prefix' => '{user}', 'middleware' => 'can:manage,user'], function() {
Route::get('/', [UserController::class, 'show'])->name('show');
Route::get('edit', [UserController::class, 'edit'])->name('edit');
Route::put('/', [UserController::class, 'update'])->name('update');
// Sensitive user operations (requires super admin)
Route::group(['middleware' => 'role:super-admin'], function() {
Route::delete('/', [UserController::class, 'destroy'])->name('destroy');
Route::post('impersonate', [UserController::class, 'impersonate'])->name('impersonate');
Route::post('reset-password', [UserController::class, 'resetPassword'])->name('reset-password');
});
});
});
// Content management
Route::group(['prefix' => 'content', 'as' => 'content.', 'middleware' => 'role:editor'], function() {
Route::resource('posts', PostController::class);
Route::resource('categories', CategoryController::class);
// Draft management (different middleware)
Route::group(['prefix' => 'drafts', 'as' => 'drafts.'], function() {
Route::get('/', [DraftController::class, 'index'])->name('index');
Route::get('{post}/preview', [DraftController::class, 'preview'])->name('preview');
Route::post('{post}/publish', [DraftController::class, 'publish'])->name('publish');
});
});
});
API Versioning with Nested Groups
Route::group(['prefix' => 'api'], function() {
// Version 1 API
Route::group(['prefix' => 'v1', 'as' => 'api.v1.'], function() {
// Public API endpoints
Route::group(['middleware' => 'api.throttle:60,1'], function() {
Route::get('posts', [Api\V1\PostController::class, 'index'])->name('posts.index');
Route::get('posts/{post}', [Api\V1\PostController::class, 'show'])->name('posts.show');
});
// Authenticated API endpoints
Route::group(['middleware' => ['api.throttle:120,1', 'auth:sanctum']], function() {
Route::post('posts', [Api\V1\PostController::class, 'store'])->name('posts.store');
Route::put('posts/{post}', [Api\V1\PostController::class, 'update'])->name('posts.update');
// Premium API endpoints (higher rate limit)
Route::group(['middleware' => 'subscription:premium'], function() {
Route::get('analytics', [Api\V1\AnalyticsController::class, 'index'])->name('analytics.index');
Route::get('exports', [Api\V1\ExportController::class, 'index'])->name('exports.index');
Route::post('exports', [Api\V1\ExportController::class, 'create'])->name('exports.create');
});
});
});
// Version 2 API (with different structure)
Route::group(['prefix' => 'v2', 'as' => 'api.v2.', 'middleware' => 'api.v2.headers'], function() {
Route::group(['middleware' => 'api.throttle:100,1'], function() {
Route::apiResource('posts', Api\V2\PostController::class);
Route::apiResource('users', Api\V2\UserController::class);
});
});
});
Multi-Tenant Application Routing
Route::group(['domain' => '{tenant}.myapp.com', 'middleware' => 'tenant'], function() {
// Public tenant routes
Route::get('/', [TenantController::class, 'home'])->name('tenant.home');
Route::get('about', [TenantController::class, 'about'])->name('tenant.about');
// Tenant authentication
Route::group(['prefix' => 'auth', 'as' => 'tenant.auth.'], function() {
Route::get('login', [TenantAuthController::class, 'showLogin'])->name('login');
Route::post('login', [TenantAuthController::class, 'login'])->name('login.post');
Route::post('logout', [TenantAuthController::class, 'logout'])->name('logout');
});
// Tenant user area
Route::group(['middleware' => 'auth:tenant', 'prefix' => 'app', 'as' => 'tenant.app.'], function() {
Route::get('dashboard', [TenantDashboardController::class, 'index'])->name('dashboard');
// Tenant admin routes
Route::group(['middleware' => 'tenant.admin', 'prefix' => 'admin', 'as' => 'admin.'], function() {
Route::get('settings', [TenantAdminController::class, 'settings'])->name('settings');
Route::get('users', [TenantAdminController::class, 'users'])->name('users');
// Billing (requires billing permission)
Route::group(['middleware' => 'can:manage-billing', 'prefix' => 'billing', 'as' => 'billing.'], function() {
Route::get('/', [TenantBillingController::class, 'index'])->name('index');
Route::get('invoices', [TenantBillingController::class, 'invoices'])->name('invoices');
Route::post('upgrade', [TenantBillingController::class, 'upgrade'])->name('upgrade');
});
});
});
});
E-commerce Application Example
Route::group(['as' => 'shop.'], function() {
// Public shop routes
Route::get('/', [ShopController::class, 'index'])->name('index');
Route::get('products', [ProductController::class, 'index'])->name('products.index');
Route::get('products/{product}', [ProductController::class, 'show'])->name('products.show');
// Cart functionality (session-based)
Route::group(['prefix' => 'cart', 'as' => 'cart.', 'middleware' => 'cart.session'], function() {
Route::get('/', [CartController::class, 'index'])->name('index');
Route::post('add/{product}', [CartController::class, 'add'])->name('add');
Route::put('update/{item}', [CartController::class, 'update'])->name('update');
Route::delete('remove/{item}', [CartController::class, 'remove'])->name('remove');
});
// Checkout process
Route::group(['prefix' => 'checkout', 'as' => 'checkout.', 'middleware' => 'cart.not.empty'], function() {
Route::get('/', [CheckoutController::class, 'index'])->name('index');
Route::post('shipping', [CheckoutController::class, 'shipping'])->name('shipping');
Route::post('payment', [CheckoutController::class, 'payment'])->name('payment');
// Authenticated checkout steps
Route::group(['middleware' => 'auth'], function() {
Route::get('review', [CheckoutController::class, 'review'])->name('review');
Route::post('complete', [CheckoutController::class, 'complete'])->name('complete');
});
// Guest checkout
Route::group(['prefix' => 'guest', 'as' => 'guest.'], function() {
Route::get('info', [GuestCheckoutController::class, 'info'])->name('info');
Route::post('info', [GuestCheckoutController::class, 'storeInfo'])->name('info.store');
Route::post('complete', [GuestCheckoutController::class, 'complete'])->name('complete');
});
});
// Customer account area
Route::group(['prefix' => 'account', 'as' => 'account.', 'middleware' => 'auth'], function() {
Route::get('/', [AccountController::class, 'dashboard'])->name('dashboard');
Route::get('profile', [AccountController::class, 'profile'])->name('profile');
// Order management
Route::group(['prefix' => 'orders', 'as' => 'orders.'], function() {
Route::get('/', [OrderController::class, 'index'])->name('index');
Route::get('{order}', [OrderController::class, 'show'])->name('show');
// Order actions (different permissions)
Route::group(['prefix' => '{order}'], function() {
Route::post('cancel', [OrderController::class, 'cancel'])
->name('cancel')
->middleware('can:cancel,order');
Route::post('return', [OrderController::class, 'return'])
->name('return')
->middleware('can:return,order');
});
});
// Wishlist
Route::group(['prefix' => 'wishlist', 'as' => 'wishlist.'], function() {
Route::get('/', [WishlistController::class, 'index'])->name('index');
Route::post('add/{product}', [WishlistController::class, 'add'])->name('add');
Route::delete('remove/{product}', [WishlistController::class, 'remove'])->name('remove');
});
});
});
Route Caching with Nested Groups
When using route caching, nested groups work seamlessly:
// Generate route cache
php artisan route:cache
// Clear route cache
php artisan route:clear
Nested route groups are fully supported by Laravel's route caching system, so you don't need to modify your structure for production optimization.
Debugging Nested Routes
List All Routes
# View all routes with their names and URIs
php artisan route:list
# Filter routes by name pattern
php artisan route:list --name=admin
# Filter routes by URI pattern
php artisan route:list --path=api/v1
Route Debugging Helper
// Create a route to debug your nested structure
Route::get('/debug/routes', function() {
$routes = collect(Route::getRoutes())->map(function ($route) {
return [
'method' => implode('|', $route->methods()),
'uri' => $route->uri(),
'name' => $route->getName(),
'action' => $route->getActionName(),
'middleware' => $route->gatherMiddleware(),
];
});
return response()->json($routes);
})->middleware('can:debug-routes');
Best Practices for Nested Route Groups
1. Keep Nesting Reasonable
// ✅ Good: 2-3 levels maximum
Route::group(['prefix' => 'admin'], function() {
Route::group(['prefix' => 'users'], function() {
Route::group(['middleware' => 'can:manage-users'], function() {
// Routes here
});
});
});
// ❌ Avoid: Too deeply nested
Route::group([], function() {
Route::group([], function() {
Route::group([], function() {
Route::group([], function() {
Route::group([], function() {
// This is hard to follow
});
});
});
});
});
2. Use Descriptive Names
Route::group(['prefix' => 'admin', 'as' => 'admin.', 'middleware' => 'auth'], function() {
Route::group(['prefix' => 'content', 'as' => 'content.', 'middleware' => 'role:editor'], function() {
Route::group(['prefix' => 'posts', 'as' => 'posts.'], function() {
Route::get('drafts', [PostController::class, 'drafts'])->name('drafts');
// Route name: admin.content.posts.drafts
// Route URL: /admin/content/posts/drafts
});
});
});
3. Extract Complex Groups to Route Files
// routes/admin.php
Route::group(['prefix' => 'admin', 'as' => 'admin.', 'middleware' => ['auth', 'admin']], function() {
require base_path('routes/admin/users.php');
require base_path('routes/admin/content.php');
require base_path('routes/admin/settings.php');
});
// routes/admin/users.php
Route::group(['prefix' => 'users', 'as' => 'users.'], function() {
// User management routes
});
4. Document Complex Structures
/**
* Admin Routes Structure:
*
* /admin (auth + admin role required)
* ├── /users (admin.users.*)
* │ ├── / (index)
* │ ├── /create (create)
* │ └── /{user} (show, edit, delete - requires user management permission)
* ├── /content (admin.content.* + editor role)
* │ ├── /posts (CRUD operations)
* │ └── /drafts (draft management)
* └── /settings (admin.settings.* + super-admin role)
*/
Route::group(['prefix' => 'admin', 'as' => 'admin.', 'middleware' => ['auth', 'role:admin']], function() {
// Implementation
});
Testing Nested Route Groups
// tests/Feature/NestedRoutesTest.php
class NestedRoutesTest extends TestCase
{
public function test_nested_route_middleware_applied_correctly()
{
$user = User::factory()->create();
// Should require authentication
$this->get(route('admin.users.index'))
->assertRedirect(route('login'));
// Should work with authentication
$this->actingAs($user)
->get(route('admin.users.index'))
->assertOk();
}
public function test_nested_route_names_generated_correctly()
{
$this->assertTrue(Route::has('admin.users.index'));
$this->assertTrue(Route::has('admin.users.show'));
$this->assertTrue(Route::has('admin.content.posts.drafts'));
}
public function test_nested_route_urls_generated_correctly()
{
$user = User::factory()->create();
$this->assertEquals('/admin/users', route('admin.users.index'));
$this->assertEquals("/admin/users/{$user->id}", route('admin.users.show', $user));
$this->assertEquals('/admin/content/posts/drafts', route('admin.content.posts.drafts'));
}
}
Conclusion
Nested route groups provide a powerful way to organize complex Laravel applications with layered configurations. They enable you to:
- Apply progressive middleware as users navigate deeper into your application
- Organize routes logically with consistent naming and URL structures
- Maintain clean code by grouping related functionality
- Scale applications without routing becoming unwieldy
When used thoughtfully, nested route groups make your Laravel applications more maintainable, secure, and easier to understand. Remember to keep nesting reasonable, use descriptive names, and document complex structures for your team.