4 ways to clean up your Laravel routes files

As your Laravel application grows, your routes file grows with it. And most often it doesn’t just grow with it, it becomes messy and hard to read. So cleaning it up once every while can be time well spend.

1. Use resource routes

The first route definition you see in your web.php routes file after starting a new Laravel project is:

<?php

Route::get('/', function () {
    return view('welcome');
});

If you’re running PHP 7.4 or higher, locally and on your server, you can clean this one up a little with an arrow function. The following is equivalent to the route definition above, but using an arrow function.

<?php

Route::get('/', fn() => view('welcome'));

But if you’d ask me routes using closures cause quite some visual noise. So I rather use controllers over closures unless it really makes no sense to make a controller for a view.

Another pro for using controllers for your routes is that you can leverage resource routes. When you define a resource route you get routes for all the CRUD actions you’ll probably need.

<?php

Route::resource('users', 'UsersController');

Just that one line generated the following routes for you.

$ php artisan route:list --compact --name user
+-----------+-------------------+----------------------------------------------+
| Method    | URI               | Action                                       |
+-----------+-------------------+----------------------------------------------+
| GET|HEAD  | users             | App\Http\Controllers\UsersController@index   |
| POST      | users             | App\Http\Controllers\UsersController@store   |
| GET|HEAD  | users/create      | App\Http\Controllers\UsersController@create  |
| GET|HEAD  | users/{user}      | App\Http\Controllers\UsersController@show    |
| PUT|PATCH | users/{user}      | App\Http\Controllers\UsersController@update  |
| DELETE    | users/{user}      | App\Http\Controllers\UsersController@destroy |
| GET|HEAD  | users/{user}/edit | App\Http\Controllers\UsersController@edit    |
+-----------+-------------------+----------------------------------------------+

Which is super nice ofcourse, but it also means you need a controller with those same actions. That’s a low effort task luckily. By running the following artisan command you get a new controller with all seven actions.

$ php make:controller UsersController --resource

There is a chance however that you don’t need all of the actions. If that’s the case you can chain the route definition with ->only([...]) or ->except([...]) to get a subset of all the resource routes.

<?php

Route::resource('users', 'UsersController')->only(['index', 'show']);

This will limit the user routes to only an index and a show action. Even if you just have a couple of actions this quickly saves you lines of code.

$ php artisan route:list --compact --name user
+----------+--------------+--------------------------------------------+
| Method   | URI          | Action                                     |
+----------+--------------+--------------------------------------------+
| GET|HEAD | users        | App\Http\Controllers\UsersController@index |
| GET|HEAD | users/{user} | App\Http\Controllers\UsersController@show  |
+----------+--------------+--------------------------------------------+

2. Register routes in tuples

Since Laravel 5.6 we can use arrays instead of strings to define the controller and action of a route. This tuple syntax looks like the following.

<?php

Route::get('users', [\App\Http\Controllers\UsersController::class, 'index']);

You can clean this up by adding a use statement for the UsersController. Check if your editor can hide these, because these will start to add up pretty quick when your application grows.

<?php

use App\Http\Controllers\UsersController;

Route::get('users', [UsersController::class, 'index']);

Or you can set the namespace of the routes file to the same namespace of your controllers. What falls in the category hacky if you’d ask me, but being a little creative can be fun sometimes :-).

<?php

namespace App\Http\Controllers;

Route::get('users', [UsersController::class, 'index']);

Using the tuple syntax might allow you to use the “Go to Definition” and “Refactor” functions of your IDE. Which are nice benefits in (at least) my workflow compared to the string syntax which feels a bit magical.

One thing that is a bit of a bummer is that it doesn’t work with resource routes at the moment.

For more information about tuple routes, Freek.dev has a great opinion about them.

3. Split up the routes file

Larger applications have easily dozens of route definitions divided in multipe route groups. Lets say your application has a public section, a section that requires authentication and a section that requires admin access. In that case the routes file has a structure similar to something like this:

<?php

// routes/web.php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});

Route::group(['middleware' => 'auth', 'prefix' => 'app'], function () {
    Route::resource('users', 'UsersController')->only(['index', 'show']);

    Route::group(['middleware' => 'admin'], function () {
        Route::get('dashboard', 'DashboardController@index');
    });
});

