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
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
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
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