DEV Community

Cover image for Dynamic API Versioning in Laravel
Muhammad MP
Muhammad MP

Posted on

Dynamic API Versioning in Laravel

Recently, I applied for a software engineering job, and at first, I was too excited about it. Yes, there were some tasks to do that finally made me give up because I didn't have much time to do all of what was wanted in the description, and I also had another interviews. So I didn't get through the tasks. Believe me or not, it was like an actual backlog!

The task description

The problem

But, one of the tasks impressed me to do some stuff and write this post. I tell you about the question and clarify the problem:

  1. We build an API (v1).
  2. Then we have another version (v2) with some new features.
  3. Not every feature has changed, let's keep it DRY!
Controllers/
├─ v1/
│  ├─ PostController.php
├─ v2/
│  ├─ PostController.php
Enter fullscreen mode Exit fullscreen mode

Let's suppose that we don't have the show() method in the v2/PostController.php because we don't need it. But the first version's index() method has a totally different implementation than the second version. Consider that the boss doesn't like any duplication!

How do you implement it?


Solution #1: Inheritance

I assume that you know what inheritance is and get directly into my first solution to the problem.

In the first version, we implement everything we need normally. When we start building the second version's endpoints, we just implement what's changed. I demonstrate the idea with this example:

api.php

Route::get('/v1/posts', [V1PostController::class, 'index']);
Route::get('/v1/posts/{id}', [V1PostController::class, 'show']);

Route::get('/v2/posts', [V2PostController::class, 'show']);
Route::get('/v2/posts/{id}', [V2PostController::class, 'show']);
Enter fullscreen mode Exit fullscreen mode

V1/PostController.php

<?php

namespace App\Http\Controllers\API\V1;

use App\Http\Controllers\Controller;

class PostController extends Controller
{
    public function index()
    {
        return 'V1 index';
    }

    public function show($id)
    {
        return "V1 show: {$id}";
    }
}
Enter fullscreen mode Exit fullscreen mode

V2/PostController.php

<?php

namespace App\Http\Controllers\API\V2;

use App\Http\Controllers\API\V1\PostController as V1PostController;

class PostController extends V1PostController
{
    public function index()
    {
        return 'V2 index';
    }
}
Enter fullscreen mode Exit fullscreen mode

As we haven't overridden the show() method, the second version's controller provides the first version's functionality and opening https://localhost/api/v2/posts/1 shows us:

V1 show: 1


Solution #2: handleVersion()

The problem with the previous solution is that we need to define all the routes, even if they do the same thing in the previous version. What's the point of defining the /v2/posts/{id} route if it's not actually implemented? Are we supposed to repeat a hundred routes per version? Of course not!

Another fix came into my head! How does router work? Let's take a glance:

Route::get('uri here', ['controller namespace', 'method name']);
Enter fullscreen mode Exit fullscreen mode

So I wrote such a function:
helpers.php

function handleVersion(string $controller, string $method)
{
    $latestApiVersion = config('app.latest_api_version');
    preg_match("/api\/v(\\d+)/i", request()->path(), $apiVersion);
    $apiVersion = $apiVersion[1] ?? $latestApiVersion;

    if ($apiVersion > $latestApiVersion) {
        abort(404);
    }

    $namespace = "App\Http\Controllers\API\V{$apiVersion}\\{$controller}";

    while (! method_exists(app($namespace), $method)) {
        if ($apiVersion === 0) {
            abort(404);
        }

        $apiVersion--;
        $namespace = "App\Http\Controllers\API\V{$apiVersion}\\{$controller}";
    }

    return [$namespace, $method];
}
Enter fullscreen mode Exit fullscreen mode

And used it as bellow:
api.php

Route::get('{version}/posts', handleVersion('PostController', 'index'));
Route::get('{version}/posts/{id}', handleVersion('PostController', 'show'));
Enter fullscreen mode Exit fullscreen mode

Now, I don't have to define plenty of pointless routes anymore!


Solution #3: A combination of my two solutions

I liked the idea of extending the previous version controllers; hence, I decided to combine the two solutions! If you're going to create empty controllers, you can get rid of the while() loop in the handleVersion().


Last words!

I truly enjoyed facing this challenge, and this post is about two ideas I'd thought about. If you have a better idea, please feel free to share it with us in the comments.

Top comments (4)

Collapse
 
stoyt profile image
Mahdi Akbari

Thanks a million for sharing your insights on coding! 🚀 Your post not only inspired me but also added valuable knowledge to the community. Grateful to have friends like you who contribute to our learning journey. Keep rocking! 💻🙌

Collapse
 
muhammadmp profile image
Muhammad MP

Thank you for reading!

Collapse
 
salehisaac profile image
saleh

thank you so much for sharing your ideas , i personally enjoyed it a lot !! 🙌

Collapse
 
muhammadmp profile image
Muhammad MP

Thanks dear Saleh!