I personally find it hard to follow what is happening with all the groupings and the level of indent. Imagine managing an application with hundreds of routes. We can solve this by splitting the routes file up in three files: routes/web.php, routes/app.php and routes/admin.php.

So we end up with the following three files:

<?php

// routes/web.php

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
    return view('welcome');
});
<?php

// routes/app.php

use Illuminate\Support\Facades\Route;

Route::resource('users', 'UsersController')->only(['index', 'show']);
<?php

// routes/admin.php

use Illuminate\Support\Facades\Route;

Route::get('dashboard', 'DashboardController@index');

But this doesn’t work correctly just like that. If we check our available routes now we that only the one from routes/web.php is still available.

$ php artisan route:list
+--------+----------+----------+------+---------+--------------+
| Domain | Method   | URI      | Name | Action  | Middleware   |
+--------+----------+----------+------+---------+--------------+
|        | GET|HEAD | /        |      | Closure | web          |
+--------+----------+----------+------+---------+--------------+

Also notice that we got rid of all the middleware in the routes/app.php and routes/admin.php files. We fix that later on ;-)

For this to work we need to add the newly created routes files to the RouteServiceProvider. And in the RouteServiceProvider class you want to look for the map() function. Which, if you haven’t touched it before, should look like this:

<?php

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

    $this->mapWebRoutes();

    //
}

We will add calls mapAppRoutes and mapAdminRoutes to this function in which we will register the other two routes files.

<?php

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

    $this->mapWebRoutes();

    $this->mapAppRoutes();

    $this->mapAdminRoutes();
}

So now we need to write those two new functions. Both should be very similar to the mapWebRoutes function that did already exist. But we need to change the file path and add back the middleware.

<?php

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

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

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

And voila, our route definitions are back as we had them before. Only now in clean separated files.

$ php artisan route:list
+--------+----------+--------------+-------------+------------------------------------------------+--------------+
| Domain | Method   | URI          | Name        | Action                                         | Middleware   |
+--------+----------+--------------+-------------+------------------------------------------------+--------------+
|        | GET|HEAD | /            |             | Closure                                        | web          |
|        | GET|HEAD | dashboard    |             | App\Http\Controllers\DashboardController@index | web          |
|        | GET|HEAD | users        | users.index | App\Http\Controllers\UsersController@index     | auth         |
|        | GET|HEAD | users/{user} | users.show  | App\Http\Controllers\UsersController@show      | auth         |
+--------+----------+--------------+-------------+------------------------------------------------+--------------+

4. Routes macros

The Marcroable trait is something you might have seen before. With macros you can basically “add” functions to the interface of a class. This is useful when you want to extend one of Laravels core classes with a function that’s really specific to your application for example.

Luckily for us, the Laravel router implements the Macroable trait. So what this allows us to do is to bundle a set of routes in a macro and register them at once in our routes file.

Lets say your application implements a service that expects data to be pushed to it from another service. An incoming webhook from GitHub for example. In that case you can bundle the route definitions with your service, and define them using a single line in your routes file.

You can define the route macro in the service provider of your GitHub service. It requires a name and an closure. And within the closure you can add route definitions as you would normally do in your routes file. Just make sure you add a use statement for the Route facade.

<?php

public function boot()
{
    Route::macro('githubServiceRoutes', function () {
        Route::group([ 'middleware' => 'validate_github_secret'], function () {
            Route::post('github/webhook', 'GithubController@handle');
        });
    });
}

In your routes file you can now use the name you gave the macro as a static function for the Route facade.

<?php

// routes/admin.php

use Illuminate\Support\Facades\Route;

Route::githubServiceRoutes();

Lets double check with the artisan routes command. And yup, the route that we registered in the Github ServiceProvider is now available.

$ php artisan route:list
+--------+----------+----------------+------+----------------------------------------------+----------------------------+
| Domain | Method   | URI            | Name | Action                                       | Middleware                 |
+--------+----------+----------------+------+----------------------------------------------+----------------------------+
|        | GET|HEAD | /              |      | Closure                                      | web                        |
|        | POST     | github/webhook |      | App\Http\Controllers\GithubController@handle | web,validate_github_secret |
+--------+----------+----------------+------+----------------------------------------------+----------------------------+

See Also

6 Non-Programming Books for Programmers