Laravel Route Organization: Split Routes by Role, Not Method
Series: Every Laravel Project Should Have These Building Blocks
Part: 7 of 35 | Level: BeginnerโIntermediate | Prerequisites: Thin Controllers
Table of Contents
What You’ll Learn
- Why a single
web.phpbreaks down at scale - How to split routes by role and domain
- Loading additional route files cleanly
- URL and route name conventions
- API versioning with separate route files
The Problem with One Route File
Every Laravel project starts with three route files: web.php, api.php, and console.php. For a while this is fine. Then you add an admin panel. Then a separate merchant portal. Then a mobile API. Then a set of webhook endpoints.
Six months in, web.php has 400 lines, no one can find anything, and route name collisions are a daily debugging session.
The fix is to split routes by the audience or domain they serve – not arbitrarily, but by natural boundaries that already exist in your system.
The Split Used Across These Projects
Here’s the route structure from the most organized projects:
routes/
โโโ web.php # Public-facing pages (guests + authenticated users)
โโโ admin.php # Admin panel (/admin/*)
โโโ api.php # REST API (v1)
โโโ api_v2.php # REST API (v2) โ when versioning is needed
โโโ merchandiser.php # Merchandiser portal
โโโ employee.php # Employee portal
โโโ qr.php # QR code scanning endpoints
โโโ console.php # Artisan commands (schedule)
Each file is small, focused, and named after who uses it.
Loading Route Files
In Laravel 11+, all route files are registered in bootstrap/app.php:
// bootstrap/app.php
->withRouting(
web: __DIR__ . '/../routes/web.php',
api: __DIR__ . '/../routes/api.php',
commands: __DIR__ . '/../routes/console.php',
health: '/up',
then: function (): void {
Route::middleware('web')
->group(base_path('routes/admin.php'));
Route::middleware(['web', 'auth:merchandiser'])
->prefix('merchandiser')
->name('merchandiser.')
->group(base_path('routes/merchandiser.php'));
Route::middleware(['web', 'auth:employee'])
->prefix('employee')
->name('employee.')
->group(base_path('routes/employee.php'));
},
)
In Laravel 10 and below, use RouteServiceProvider:
// app/Providers/RouteServiceProvider.php
public function boot(): void
{
$this->routes(function (): void {
Route::middleware('web')
->group(base_path('routes/web.php'));
Route::middleware('api')
->prefix('api')
->name('api.')
->group(base_path('routes/api.php'));
Route::middleware('web')
->prefix('admin')
->name('admin.')
->group(base_path('routes/admin.php'));
Route::middleware(['web', 'auth:merchandiser'])
->prefix('merchandiser')
->name('merchandiser.')
->group(base_path('routes/merchandiser.php'));
});
}
What Goes in Each File
web.php – Public and authenticated user routes
// routes/web.php
Route::get('/', [HomeController::class, 'index'])->name('home');
Route::get('/about', [PagesController::class, 'about'])->name('about');
Route::middleware('guest')->group(function (): void {
Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
Route::post('/login', [AuthController::class, 'login']);
Route::get('/register', [AuthController::class, 'showRegister'])->name('register');
Route::post('/register', [AuthController::class, 'register']);
});
Route::middleware('auth')->group(function (): void {
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::resource('orders', OrdersController::class);
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
});
admin.php – Admin panel
// routes/admin.php
// Note: this file is already wrapped in ->prefix('admin')->name('admin.')
// so no need to repeat it here
Route::middleware('guest:admin')->group(function (): void {
Route::get('/login', [AdminAuthController::class, 'showLogin'])->name('login');
Route::post('/login', [AdminAuthController::class, 'login']);
});
Route::middleware('auth:admin')->group(function (): void {
Route::get('/', [AdminDashboardController::class, 'index'])->name('dashboard');
Route::resource('users', AdminUsersController::class);
Route::resource('orders', AdminOrdersController::class);
Route::post('/logout', [AdminAuthController::class, 'logout'])->name('logout');
});
api.php – REST API
// routes/api.php
// Already wrapped with ->prefix('api')->middleware('api') by Laravel
Route::prefix('v1')->name('api.v1.')->group(function (): void {
Route::post('/auth/login', [ApiAuthController::class, 'login'])->name('auth.login');
Route::middleware('auth:sanctum')->group(function (): void {
Route::get('/me', [ProfileController::class, 'show'])->name('profile.show');
Route::apiResource('products', ProductsController::class);
Route::apiResource('orders', OrdersController::class);
});
});
Naming Conventions
Consistent naming makes route() calls predictable throughout the codebase.
URL paths: kebab-case
/service-requests โ
/serviceRequests โ
/service_requests โ
Route names: camelCase, dot-separated hierarchy
Route::get('/service-requests', ...)->name('serviceRequests.index');
Route::post('/service-requests', ...)->name('serviceRequests.store');
Route::get('/service-requests/{id}', ...)->name('serviceRequests.show');
Route::put('/service-requests/{id}', ...)->name('serviceRequests.update');
Route::delete('/service-requests/{id}', ...)->name('serviceRequests.destroy');
Route parameters: camelCase
Route::get('/orders/{orderId}', ...);
Route::get('/users/{userId}/orders/{orderId}', ...);
Admin routes with prefix:
// With prefix 'admin.' already applied:
'admin.dashboard' // โ /admin
'admin.users.index' // โ /admin/users
'admin.orders.show' // โ /admin/orders/{id}
API Versioning
When a breaking API change is needed, add a second file rather than breaking existing clients:
// routes/api_v2.php
Route::prefix('v2')->name('api.v2.')->group(function (): void {
Route::middleware('auth:sanctum')->group(function (): void {
Route::apiResource('products', V2\ProductsController::class);
// New response format for products in v2
});
});
Load it alongside api.php:
->withRouting(
then: function (): void {
Route::middleware('api')
->group(base_path('routes/api_v2.php'));
}
)
Both v1 and v2 coexist. When all clients are on v2, deprecate v1 with a warning middleware, then remove.
Route Caching
In production, cache routes for a significant performance boost:
php artisan route:cache
Requirements for route caching:
- No closures in route files (they can’t be serialized – always use controller classes)
- Run after every deploy:
php artisan route:cache
Inspect the full route list at any time:
php artisan route:list --except-vendor
php artisan route:list --path=admin
php artisan route:list --method=POST
FAQs
How do I split routes into multiple files in Laravel?
Create routes/admin.php etc. and load them in bootstrap/app.php (Laravel 11+) or a service provider with Route::middleware(‘web’)->group(base_path(‘routes/admin.php’)).
Should every route group have a name prefix?
Yes, ->name(‘admin.’) makes route(‘admin.dashboard’) unambiguous and lets you audit permissions by prefix.
Key Takeaways
- Split route files by the audience they serve:
admin.php,merchandiser.php,employee.php. - Apply middleware and prefix at the loader level – route files themselves should be clean groups.
- URL paths are kebab-case. Route names are camelCase, dot-separated.
- API versioning: add
api_v2.phpinstead of breaking existing routes. - No closures in route files – always reference controller classes so routes can be cached.
Tips and Gotchas
โ ๏ธ Warning: Avoid adding
Route::resource()toroutes/web.phpfor admin routes. When all routes share one file, you lose the ability to apply different middleware groups cleanly. Split the files, then apply middleware at the group level.
๐ก Tip: Use
->only(['index', 'store', 'show'])or->except(['destroy'])on resource routes. Registering all 7 RESTful routes and only implementing 3 of them creates dead routes that could be exploited if you forget to add authorization to the missing ones.
๐ฅ Expert Note: Named routes are your API contract with the frontend. The name
admin.orders.indexis stable even if the URL path changes from/admin/ordersto/dashboard/orders. Frontend devs using Ziggy orroute()helpers are shielded from URL refactors.
Further Reading
- Laravel Docs: Routing
- Laravel Docs: Route Groups
- Ziggy โ use Laravel named routes in JavaScript
- Laravel Docs:
withRouting()in Laravel 11+
2 Comments