📜 ⬆️ ⬇️

Using Accept Header for API Versioning

I explored various options for versioning the REST API. Most of the sources I found say almost the same thing. To version any resource on the Internet, you do not need to change the URL. The web is not versioned, and changing the URL tells the client that there are more than 1 resource. But in reality there are not several resources, they are simply different representations of the same thing. Of course, there are times when you need to change the URL, for example, when the functionality changes. In this particular case, the reason for the change is the fact that it is no longer the same resource.

But on the other hand, and perhaps even more important, you should always try to make sure that the changes will be backward compatible. You need to design the architecture very well so that the clients using your API do not change their code. A well-designed API can save you from a big, very big headache.

Using Accept Header


Of course, there are always cases where breaking backward compatibility is necessary in order to move forward. In this case, versioning becomes important. The method that I found seems to be the most logical one; it offers to request a specific version of the API using the Accept header. In this post, I’ll show a possible implementation using symfony routing.

GET /jobs HTTP/1.1 Host: api.example.com Accept: application/vnd.example.api+json;version=2 

vnd. the part is dictated by the requirements of rfc4288-3.2 , and is used to specify the types of data provided by the supplier. In vnd.* data type can be provided to IANA and registered as an official type. In theory, you could just use application/json and add a version parameter, but since the json standard does not allow such liberties, this would not be entirely correct use.
')
A great example of a slightly different implementation is the Github API . They just decided to use the latest version of the API, if the client specifies a specific one. But there is a good argument for always requiring a version indication; when you default to the latest version of api, then if it is updated, all those who for some reason have not foreseen the likelihood that the API will be changed will suffer from the incompatibility that has appeared.

But how?


So now we get to the implementation. Suppose you need to redirect requests from different controllers based on the Accept header. To achieve this, you need to accept Accept and create routing rules that can use this information.

Especially for this, I created a sandbox with a very simple API that shows how you can send requests based on the requested version. The first version (1) of this API simply returns a static list of tasks:

 class ApiController extends Controller { public function getJobsAction() { $jobs = $this->get('wjzijderveld_api.job_manager')->getJobs(); return new Response(json_encode($jobs)); } public function getJobAction($id) { $job = $this->get('wjzijderveld_api.job_manager')->getJob($id); return new Response(json_encode($job)); } } 

In version 2, I sorted these tasks alphabetically, depending on the title.

 class Api2Controller extends ApiController { public function getJobsAction() { $jobs = $this->get('wjzijderveld_api.job_manager')->getJobs(); usort($jobs, function ($a, $b) { return strcmp($a['title'], $b['title']); }); return new Response(json_encode($jobs)); } } 

For simplicity, I created a BaseBundle with custom routing. To be able to switch to the desired controller, we must determine the requested version. For this, I expanded the standard classes Router and RequestContext , I defined new classes in parameters.yml.

 router.class: Wjzijderveld\BaseBundle\Router\ApiRouter router.options.matcher_class: Wjzijderveld\BaseBundle\Router\VersionMatcher router.request_context.class: Wjzijderveld\BaseBundle\Router\RequestContext 


In my router I get the Accept header and expose the version in RequestContext . Thanks to symfony, for being such a cake and has an AcceptHeader implementation:

 public function matchRequest(Request $request) { $acceptHeader = AcceptHeader::fromString($request->headers->get('Accept'))->get($this->acceptHeader); // .. if (null === ($version = $acceptHeader->getAttribute('version'))) { return $this->match($request->getPathInfo()); } $this->getContext()->setApiVersion($version); return $this->match($request->getPathInfo()); } 

From version 2.5 you can use expression language to create new routing conditions.

 # Version 1 api1_get_jobs: path: "/jobs" defaults: _controller: "WjzijderveldApiBundle:Api:getJobs" condition: "context.getApiVersion() === '1'" # Version 2 api2_get_jobs: path: "/jobs" defaults: _controller: "WjzijderveldApiBundle:Api2:getJobs" condition: "context.getApiVersion() === '2'" 

Results


Managing the versions of your API can be quite difficult, especially if you want to do it right! It was just my opinion on a part of a big problem called “Creating an API”, but what is your opinion?

Resources used


urthen.imtqy.com : Part Two: How to architect a version-less API
pivotallabs.com : API Versioning
troyhunt.com : Your API versioning is wrong, which is why I decided to do it 3 different wrong ways

Source: https://habr.com/ru/post/240817/


All Articles