Laravel Route Grouping: 6 Techniques to Organize Routes

Tutorials

June 3rd, 2022

Laravel Route Grouping: 6 Techniques to Organize Routes

Laravel Routing is the feature that developers learn from the very beginning. But as their projects grow, it's getting harder to manage evergrowing routes files, scrolling to find the right Route::get() statements. Luckily, there are techniques to make the route files shorter and more readable, grouping routes and their settings in different ways. Let's take a look.

And no, I won't just talk about the general simple Route::group(), that one is the beginner level. Let's dive a bit deeper than that.


Grouping 1. Route::resource and Route::apiResource

Let's start with the elephant in the room: this is probably the most well-known grouping. If you have a typical set of CRUD actions around one Model, it's worth grouping them into a resource controller

Such controller can consist up to 7 methods (but may have fewer):

  • index()
  • create()
  • store()
  • show()
  • edit()
  • update()
  • destroy()

So if your set of routes corresponds to those methods, instead of:

1Route::get('books', [BookController::class, 'index'])->name('books.index');
2Route::get('books/create', [BookController::class, 'create'])->name('books.create');
3Route::post('books', [BookController::class, 'store'])->name('books.store');
4Route::get('books/{book}', [BookController::class, 'show'])->name('books.show');
5Route::get('books/{book}/edit', [BookController::class, 'edit'])->name('books.edit');
6Route::put('books/{book}', [BookController::class, 'update'])->name('books.update');
7Route::delete('books/{book}', [BookController::class, 'destroy'])->name('books.destroy');

... you may have just one line:

1Route::resource('books', BookController::class);

If you work with an API project, you don't need the visual forms for create/edit, so you may have a different syntax with apiResource() that would cover 5 methods out of 7:

1Route::apiResource('books', BookController::class);

Also, I advise you to consider the resource controllers even if you have 2-4 methods, and not the full 7. Just because it keeps the standard naming convention - for URLs, methods, and route names. For example, in this case, you don't need to provide the names manually:

1Route::get('books/create', [BookController::class, 'create'])->name('books.create');
2Route::post('books', [BookController::class, 'store'])->name('books.store');
3 
4// Instead, here names "books.create" and "books.store" are assigned automatically
5Route::resource('books', BookController::class)->only(['create', 'store']);

Grouping 2. Group Within a Group

Of course, everyone knows about the general Route grouping. But for more complex projects, one level of grouping may not be enough.

Realistic example: you want the authorized routes to be grouped with auth middleware, but inside you need to separate more sub-groups, like administrator and simple user.

1Route::middleware('auth')->group(function() {
2 
3 Route::middleware('is_admin')->prefix('admin')->group(function() {
4 Route::get(...) // administrator routes
5 });
6 
7 Route::middleware('is_user')->prefix('user')->group(function() {
8 Route::get(...) // user routes
9 });
10});

Grouping 3. Repeating Middleware Into Group

What if you have quite a lot of middlewares, some of them repeating in a few route groups?

1Route::prefix('students')->middleware(['auth', 'check.role', 'check.user.status', 'check.invoice.status', 'locale'])->group(function () {
2 // ... student routes
3});
4 
5Route::prefix('managers')->middleware(['auth', 'check.role', 'check.user.status', 'locale'])->group(function () {
6 // ... manager routes
7});

As you can see, there are 5 middlewares, 4 of them repeating. So, we can move those 4 into a separate middleware group, in the file app/Http/Kernel.php:

1protected $middlewareGroups = [
2 // This group comes from default Laravel
3 'web' => [
4 \App\Http\Middleware\EncryptCookies::class,
5 \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
6 \Illuminate\Session\Middleware\StartSession::class,
7 \Illuminate\View\Middleware\ShareErrorsFromSession::class,
8 \App\Http\Middleware\VerifyCsrfToken::class,
9 \Illuminate\Routing\Middleware\SubstituteBindings::class,
10 ],
11 
12 // This group comes from default Laravel
13 'api' => [
14 // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
15 'throttle:api',
16 \Illuminate\Routing\Middleware\SubstituteBindings::class,
17 ],
18 
19 // THIS IS OUR NEW MIDDLEWARE GROUP
20 'check_user' => [
21 'auth',
22 'check.role',
23 'check.user.status',
24 'locale'
25 ],
26];

So we named our group check_user, and now we can shorten the routes:

1Route::prefix('students')->middleware(['check_user', 'check.invoice.status'])->group(function () {
2 // ... student routes
3});
4 
5Route::prefix('managers')->middleware(['check_user'])->group(function () {
6 // ... manager routes
7});

Grouping 4. Same Name Controllers, Different Namespaces

