API version control in header

Published 2 years ago by tjoskar

Hi,

I'm working on a api application and wants to version controll it by header information eg, api-version: 3.

I know its possible to include this information in the url with Route::group like:

Route::group(['prefix' => 'v3'], function() {
    Route::resource('user', 'v3\UserController');
});

But I don't want to have this information in the URL.

I suppose it's possible to do something like this:

// Routes
Route::resource('user', 'UserController');

// UserController.php

private $userRepository;

function __construct() {
    parent::__construct();
    $apiVersion = Request::header('api-version');
    $this->userRepository = App::make('userRepository-' . $apiVersion);
}

public function show($id)
{
    return $this->userRepository->get_resource($id);
}

However, this method do not scaling very well.

What I realy wants is something like this:

class VersionMiddleware {

    public function handle($request, Closure $next)
    {
        $apiVersion = $request->header('api-version');
        if ($apiVersion === 3)
        {
            // This will make laravel to use the correct controller in
            // namespace /v3
            return useVersionThree($request);
        }
        elseif ($apiVersion === 2)
        ...
        elseif ($apiVersion === 1)
        ...

        App::abort(400);
    }

}

// The version-middleware instancinate the correct version of UserController
Route::get('user/{id}', ['middleware' => 'version', 'uses' => 'UserController@show']);

Is that possible?

Best Answer (As Selected By tjoskar)
Ruffles

Create a class ApiVersion and have a method checkVersion($version) and a few additional methods if needed, and put the logic there and just inject an object into the controllers to see which version you'll get. I think it will be cleaner this way.

tjoskar

I just found that one can check the header informtion in the route file:

$version = Request::header('api-version');
if ($version === '3') {
    Route::resource('user', 'v3\UserController');
} elseif ($version === '2') {
    Route::resource('user', 'v2\UserController');
}
....

Is there a better way?

xingfucoder

I don't know if you could use the { } within the second parameter of resource method but maybe you could use as follow:

$version = Request::header('api-version');
if (isset($version)) {
    Route::resource('user', 'v{$version}\UserController');
}

Or if you use a default api-version this option (in this example the v2 as default):

$version = Request::header('api-version');
(isset($version)) ? Route::resource('user', 'v{$version}\UserController'); : Route::resource('user', 'v2\UserController');

If you need to validate the $version variable you could add some validation.

Ruffles
Ruffles
2 years ago (282,470 XP)

Create a class ApiVersion and have a method checkVersion($version) and a few additional methods if needed, and put the logic there and just inject an object into the controllers to see which version you'll get. I think it will be cleaner this way.

yayuj
yayuj
2 years ago (14,325 XP)

@Ruffles - Yeah, that is it. I was going to post something similar to that. - Remember @tjoskar: Find What Is Varying and Encapsulate It (a.k.a - Encapsulate What Varies) Principle. - Every time that you have a new version you will have to open your code and edit, adding a new version, with this you would be breaking the Open/Closed Principle. - And also, it's going to make your code harder to test.

yayuj
yayuj
2 years ago (14,325 XP)

@tjoskar - You bother posting how you solved? I think that this would help others. Thanks in advance.

tjoskar

@yayuj, sure.

This may not be the prettiest solution but it's good enough for now.

Route::group(['middleware' => 'api-version'], function()
{
    // Movie
    Route::get('movie/{id}', '{api-namespace}\MovieController@show');
    Route::put('movie/{id}', '{api-namespace}\MovieController@update');
});
// ApiVersionMiddleware.php
public function handle($request, Closure $next)
{
    $route = $request->route();
    $actions = $route->getAction();

    $requestedApiVersion = ApiVersion::get($request);
    if (!ApiVersion::isValid($requestedApiVersion)) {
        throw up_some_error;
    }

    $apiNamespace = ApiVersion::getNamespace($requestedApiVersion);

    $actions['uses'] = str_replace(
        '{api-namespace}',
        $apiNamespace,
        $actions['uses']
    );

    $route->setAction($actions);

    return $next($request);
}
// ApiVersion.php
class ApiVersion {

    private static $valid_api_versions = [
        3 => 'v3'
    ];

    /**
     * Resolve the requested api version.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return integer
     */
    public static function get($request) {
        return intval($request->header('api-version'));
    }

    /**
     * Determines if a version is valid or not
     *
     * @param integer $apiVersion
     * @return bool
     */
    public static function isValid($apiVersion) {
        return in_array(
            $apiVersion,
            array_keys(self::$valid_api_versions)
        );
    }

    /**
     * Resolve namespace for a api version
     *
     * @param integer $apiVersion
     * @return string
     */
    public static function getNamespace($apiVersion)
    {
        if (!self::isValid($apiVersion)) {
            return null;
        }

        return self::$valid_api_versions[$apiVersion];
    }

}
JeroenVanOort

And why exactly would you want to do version control in the headers at all?

tjoskar

@JeroenVanOort , I believe it's best practice. The URL should uniquely identify a resource, the versionnumber has nothing to do with that. There is an interesting discussion on the topic here: http://stackoverflow.com/questions/389169/best-practices-for-api-versioning

Ruffles
Ruffles
2 years ago (282,470 XP)

Having an API version in headers is one of the ways to handle the versioning. It is considered to be more restful than having it in the URI/URL (or subdomain).

Pros

  • Simple for API consumers (if they know about headers)

  • Keeps URLs the same

  • Technically a bit more RESTful than putting version in the URI

Cons

  • Cache systems can get confused

  • API developers can get confused (if they do not know about headers)

From the book Build APIs You Won't Hate by Phil Sturgeon

bgarrison25

Would just like to say that I implemented this today and it works amazingly. Quick and easy header versioning for my api. Thanks for the help!

torkil

@bgarrison25 @tjoskar Have you been able to make this work in Laravel 5.2? In my tests I just get this error:

Class App\Http\Controllers\{api-namespace}\PhotosController does not exist

Seems like the loading of the controller is done before the middleware has the chance to search/replace the API namespace placeholder.

tosh001

Having same issues with Laravel 5.2

Any ideas?

bgarrison25

My solution in 5.2 is....it simply doesn't work. I figured out why it didn't....offered a couple of solutions to the laravel guys which summarily were dismissed and was told to do something else. Other priorities came up and now I simply don't use it till I can look back at it. If your curious about the issue I put in and the suggestions etc please look here: https://github.com/laravel/framework/issues/11496

danhunsaker

Anyone thought of trying to use Route groups for this?

$version = Request::header('api-version', '3');
Route::group(['namespace' => "v{$version}"], function() {
    Route::resource('user', 'UserController');
});

Haven't tested this, yet, but if it works, you just update the default API version (3 in this example) as needed. Should get around the issue in 5.2 as well, since the namespace isn't even specified until the header is checked (and you can, of course, do more extensive logic on the header value as needed).

ChristophAust

Hast anyone tried danhunsaker's solution yet? I also think adding the version to the URL is simply unnecessary though IT is easier to implementiert.

Please sign in or create an account to participate in this conversation.