What is a recommended API versioning in Tapir?

Hi!

I have a couple of questions regarding supported API versioning ways in Tapir.

  1. Is there any way in tapir to version endpoints through the request body field?
    Let’s say I have such a request:
    Request version 1: {data: String, version: Int = 1}
    Request version 2: {data: [String], version: Int = 2}
    So the two requests are not backwards compatible, yet can be handled through the same HTTP endpoint route. Is there any convenient way to express such versioning in Tapir?
    To be honest, I don’t even know how this should be displayed with OpenAPI documentation - is this approach just “wrong”?

  2. Can Tapir support versioning through the custom header? (similarly to content negotiation, I assume)

  3. And finally - what is a recommended way of versioning API. I would assume that the URI path parameters, as it works out of the box with Tapir, can be documented nicely (just a different endpoint) and is the most flexible approach (as you can also change other path parameters in the next API versions).

Any other thoughts or considerations? :slight_smile:

By the way, maybe a documentation page on API versioning would be a nice addition to Tapir docs (as I couldn’t find anything in this regard) :slight_smile:

Best regards,
Krzysztof

I think OpenAPI can provide some guidance here. In OpenAPI, an endpoint is uniquely identified by the method + path template combination. So if we’d like to have both v1 and v2 endpoints in the same documentation page, a different path is required. By the way, endpoints for which method+paths overlap can be detected in a test using the EndpointVerifier. Using endpoint.tag, endpoints can be grouped into categories (e.g. corresponding to the version).

Alternatively, versioning can also be performed using a query parameter or a header value. In this situation, you’ll probably want to deploy two documentation sites, one for v1, another one for v2. You can still have a single list of ServerEndpoints to deploy, and partition them using an attribute / checking if an endpoint contains an input, to pass the proper endpoints the OpenAPI interpreter (you’d call the interpreter once for each version).

Differentiating the endpoints can be done using e.g. a fixed header: .in(header("API-Version", "v1")). This way, only requests where the value of the header matches the given one will be allowed. If you need a default (fall-back) endpoint, e.g. if we assume that v2 is the default version is it’s not provided explicitly, simply include an endpoint with the fixed-header input after the one with the fixed-header requirement.

One nice feature of tapir is that it’s quite easy to create endpoints which have the same definition across versions, without duplicating code, e.g.:

def myEndpoint(version: String) = 
  endpoint.in("api" / version / "user").in(...)

What I would not recommend is doing versioning based on a field in the body. In general, it’s better to perform routing using information which is available upfront for a request (method, path, query parameters, headers), so that any middleware components can perform their task without a need to read & parse the body. This is especially important if the body can be large and should be streamed (instead of reading it into memory entirely), but also complicates the implementation when it comes to avoiding having to parse the body twice (once for routing, second time for the server logic).

However, if the endpoints are otherwise identical, and differ only in the body, I think a oneOf input with variants corresponding to the body versions should work as well. When decoding a request, the variant which decodes successfully is chosen - so the task of checking if the version matches would be delegated to the JSON decoder. You could start with a oneOf[Either[OldBody, NewBody]] and provide variants for Left/Right cases. In this approach, the OpenAPI docs would include a single endpoint, with a oneOf body specification.

I don’t think there’s a “recommended” way of doing API versioning, just different tradeoffs when it comes to structuring the application’s code and presenting the documentation. Hope this helps :slight_smile:

1 Like