How to Structure Routes in Large Laravel Projects?

Imagine a Laravel project with 100+ routes, separate section for guest, users, administrators etc. Do you really want to keep it all in one file? How can you group them, add prefixes to URLs? Let's see what options we have.


1. Separate WEB and API Routes

This one is easy, as Laravel is shipped with this feature by default. There are two files:

So if your project has both visual web-pages, and API (which is more and more common these days), please put API routes in that separate file.

For example, if you have /users page and then /api/users/ endpoint, separating them into their own files help to not get confused with the same names in the same file.

That said, I recently saw counter-intuitive example from official Laravel project. With Laravel Horizon, Taylor has only API routes, and he didn't use separate file, instead he put it into routes/web.php:

Another proof that structuring in Laravel is very personal and there is no 100% standard, even from Taylor himself.


2. Structure routes/web.php File into Groups

That also comes from "basic" Laravel - route grouping. This is an example from the official Laravel documentation:

Route::middleware(['first', 'second'])->group(function () {
    Route::get('/', function () {
        // Uses first & second Middleware
    });

    Route::get('user/profile', function () {
        // Uses first & second Middleware
    });
});

The most basic usage is hiding different groups under different middleware. For example, you want one group to be restricted by default auth middleware, another group by separate admin custom middleware etc.

With that, you can also use Route group names and prefixes. Again, a few examples from the official documentation:

Route::prefix('admin')->group(function () {
    Route::get('users', function () {
        // Matches The "/admin/users" URL
    });
});

Route::name('admin.')->group(function () {
    Route::get('users', function () {
        // Route assigned name "admin.users"...
    })->name('users');
});

Also, if you want to add all middleware+name+prefix to one group, it's more readable to put them into an array:

// Instead of chaining like this:
Route::name('admin.')->prefix('admin')->middleware('admin')->group(function () {
    // ...
});

// You can use an array
Route::group([
    'name' => 'admin.',
    'prefix' => 'admin',
    'middleware' => 'auth'
], function () {
    // ...
});

Let's tie it all together into a real-life example and three groups:

  • "Guest" group with /front/XXXXX URLs and no middleware;
  • "User" group with /user/XXXXX URLs and auth middleware;
  • "Admin" group with /admin/XXXXX URLs and custom admin middleware.

Here's a way to group it all in routes/web.php file:

Route::group([
    'name' => 'admin.',
    'prefix' => 'admin',
    'middleware' => 'admin'
], function () {

    // URL: /admin/users
    // Route name: admin.users
    Route::get('users', function () {
        return 'Admin: user list';
    })->name('users');

});

Route::group([
    'name' => 'user.',
    'prefix' => 'user',
    'middleware' => 'auth'
], function () {

    // URL: /user/profile
    // Route name: user.profile
    Route::get('profile', function () {
        return 'User profile';
    })->name('profile');

});

Route::group([
    'name' => 'front.',
    'prefix' => 'front'
], function () {

    // No middleware here
    // URL: /front/about-us
    // Route name: front.about
    Route::get('about-us', function () {
        return 'About us page';
    })->name('about');

});

3. Grouping Controllers with Namespaces

In the example above, we didn't use Controllers, we just returned static text as an example. Let's add Controllers, with one more "twist" - we will structure them to the folders with their own different namespaces, like this:

And then we can use them in our Routes file:

Route::group([
    'name' => 'front.',
    'prefix' => 'front'
], function () {
    Route::get('about-us', 'Front\AboutController@index')->name('about');
});

But what if we have a lot of controllers in that group? Should we keep adding Front\SomeController all the time? Of course not. You can specify the namespace as one of the parameters, too.

Route::group([
    'name' => 'front.',
    'prefix' => 'front',
    'namespace' => 'Front',
], function () {
    Route::get('about-us', 'AboutController@index')->name('about');
    Route::get('contact', 'ContactController@index')->name('contact');
});

4. Group within a Group

The situation above, with three groups, is simplified, real projects have a little different structure - of two groups: front and auth. And then inside of auth there are sub-groups: user and admin. For that, we can create sub-groups in routes/web.php and assign different middlewares/prefixes etc.

