Skip to content

Latest commit

 

History

History
107 lines (91 loc) · 4.35 KB

rest-versioning.md

File metadata and controls

107 lines (91 loc) · 4.35 KB

Versioning

Your APIs should be versioned. Unlike Web applications which you have full control on both client side and server side code, for APIs you usually do not have control of the client code that consumes the APIs. Therefore, backward compatibility (BC) of the APIs should be maintained whenever possible, and if some BC-breaking changes must be introduced to the APIs, you should bump up the version number. You may refer to Semantic Versioning for more information about designing the version numbers of your APIs.

Regarding how to implement API versioning, a common practice is to embed the version number in the API URLs. For example, http://example.com/v1/users stands for /users API of version 1. Another method of API versioning which gains momentum recently is to put version numbers in the HTTP request headers, typically through the Accept header, like the following:

// via a parameter
Accept: application/json; version=v1
// via a vendor content type
Accept: application/vnd.company.myapp-v1+json

Both methods have pros and cons, and there are a lot of debates about them. Below we describe a practical strategy of API versioning that is kind of a mix of these two methods:

  • Put each major version of API implementation in a separate module whose ID is the major version number (e.g. v1, v2). Naturally, the API URLs will contain major version numbers.
  • Within each major version (and thus within the corresponding module), use the Accept HTTP request header to determine the minor version number and write conditional code to respond to the minor versions accordingly.

For each module serving a major version, it should include the resource classes and the controller classes serving for that specific version. To better separate code responsibility, you may keep a common set of base resource and controller classes, and subclass them in each individual version module. Within the subclasses, implement the concrete code such as Model::fields().

Your code may be organized like the following:

api/
    common/
        controllers/
            UserController.php
            PostController.php
        models/
            User.php
            Post.php
    modules/
        v1/
            controllers/
                UserController.php
                PostController.php
            models/
                User.php
                Post.php
        v2/
            controllers/
                UserController.php
                PostController.php
            models/
                User.php
                Post.php

Your application configuration would look like:

return [
    'modules' => [
        'v1' => [
            'basePath' => '@app/modules/v1',
        ],
        'v2' => [
            'basePath' => '@app/modules/v2',
        ],
    ],
    'components' => [
        'urlManager' => [
            'enablePrettyUrl' => true,
            'enableStrictParsing' => true,
            'showScriptName' => false,
            'rules' => [
                ['class' => 'yii\rest\UrlRule', 'controller' => ['v1/user', 'v1/post']],
                ['class' => 'yii\rest\UrlRule', 'controller' => ['v2/user', 'v2/post']],
            ],
        ],
    ],
];

As a result, http://example.com/v1/users will return the list of users in version 1, while http://example.com/v2/users will return version 2 users.

Using modules, code for different major versions can be well isolated. And it is still possible to reuse code across modules via common base classes and other shared classes.

To deal with minor version numbers, you may take advantage of the content negotiation feature provided by the [[yii\filters\ContentNegotiator|contentNegotiator]] behavior. The contentNegotiator behavior will set the [[yii\web\Response::acceptParams]] property when it determines which content type to support.

For example, if a request is sent with the HTTP header Accept: application/json; version=v1, after content negotiation, [[yii\web\Response::acceptParams]] will contain the value ['version' => 'v1'].

Based on the version information in acceptParams, you may write conditional code in places such as actions, resource classes, serializers, etc.

Since minor versions require maintaining backward compatibility, hopefully there are not much version checks in your code. Otherwise, chances are that you may need to create a new major version.