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.

Like our articles?
Check out our Laravel online courses!

9 COMMENTS

  1. The mapping new routing files is the only way to go doing a multi-tenant application. This keeps all routes for that tenant in one place rather than trying to hunt them down in a web file that could potentially have 1000’s of links.
    There is also the potential to do dynamic routes and just pull them from the database, if not done right though there is a huge performance hit.
    For me the best way when creating a new tenant was to append a route to the web file for the index and that could either be an auth route or public.

    • Hi Jeremy, thanks for the comment, great point about multitenant applications, I didn’t think about those when writing this article, and personally I’ve never built such structure that would require things you mentioned. But you’re totally right, it’s a good case for separate files.

  2. Thanks for the article!

    Mapping of separate route files is also useful to organize webhook routes for services like Stripe and Nexmo. I like to create a `webhooks.php` routes file in any project requiring endpoints for webhooks from third party services.

  3. Thanks Povilas for the article, enjoyed it. I wanted to ask a question in regards to setting up routes for a site which could be completely private, requires login or a public site with a login component. This is a config setting on setup, would it better to have separate route files or could this be handled by prefixes?

    • Hi Danny,
      Not sure if I understood correctly, but probably you’re talking about route group with Middleware Auth?

LEAVE A REPLY

Please enter your comment!
Please enter your name here