Laravel API Versioning

Intro

I am developing a project that resembles the application I am working on at my job. I thought it would be good to record some of my research. The first roadblock I came across was API versioning, and I realized I had no idea how the library I use at work actually functions.

Another team member integrated this versioning library at work, so when I added it to my own project I decided to actually take a look at the source code. I also found this tutorial which performs a similar function, although this takes a manual file editing approach.

How it works

Both options work on the idea that you will have an API prefix (/v1, /v2, …, /vn) to denote updates to your software. You will also have n number versions of your Models, Views, Controllers, Services, Repositories, or any piece of the project that changes. On app/Providers/RouteServiceProvider.php – inside the boot() function we can modify our api routes (I have grouped all my code into one large function here for readability, you can split into as many separate files as you wish).

$this->routes(function () {
    Route::prefix('api')
        ->middleware('api')
        ->namespace('App\Http\Controllers\Api')
        ->group(function () {
            Route::prefix('/v1')
                ->namespace('V1')
                ->group(function () {
                    Route::get('/user', function () {
                        return 'user index v1';
                    });

                    Route::get('/user/{userId}', 'UserController@show');
                });

            Route::prefix('/v2')
                ->namespace('V2')
                ->group(function () {
                    Route::get('/user/{userId}', 'UserController@show');
                });
        });

    ...
});

This gives us two versions of the API. Both v1 and v2 contain a ‘show’ route inside a versioned UserController. Also v1 contains an ‘index’ route, which it is not present in v2. These nested route groups merge attributes with their parent group. For example: The v1 index function inherits prefix=api/v1, middleware=api, and namespace=App\Http\Controllers\Api\V1.

The versioning tutorial seems to be a little bit older (pre Laravel 8) so I decided to find out what the use of this namespace function was. In the Namespace Prefixing Upgrade section of the documentation I found that the namespace would automatically be applied to controllers. So instead of having a ‘use’ statement, or specifying the entire controller namespace like App\Http\Controllers\Api\V1\UserController – you can simply write UserController and Laravel will resolve it. One advantage I saw in using this method is that if your routes did not change from one version to another (only the internal logic changes) then you could reuse a route file and all the versioning would be inherited through that namespace.

use App\Http\Controllers\Api\V1\UserController;

// Using PHP callable syntax...
Route::get('/users', [UserController::class, 'index']);
// Does not work with inherited namespace
Route::get('/user/{userId}', [UserController::class, 'show']);
// surprisingly works
Route::resource('users', UserController::class);

// Using string syntax...
// works, but specifying namespace is redundant
Route::get('/users', 'App\Http\Controllers\UserController@index');
// works too
Route::get('/user/{userId}', 'UserController@show');

It seems like a bug that callable syntax is not resolving for the ‘show’ route, but abbreviated string syntax does resolve. Add to the fact that use statements along with callable syntax ‘provides better support for jumping to the controller class in many IDEs’ and you can see why I finally ditched the namespace function in favor of the modern approach.

Finishing up with the tutorial I found that config setting api_latest and APIVersion middleware essentially did nothing. Calling the API without a version prefix just results in a 404. Calling a nonexistent v2 endpoint does not fallback to the v1 version either. Once I realized these points, and deleted the unnecessary code, I saw that this tutorial was essentially only about how to structure your versioned files. Now it’s time for a library to fill in some of the gaps.

Package Discovery

https://laravel.com/docs/8.x/packages#package-discovery

When I install this versioning library via composer – it includes an entry point into my project from its contained composer.json file.

"extra": {
    "laravel": {
        "providers": [
            "MbpCoder\\ApiVersioning\\ApiVersioningServiceProvider"
        ],
        "aliases": {
            "ApiVersioning": "MbpCoder\\ApiVersioning\\ApiVersioningFacade"
        }
    }
}

Here a Service Provider is added to our app. Beginning in vendor/mbpcoder/laravel-api-versioning/src/ApiVersioningServiceProvider.php we can trace all the logic happening. We begin in the register() method which creates a new ApiVersioning() class. In this constructor we see the author is replacing the UriValidator from the default \Illuminate\Routing\Matching\UriValidator, to their own version \MbpCoder\ApiVersioning\UriValidator. It seems the purpose of the default UriValidator is to remove and trailing ‘/’ from the path, and then check that path matches one of the routes you have defined in RouteServiceProvider.php. The version by mbpcoder maintains that same logic while also looping through config(‘apiVersioning.api_versions’) to find an API fallback path when the given path is not found (nonexistent localhost/api/v2/user would try localhost/api/v1/user next).

The next step is to publish the config file. This runs the boot() method inside ApiVersioningServiceProvider.php and copies the config from vendor/mbpcoder/laravel-api-versioning/config/config.php to config/apiVersioning.php. Now you can configure the external library from your main app. I set 'api_versions' => ['v2', 'v1'] and now the API fallback is completely setup.

Conclusion

It seems that my testing with this library has shown some flaws. Namely if I attempt to fallback on a route that has route parameters (localhost/api/v2/user/1) I get this error: Too few arguments to function App\Http\Controllers\Api\V1\UserController::show(). This means that we are falling back to the correctly versioned controller, but route parameters are not being transferred. Maybe we can find the answer in another post.

Leave a comment

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