We all use simple Route::get() and Route::post() syntax, but in bigger projects, it gets much more complicated. This article will compile various tips for different situations.

Tip 1. Route::get() BEFORE Route::resource()

For Resource Controllers, this is one of the most common mistakes, see this example:

Route::resource('photos', 'PhotoController');
Route::get('photos/popular', 'PhotoController@method');

The second route won’t be accurate, you know why? Because it will match show() method of Route::resource() which is /photos/{id}, which will assign “popular” as $id parameter.

So if you want to add any get/post route, in addition to Route::resource(), you need to put them BEFORE the resource. Like this:

Route::get('photos/popular', 'PhotoController@method');
Route::resource('photos', 'PhotoController');

Tip 2. Group in Another Group

We probably all know that we can group routes with Route::group() and assign different middlewares/prefixes and other parameters, like public routes and logged-in routes.

But what if you need a certain set of rules for sub-groups of those groups?

A typical example: you need public routes and authenticated routes, but within the authenticated group, you need to separate administrators from simple users.

So you can do this:

// public routes
Route::get('/', 'HomeController@index');

// Logged-in users - with "auth" middleware
Route::group(['middleware' => ['auth']], function () {

    // /user/XXX: In addition to "auth", this group will have middleware "simple_users"
    Route::group(['middleware' => ['simple_users'], 'prefix' => 'user'], function () {
        Route::resource('tasks', 'TaskController');
    });

    // /admin/XXX: This group won't have "simple_users", but will have "auth" and "admins"
    Route::group(['middleware' => ['admins'], 'prefix' => 'admin'], function () {
        Route::resource('users', 'UserController');
    });
});

Tip 3. Route Parameter Validation – Multi-Language Example

A pretty typical case is to prefix your routes by language locale, like fr/blog and en/article/333. How do we ensure that those two first letters are not used for some other than language?

We can validate it directly in the route, with “where” parameter:

Route::group(['prefix' => '{locale}', 'where' => ['locale' => '[a-zA-Z]{2}']], function () {
    Route::get('/', 'HomeController@index');
    Route::get('article/{id}', 'ArticleController@show');
});

The main part here is ‘where’ => [‘locale’ => ‘[a-zA-Z]{2}’] where we use a regular expression to match only two-letter combinations.


Tip 4. Dynamic Subdomain Routing

This comes directly from the official Laravel documentation, but rarely used so I think to mention it.

If you have a dynamic subdomain, like a different subdomain for every user, it needs to become a variable, right? Laravel has done that automatically for you. See example:

Route::domain('{account}.myapp.com')->group(function () {
    Route::get('user/{id}', function ($account, $id) {
        //
    });
});

Note that {account} automatically is passed as $account parameter in all inside controllers methods, so you need to accept that in all of them, don’t forget.


Tip 5. Be Careful with Non-English Route Model Binding

Sometimes, URLs should contain non-English words. For example, you have a Spanish portal for books and you want to have URL like /libros for the book list, and single book would have /libros/1, like a normal Resource Controller.

But in the database, all names should all be in English, so Laravel “magic” could work between singular and plural, right?

So if you generate a model Book with migration and Controller, you may have this command:

php artisan make:model Book -mcr

That -mcr key would generate a model and a resource controller, read more in this article. So, in that Controller, you would have this:

/**
 * Display the specified resource.
 *
 * @param  \App\Book  $book
 * @return \Illuminate\Http\Response
 */
public function show(Book $book)
{
    // ...
}

But in your routes/web.php, you would have this:

Route::resource('libros', 'BookController');

The problem is that it wouldn’t work. An even bigger problem is that it wouldn’t throw any error, just $book would be empty, and you wouldn’t understand why.

According to the official Resource Controller description, the name of the variable should be the same as a parameter in singular:

// So instead of
public function show(Book $book)
{
    // ...
}

// You should have
public function show(Book $libro)
{
    // ...
}

But, to be very honest, in projects with non-English projects, I would suggest to not use Route::resource and Route Model Binding at all. Too much “magic” is unpredictable, like how Laravel would “guess” that singular of “libros” is “libro”?


Tip 6. API Routes – from V1 to V2

Imagine you’re working with API-based project and you need to release a new version of this API. So older endpoints will stay at api/[something], and for new version you would use api/V2/[something].

The whole logic is in app/Providers/RouteServiceProvider.php:

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'));
}

As you can see, API routes are registered in a separate function with prefix api/.

So, if you want to create V2 route group, you can create a separate routes/api_v2.php and do this:

public function map()
{
    // ... older functions

    $this->mapApiV2Routes();
}

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

This way, old routes wouldn’t break, and you would just create a new set of routes.


Tip 7. Rate Limiting – Global and for Guests/Users

This also comes from official documentation, but with less-known details.

First, you can limit some URL to be called a maximum of 60 times per minute, with throttle:60,1.

Route::middleware('auth:api', 'throttle:60,1')->group(function () {
    Route::get('/user', function () {
        //
    });
});

But did you know you can do it separately for public and for logged-in users?

// maximum of 10 requests per minute for guests 60 for authenticated users
Route::middleware('throttle:10|60,1')->group(function () {
    //
});

Also, you can have a DB field users.rate_limit and limit the amount for specific user:

Route::middleware('auth:api', 'throttle:rate_limit,1')->group(function () {
    Route::get('/user', function () {
        //
    });
});

Tip 8. Route List and Route Caching

Final tip – how to check existing routes.

Not all of you know what routes exactly are hidden under Route::resource(), or under some more complex Route::group statement.

But at any point, you can check your actual route with Artisan command:

php artisan route:list

Keep in mind, that you are using route caching, after every change of your routes, you need to launch command:

php artisan route:clear