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.

Laravel Nested Route Groups: Organizing Complex Routing Structures - Ferre Mekelenkamp