Implement versioning for REST API resources

Edit on GitHub

In the course of the development of your REST APIs, you may need to change the data contracts of API resources. However, you can also have clients that rely on the existing contracts. To preserve backward compatibility for such clients, we recommend implementing a versioning system for REST API resources. In this case, each resource version has its own contract in terms of data, and various clients can request the exact resource versions they are designed for.

Resources that are provided by Spryker out of the box do not have a version. When developing resources, only new resources or attributes are added without removing anything, which ensures backward compatibility for all clients. If necessary, you can implement versioning for built-in resources as well as extend the corresponding resource module on your project level.

To implement versioning for a REST API resource, follow these steps:

1. Implement ResourceVersionableInterface

To add versioning to a resource, the route plugin of the resource module needs to implement not only ResourceRoutePluginInterface, but also \Spryker\Glue\GlueApplicationExtension\Dependency\Plugin\ResourceVersionableInterface. The latter exposes a method called getVersion that lets you set the resource version.

For more information on route plugins, see the Resource routing section in Glue Infrastructure.

Consider the following implementation of a route plugin:

CustomerRestorePasswordResourceRoutePlugin.php
<?php

namespace Spryker\Glue\CustomersRestApi\Plugin;

use Generated\Shared\Transfer\RestCustomerRestorePasswordAttributesTransfer;
use Generated\Shared\Transfer\RestVersionTransfer;
use Spryker\Glue\CustomersRestApi\CustomersRestApiConfig;
use Spryker\Glue\GlueApplicationExtension\Dependency\Plugin\ResourceRouteCollectionInterface;
use Spryker\Glue\GlueApplicationExtension\Dependency\Plugin\ResourceRoutePluginInterface;
use Spryker\Glue\GlueApplicationExtension\Dependency\Plugin\ResourceVersionableInterface;
use Spryker\Glue\Kernel\AbstractPlugin;

/**
 * @method \Spryker\Glue\CustomersRestApi\CustomersRestApiFactory getFactory()
 */
class CustomerRestorePasswordResourceRoutePlugin extends AbstractPlugin implements ResourceRoutePluginInterface, ResourceVersionableInterface
{
    public function configure(ResourceRouteCollectionInterface $resourceRouteCollection): ResourceRouteCollectionInterface
    {
        $resourceRouteCollection
            ->addPatch('patch', false);

        return $resourceRouteCollection;
    }

    public function getResourceType(): string
    {
        return CustomersRestApiConfig::RESOURCE_CUSTOMER_RESTORE_PASSWORD;
    }

    public function getController(): string
    {
        return CustomersRestApiConfig::CONTROLLER_CUSTOMER_RESTORE_PASSWORD;
    }

    public function getResourceAttributesClassName(): string
    {
        return RestCustomerRestorePasswordAttributesTransfer::class;
    }

    public function getVersion(): RestVersionTransfer
    {
        return (new RestVersionTransfer())
            ->setMajor(2)
            ->setMinor(0);
    }
}

As you can see, the CustomerRestorePasswordResourceRoutePlugin class implements the ResourceRoutePluginInterface and ResourceVersionableInterface interfaces. The resource supports only one HTTP method: PATCH. Also, the getVersion function sets version 2.0 for the resource:

Code sample:

class CustomerRestorePasswordResourceRoutePlugin extends AbstractPlugin implements ResourceRoutePluginInterface, ResourceVersionableInterface
{
    ...
    public function getVersion(): RestVersionTransfer
    {
        return (new RestVersionTransfer())
            ->setMajor(2)
            ->setMinor(0);
    }
}

Set both the major and minor versions of a resource; otherwise, requests to this resource fail.

2. Query specific resource version

After implementing a specific resource version, you can query the resource specifying the version you need. Send a PATCH request to the /customer-restore-password endpoint that now has version 2.0. The payload is as follows:

Code sample:

PATCH /customer-restore-password

{
  "data": {
    "type": "customer-restore-password",
    "attributes": {
        "email":"jdoe@example.com"
   }
}

If \Spryker\Glue\GlueApplication\GlueApplicationConfig::getPathVersionResolving is set to false, specify the exact version you need, in the HTTP header of your request:

Content-Type: application/vnd.api+json; version=2.0

If getPathVersionResolving is set to true, then you have to set some value in \Pyz\Glue\GlueApplication\GlueApplicationConfig::getPathVersionPrefix, “v” in our examples, and then your resource path should look like this: PATCH /v2.0/customer-restore-password

In the preceding example, version 2.0 is specified. If you repeat the request with such headers, you receive a valid response with resource version 2.0. However, if you specify a non-existent version, for example, 3.0, the request fail.

Content-Type: application/vnd.api+json; version=3.0

In this case, the endpoint responds with the 404 Not Found error.

Here’s a version matching rule-set:

PHP version:

(new RestVersionTransfer())
            ->setMajor(A)
            ->setMinor(B);

Then use version

In the header: Content-Type: application/vnd.api+json; version=A.B

In the path: /vA.B

PHP version:

(new RestVersionTransfer())
            ->setMajor(A);

Then, use version

In the header: Content-Type: application/vnd.api+json; version=A

In the path: /vA

There’s no fall-back to the latest minor, only exact match of version is used.

If a version is not specified, the latest available version is returned.

In order to call the the latest version of the resource, do not specify version in the request.

3. Add more versions

To implement a new version, you can create a new route plugin in your module—for example, to support version 3.0, you can use the following code in your plugin:

Code sample:

class CustomerRestorePasswordResourceRouteVersion3Plugin extends AbstractPlugin implements ResourceRoutePluginInterface, ResourceVersionableInterface
{
    ...
    public function getVersion(): RestVersionTransfer
    {
        return (new RestVersionTransfer())
            ->setMajor(3)
            ->setMinor(0);
    }
}

In the new plugin, you can configure routing differently. You can use a different controller class or use a different transfer for the resource attributes. See the following example:

Code sample:

...
public function getResourceAttributesClassName(): string
{
    return RestCustomerRestorePasswordVersion3AttributesTransfer::class;
}
...

After implementing the plugin and the required functionality, you register the new plugin in Pyz\Glue\GlueApplication\GlueApplicationDependencyProvider:

Code sample:

class GlueApplicationDependencyProvider extends SprykerGlueApplicationDependencyProvider
{
    /**
     * @return \Spryker\Glue\GlueApplicationExtension\Dependency\Plugin\ResourceRoutePluginInterface[]
     */
    protected function getResourceRoutePlugins(): array
    {
        return [
            ...
            new CustomerRestorePasswordResourceRouteVersion3Plugin(),
        ];
    }

You can add as many plugins as required by your project needs.