Route::group([
    'middleware' => 'auth',
], function() {

    Route::group([
        'name' => 'admin.',
        'prefix' => 'admin',
        'middleware' => 'admin'
    ], function () {

        // URL: /admin/users
        // Route name: admin.users
        Route::get('users', 'UserController@index')->name('users');

    });

    Route::group([
        'name' => 'user.',
        'prefix' => 'user',
    ], function () {

        // URL: /user/profile
        // Route name: user.profile
        Route::get('profile', 'ProfileController@index')->name('profile');

    });

});

We can do it even with more than two levels, here's an example from open-source project Akaunting:

Route::group(['middleware' => 'language'], function () {
    Route::group(['middleware' => 'auth'], function () {
        Route::group(['prefix' => 'uploads'], function () {
            Route::get('{id}', 'Common\Uploads@get');
            Route::get('{id}/show', 'Common\Uploads@show');
            Route::get('{id}/download', 'Common\Uploads@download');
        });

        Route::group(['middleware' => 'permission:read-admin-panel'], function () {
            Route::group(['prefix' => 'wizard'], function () {
                Route::get('/', 'Wizard\Companies@edit')->name('wizard.index');

        // ...

Another example is from another popular Laravel CRM called Monica:

Route::middleware(['auth', 'verified', 'mfa'])->group(function () {
    Route::name('dashboard.')->group(function () {
        Route::get('/dashboard', 'DashboardController@index')->name('index');
        Route::get('/dashboard/calls', 'DashboardController@calls');
        Route::get('/dashboard/notes', 'DashboardController@notes');
        Route::get('/dashboard/debts', 'DashboardController@debts');
        Route::get('/dashboard/tasks', 'DashboardController@tasks');
        Route::post('/dashboard/setTab', 'DashboardController@setTab');
    });

5. Global Settings in RouteServiceProvider

There is a file which serves for all routes settings - app/Providers/RouteServiceProvider.php. It has method map() where it binds both routes files - web and API:

    public function map()
    {
        $this->mapApiRoutes();
        $this->mapWebRoutes();
    }

    protected function mapWebRoutes()
    {
        Route::middleware('web')
             ->namespace($this->namespace)
             ->group(base_path('routes/web.php'));
    }

    protected function mapApiRoutes()
    {
        Route::prefix('api')
             ->middleware('api')
             ->namespace($this->namespace)
             ->group(base_path('routes/api.php'));
    }

Have you noticed middleware, namespace and prefix being mentioned in the methods? That's where you can set the global settings for the whole file, so you wouldn't have to repeat them for every Route group inside the file.

It's mostly used for API routes, as their settings are usually the same, like this:

protected function mapApiRoutes()
{
    Route::group([
        'middleware' => ['api'],
        'namespace' => $this->namespace,
        'prefix' => 'api/v1',
    ], function ($router) {
        require base_path('routes/api.php');
    });
}

This method above will prefix all API URLs with api/v1/ in the beginning.


6. Grouping into More Files - is it worth it?

If you have huge amount of routes and want to group them even more, into separate files, then you can use the same file mentioned in the previous section - app/Providers/RouteServiceProvider.php. If you take a closer look at its map() methods, you will see commented out place at the end:

public function map()
{
    $this->mapApiRoutes();

    $this->mapWebRoutes();

    //
}

You can interpret it as kind of an "invitation" to add more files, if you wish. So you can create another method like mapAdminRoutes() inside this file, and then add it into the map() method, and your separate file will be registered and loaded automatically.

But, personally, I don't see much advantage in this approach, and I haven't seen it done very often. It brings a little more separation of routes, but sometimes you get lost in those files and not sure where to look for specific route.


7. Find Exact Route with Artisan route:list command

Speaking of bigger routes and getting lost there, we have one artisan command which helps to locate a certain route.

You probably all know that php artisan route:list will give you all the routes in the project:

But did you know you have more filter abilities to find the exact thing you want? Just add --method, or --name, or --path with parameters.

Filter by method - GET, POST etc:

Filter by name or URL part:


That's all I could tell about grouping routes in bigger projects. Do you have any other examples? Please share in the comments.

No comments or questions yet...

Like our articles?

Become a Premium Member for $129/year or $29/month
What else you will get:
  • 57 courses (1055 lessons, total 46 h 42 min)
  • 78 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent Premium Tutorials