Versioning psr-7 apps

Hey y’all. I’ve written a middleware package intended to manage API versioning. It’s inspired somewhat by zfcampus/zf-versioning but for Expressive and other PSR-7 apps. Before I go further, I’m looking for feedback. I started writing a test suite but chose that as a stopping point as I wasn’t immediately sure how best to test it.

The theory of operation is in the README. In general terms, the middleware is wired into your pipeline before routing. It specifies a default version and detects version in request URI path as well as in Accept header similar to the methodology of zfcampus/zf-versioning. You can then define different routes for different versions of the “same” resources. https://github.com/marcguyer/version-middleware

1 Like

I’m realizing that using different routes for different versions can be quite problematic. For example, if you’re referencing a route by name in your app, then you don’t really know which version (name) of the route to get. Further, any configuration based on route name can be an issue (e.g., HAL config - Zend\Expressive\Hal\Metadata\RouteBasedResourceMetadata). I did it with different routes initially in an effort to keep all of your versioning config nice and neat in your route config. Unfortunately that seems to cause bigger problems.

As an example, in a HAL response, I think the _links should not include any versioning information. If a link includes the version in the path, it’s the HAL response itself that dictates which version of the resource (or related resource) being linked. I think the client should decide which version of the resource is requested via the link (by using the media header method of versioning). This brings the question of how “standardized” HAL client libraries handle this… or do they?

For example, here’s how this works now in the above referenced middleware pkg:

// in your middleware pipeline before routing:
$app->pipe('/api', MG\Versioning\VersionMiddleware::class);

… now we know which version is requested via media type in the Accept header or via the URI path

// in your route config:
$app->get('/api/book', Handler\BookHandler::class, 'api.book');
$app->get('/api/v2/book', V2Handler\BookHandler::class, 'api.v2.book');
$app->get('/api/author', Handler\AuthorHandler::class, 'api.author');
$app->get('/api/v2/author', V2Handler\AuthorHandler::class, 'api.v2.author');

Using the HAL response example, if you request V2 of a Book resource using the Accept header versioning method, the Author resource is in the _links with the version in the path. Here’s a visual:

{
    "id": "123",
    "title": "The Best Book",
    "_links": {
        "self": {
            "href": "/api/v2/book/123"
        },
        "author" : {
            "href" : "/api/v2/book/123/author"
        }
    }
}

But what if you want V1 of the Author (or v3 for that matter)?

I guess the question is, is it valuable to be able to request different versions of resources via the same client app? If you could, then updating your client app to use v3 instead of v2 of an API doesn’t have to be complete. Instead, you could migrate your client app over time… In other words, keep everything on v2 but only request the v3 of Author because you want to take advantage of only that new version of Author and not anything else new. For that matter, you also wouldn’t need to write the entire V3 of an API before release but only parts of it over time…

@marcguyer How do you refer to the API from the module point of view? Is \Api a single module (only one ConfigProvider class) or each version is a separate module (there is a ConfigProvider class for each version) ?

You could do it either way. The example implementation in the repo shows a V2 directory in the Api module but that isn’t a requirement. The middleware simply introspects the request for version information then adds the version as a path component to the request URI. With that, you can use routing, or path segregated middleware via routing, or whatever via routing to direct the request down the pipeline of your choice.