Quite a common situation is to have, for example, HomeController for different user roles, like Admin/HomeController and User/HomeController. And if you use the full path in your routes, it looks something like this:

1Route::prefix('admin')->middleware('is_admin')->group(function () {
2 Route::get('home', [\App\Http\Controllers\Admin\HomeController::class, 'index']);
3});
4 
5Route::prefix('user')->middleware('is_user')->group(function () {
6 Route::get('home', [\App\Http\Controllers\User\HomeController::class, 'index']);
7});

Quite a lot of code to type with those full paths, right? That's why many developers prefer to have only HomeController::class in the route list and add something like this on top:

1use App\Http\Controllers\Admin\HomeController;

But the problem here is that we have the same controller class name! So, this wouldn't work:

1use App\Http\Controllers\Admin\HomeController;
2use App\Http\Controllers\User\HomeController;

Which one would be the "official" one? Well, one way is to change the name and assign the alias for one of them:

1use App\Http\Controllers\Admin\HomeController as AdminHomeController;
2use App\Http\Controllers\User\HomeController;
3 
4Route::prefix('admin')->middleware('is_admin')->group(function () {
5 Route::get('home', [AdminHomeController::class, 'index']);
6});
7 
8Route::prefix('user')->middleware('is_user')->group(function () {
9 Route::get('home', [HomeController::class, 'index']);
10});

But, personally, changing the name of the class on top is quite confusing to me, I like another approach: to add a namespace() for the sub-folders of the Controllers:

1Route::prefix('admin')->namespace('App\Http\Controllers\Admin')->middleware('is_admin')->group(function () {
2 Route::get('home', [HomeController::class, 'index']);
3 // ... other controllers from Admin namespace
4});
5 
6Route::prefix('user')->namespace('App\Http\Controllers\User')->middleware('is_user')->group(function () {
7 Route::get('home', [HomeController::class, 'index']);
8 // ... other controllers from User namespace
9});

Grouping 5. Separate Route Files

If you feel that your main routes/web.php or routes/api.php is getting too big, you may take some of the routes and put them into a separate file, name them however you want, like routes/admin.php.

Then, to enable that file to be included, you have two ways: I call the "Laravel way" and "PHP way".

If you want to follow the structure of how Laravel structures its default route files, it's happening in the app/Providers/RouteServiceProvider.php:

1public function boot()
2{
3 $this->configureRateLimiting();
4 
5 $this->routes(function () {
6 Route::middleware('api')
7 ->prefix('api')
8 ->group(base_path('routes/api.php'));
9 
10 Route::middleware('web')
11 ->group(base_path('routes/web.php'));
12 });
13}

As you can see, both routes/api.php and routes/web.php are here, with a bit different settings. So, all you need to do is add your admin file here:

1$this->routes(function () {
2 Route::middleware('api')
3 ->prefix('api')
4 ->group(base_path('routes/api.php'));
5 
6 Route::middleware('web')
7 ->group(base_path('routes/web.php'));
8 
9 Route::middleware('is_admin')
10 ->group(base_path('routes/admin.php'));
11});

But if you don't want to dive into service providers, there's a shorter way - just include/require your routes file into another file like you would do in any PHP file, outside of the Laravel framework.

In fact, it's done by Taylor Otwell himself, requiring the routes/auth.php file directly into Laravel Breeze routes:

routes/web.php:

1Route::get('/', function () {
2 return view('welcome');
3});
4 
5Route::get('/dashboard', function () {
6 return view('dashboard');
7})->middleware(['auth'])->name('dashboard');
8 
9require __DIR__.'/auth.php';

Grouping 6. New in Laravel 9: Route::controller()

If you have a few methods in the Controller but they don't follow the standard Resource structure, you may still group them, without repeating the Controller name for every method.

Instead:

1Route::get('profile', [ProfileController::class, 'getProfile']);
2Route::put('profile', [ProfileController::class, 'updateProfile']);
3Route::delete('profile', [ProfileController::class, 'deleteProfile']);

You can do:

1Route::controller(ProfileController::class)->group(function() {
2 Route::get('profile', 'getProfile');
3 Route::put('profile', 'updateProfile');
4 Route::delete('profile', 'deleteProfile');
5});

This functionality is available in Laravel 9, and the latest minor versions of Laravel 8.


That's it, these are the grouping techniques that, hopefully, will help you to organize and maintain your routes, no matter how big your project grows.

Filed in:

PovilasKorop

A web-developer with 15+ years experience, founder of Laravel QuickAdminPanel generator.

Sharing Laravel lessons on Youtube with channel Laravel Daily.