Laravel Route Organization: Split Routes by Role, Not Method

Laravel Route Organization: Split Routes by Role, Not Method

Reading Time: 3 minutes

Series: Every Laravel Project Should Have These Building Blocks 
Part: 7 of 35 | Level: Beginnerโ€“Intermediate | Prerequisites: Thin Controllers


What You’ll Learn

  • Why a single web.php breaks 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.phpapi.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.phpmerchandiser.phpemployee.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.php instead 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() to routes/web.php for 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.index is stable even if the URL path changes from /admin/orders to /dashboard/orders. Frontend devs using Ziggy or route() helpers are shielded from URL refactors.

Further Reading


โ† Form Requests | Next: Service Classes โ†’

Laravel Route Organization: Split Routes by Role, Not Method

Oh hi there ๐Ÿ‘‹
Itโ€™s nice to meet you.

Sign up to receive awesome content in your inbox.

We donโ€™t spam! Read our privacy policy for more info.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

2 Comments