How to structure and generate external URLs in Laravel

Laravel provides the features and structure for generating your application's URLs straight out of the box. It includes URL generation, named routes, appending queries, and more. This works well as long as we're generating URLs within our application. As soon as it comes to external URLs, though, the framework doesn't really cover anything. Or so I've thought.

In this short post, I'll explain how you can use the internal Laravel's route feature set to generate your external URLs. You might be wondering why. Since software development is all about solving problems, let's start with a real-life example:

# The problem

For the past few months, I've been working on a web application, which is part of a bigger ecosystem of apps. It is a simple platform that helps customers manage their plans for a virtual product - see basic information, edit some settings, upgrade their plan and make some purchases.

The business already uses a different application for making purchases/placing orders, and also uses a lot of informational pages that must be accessible from the whole ecosystem of apps. So to keep everything maintainable and scalable, a decision was made to create a new separate app and keep its scope simple: this means, instead of duplicating content or features, we need to link customers from the new application to external sources.

Sounds simple enough - just type in the desired URLs to the anchor tags, and you're done, right?

What happens when you need to reuse the same endpoint multiple times, in multiple pages/components? Do you just copy and paste your URLs? What do you do when the endpoint or domain name changes for those external sources? If you've only used hardcoded values in your templates, this could mean going over hundreds of templates/components and refactoring them.

Now sure, you could do yourself a favor and use some constants, and reference them in your templates. This saves you from the headache when the URLs change, you'll avoid duplication and probably some typos too. But where do you store those constants, how do you organize them? Do you create a config file or a class constant? Where do you place those?

Moreover, imagine that before displaying said URLs, you need to pre-process them, depending on the current user's request (like adding additional query parameters, etc.). If you're using constants, you're back to square one - going over each of their usages, wrapping them in methods, or appending some values.

Well guess what - to solve all of these problems, we can actually repurpose Laravel's default route generation 😉 As mentioned previously, it already covers URL structuring, placement, and generating. Here's how:

# The solution

We'll define our new routes in a standard Laravel routes file, register them with a new RouteServiceProvider and generate URLs with the route() helper function. As always, I think examples work best, so let's imagine we're generating routes for our company's pages at Trustpilot and Facebook.

Let's get started:

# 1. Defining our routes.

Go ahead and create a new file in the routes directory. I'll call mine routes/external.php to place all of my external routes in a single file. You can split them by provider here, if you wish, for example: routes/trustpilot.php, routes/facebook.php.

We'll learn how to define our routes more elegantly, at another time. To simply get my point across, it may look something like:

<?php 

use Illuminate\Support\Facades\Route;

Route::name('trustpilot.')->domain('https://www.truspilot.com')->group(function () {
    Route::get('/review/acme.com')
        ->name('reviews');

    Route::get('/evaluate/acme.com')
        ->name('evaluate');
});

Route::name('facebook.')->domain('https://www.facebook.com')->group(function () {
    Route::get('/AcmeCorp/reviews')
        ->name('reviews');

    Route::get('/AcmeCorp/about')
        ->name('about');
});

As you can see, we're using the standard Laravel structure - routes are in their respectful directory, we're only using the framework's native methods, and we're not using any workarounds to ensure future compatibility. We're placing our routes in two groups that each use a domain name. Since we're generating external routes, make sure those domains aren't bound to your current application.

You may also notice, that we didn't define an "action" (like controller methods) for our routes. That's because Laravel's router accepts null for the second argument, which is registered as a Closure under the hood. Please keep in mind that Laravel only supports caching routes (php artisan route:cache) with Closures since version 7 (opens new window).

# 2. Registering new routes

Laravel registers routes by the use of the Illuminate\Foundation\Support\Providers\RouteServiceProvider class. By default, you should already have a class that implements it, which is usually found in App\Providers\RouteServiceProvider.

Since we've already defined our routes in a separate file, let's stay consistent and whip up a new service provider class. You can either create a new class from scratch or just use the command provided by Laravel.

Let's call ours ExternalRouteServiceProvider:

php artisan make:provider ExternalRouteServiceProvider

Once the class has been created, make sure to also register (opens new window) it in your app's configuration at config/app.php.

With that out of the way, let's go ahead and extend the original service provider Illuminate\Foundation\Support\Providers\RouteServiceProvider. It provides us with the routes() method, which we'll use to register the routes, just like in App\Providers\RouteServiceProvider. I also like to prefix all of my route names as external in the provider.

At the end of the day, it looks something like this:

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\RouteServiceProvider;
use Illuminate\Support\Facades\Route;

class ExternalRouteServiceProvider extends RouteServiceProvider
{
    public function boot()
    {
        $this->routes(function () {
            Route::namespace($this->namespace)->name('external.')
                ->group(base_path('routes/external.php'));
        });
    }
}

And that's it! You should already see your routes by listing them:

php artisan route:list
+-------------------+----------+-------------------+-----------------------------+---------+------------+
| Domain            | Method   | URI               | Name                        | Action  | Middleware |
+-------------------+----------+-------------------+-----------------------------+---------+------------+
| www.truspilot.com | GET|HEAD | review/acme.com   | external.trustpilot.reviews | Closure |            |
| www.truspilot.com | GET|HEAD | evaluate/acme.com | external.truspilot.evaluate | Closure |            |
| www.facebook.com  | GET|HEAD | AcmeCorp/reviews  | external.facebook.reviews   | Closure |            |
| www.facebook.com  | GET|HEAD | AcmeCorp/about    | external.facebook.about     | Closure |            |
+----------------+----------+----------------------+-----------------------------+---------+------------+

# 3. Generating URLs by using routes

Now you can easily generate your external URLs the same way you do with the internal ones. Let's say we want to list them in an arbitrary navigation component:

<nav>
  <ul>
    <li><a href="{{ route('external.trustpilot.reviews') }}">Trustpilot Reviews</a></li>
    <li><a href="{{ route('external.facebook.reviews') }}">Facebook Reviews</a></li>
    <li><a href="{{ route('external.truspilot.evaluate') }}">Review us on Trustpilot</a></li>
    <li><a href="{{ route('external.facebook.about') }}">About</a></li>
  </ul>
</nav>

Voilà, that's all there is to it! 👏

# Conclusion & TLDR

With this approach, we:

  • Achieved a consistent and organized solution, by following the framework's provided structure.
  • Did little to no work and got all the benefits! We've just created a new route file, registered it in a new provider, and generated links with the default route() helper.
  • Can change our endpoints quickly and without refactoring a bunch of code.
  • And of course, we can pre-process the routes since we're not using hard-coded strings anymore. Just override the helper function.

Thanks for taking your time. I hope you've got some value from my post ✌️