Streamlining API Versioning in Laravel: A Smart Approach to Route Inheritance and Overrides

Streamlining API Versioning in Laravel: A Smart Approach to Route Inheritance and Overrides

A new Laravel install has several route files. The two most widely used route files are routes/web.php and route/api.php. As their names imply, these are for web (browser) routes and API related routes.

In this post we’re only going to be looking at the API route.

Let’s assume that we have a GET endpoint to get a list of all users in our API. We could do this simply in the route file (typically we’d set up controllers etc but this is not the purpose of this post).

So, in our routes/api.php file we could have this route:

// routes/api.php
Route::get("users", function() {
    return User::get();
});

This route would be accessible via the api prefix, like this: https://example.com/api/users. Incidentally, Laravel will give this to us as nicely formatted JSON.

This is really nice, but what happens if/when your API needs to be updated and you’re introducing breaking changes? You still want people using the old code to get valid results (at least for a while in your deprecation window), but you want people using the new code to get valid results too. In this case, having a single api route won’t do.

A fairly common way of handling this is to version your endpoints. So rather than having an endpoint like https://example.com/api/users, you might want https://example.com/v1/users.

So how do we version our API endpoints? In your favourite code editor navigate to app/Providers/RouteServiceProvide.php. In the boot method of this file you should see a call to a routes function, like this:

// app/Providers/RouteServiceProvider.php
        $this->routes(function () {
            Route::middleware('api')
                ->prefix('api')
                ->group(base_path('routes/api.php'));

            Route::middleware('web')
                ->group(base_path('routes/web.php'));
        });

As you can see, this is where the api and web endpoints defined. Notice that the api middleware has a prefix function where you tell it what the prefix is. That’s the api part in https://example.com/api/users.

Here, we’ll simply copy the api route and change it to v1 (or you can change the api route, no need to keep it – we’ll keep it for this example). This is what you’ll end up with:

        $this->routes(function () {
            Route::middleware('api')
                ->prefix('api')
                ->group(base_path('routes/api.php'));


            Route::middleware('api')
                ->prefix('v1')
                ->group(base_path('routes/v1.php'));

            Route::middleware('web')
                ->group(base_path('routes/web.php'));
        });

Alright, so lets notice a few things here:

  • We don’t change the middleware parameter. That remains api. That’s does not have anything to do with the route endpoint, that’s got to do with middleware (as the name implies).
  • The group function is where we tell Laravel which route file to use.

Ok, so now we need out v1.php file. In the routes directory you can copy the api.php file and name the copy v1.php. Keep the includes but remove any routes. For this simple demo we’ll add in a single route called users and we’ll simple return a simple message from a closure:

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;


Route::get("users", function () {
    return "this is v1 users";
});

So, now that we’ve set the route up in app/Providers/RouteServiceProvider.php and we’ve created the routes/v1.php file with our get route we should be able to navigate to https://example.com/v1/users to get our list of json users! Hooray we have versioning working. Well, sort of.

Right, so now time goes on and our app and api have grown. We have a second route in our v1.php file called users2, like this:

// routes/v1.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;


Route::get("users", function () {
    return "this is v1 users";
});


Route::get("users2", function () {
    return "this is v1 users2";
})->name("v1.users2");

So now we can also navigate to https://example.com/v1/users2.

Now more time goes on and we need to make changes to handle new requirements or traffic, etc so we need to move to a v2 of our API. This will introduce some breaking changes.

Back in app/Providers/RouteServiceProvider.php we copy the v1 route and add in our v2 route, like this:

        $this->routes(function () {

            Route::middleware('api')
                ->prefix('api')
                ->group(base_path('routes/api.php'));


                Route::middleware('api')
                ->prefix('v1')
                ->group(base_path('routes/v1.php'));
    

                Route::middleware('api')
                ->prefix('v2')
                ->group(base_path('routes/v2.php'));
    
        

            Route::middleware('web')
                ->group(base_path('routes/web.php'));
        });

We also need to create our new v2.php route file. This is where things could get messy! If some of our routes still use the same code but other routes use new code we want a way to manage this without duplicating identical routes. In Laravel route files we can include other route files. Laravel’s route registration process means that the latest route definitions take precedence. In other words we can include the v1 routes in the v2.php file and override the routes which have changed, like this:

// routes/v2.php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

require __DIR__.'/v1.php';


Route::get("users2", function () {

    return "this is v2 users2";
    
})->name("v2.users2");

In this new v2.php file we first include the v1.php file. This will import the get endpoint users. In v2.php we override the users2 endpoint and as mentioned because of Laravel’s route registration process giving precedence to routes declared later the users2 route will come from the v2.php file.

This is one way to manage api versions in Laravel. If you have any other methods to achieve this please post in the comments.

Share

Leave a Reply

Your email address will not be published. Required fields are marked *