Micronaut-core: Support for different methods of Versioning APIs

Created on 3 Dec 2018  ·  22Comments  ·  Source: micronaut-projects/micronaut-core

There are a number of situations where we would need to version APIs. I was unable to find support for 3 versioning options in Micronaut.

Request Parameter versioning

Implementations in Spring Boot is Shown below

  @GetMapping(value = "/student/param", params = "version=1")
  public StudentV1 paramV1() {
    return new StudentV1("Bob Charlie");
  }

  @GetMapping(value = "/student/param", params = "version=2")
  public StudentV2 paramV2() {
    return new StudentV2(new Name("Bob", "Charlie"));
  }

(Custom) Headers versioning

Use a Request Header to differentiate the versions.

Examples

Sent GET to http://localhost:8080/person/header using a header
headers=[X-API-VERSION=1]

Sent GET to http://localhost:8080/person/header using a header
headers=[X-API-VERSION=2]

Spring Boot Implementations are shown below:

  @GetMapping(value = "/student/header", headers = "X-API-VERSION=1")
  public StudentV1 headerV1() {
    return new StudentV1("Bob Charlie");
  }

  @GetMapping(value = "/student/header", headers = "X-API-VERSION=2")
  public StudentV2 headerV2() {
    return new StudentV2(new Name("Bob", "Charlie"));
  }

Media type versioning (a.k.a “content negotiation” or “accept header”)

The last versioning approach is to use the Accept Header in the request.

Sending GET to http://localhost:8080/person/produces using this header
Accept=application/vnd.company.app-v1+json
Sending GET to http://localhost:8080/person/produces using this header
Accept=application/vnd.company.app-v2+json

Corresponding code in Spring Boot is below

  @GetMapping(value = "/student/produces", produces = "application/vnd.company.app-v1+json")
  public StudentV1 producesV1() {
    return new StudentV1("Bob Charlie");
  }

  @GetMapping(value = "/student/produces", produces = "application/vnd.company.app-v2+json")
  public StudentV2 producesV2() {
    return new StudentV2(new Name("Bob", "Charlie"));
  }

http-server enhancement

All 22 comments

Currently, the executableMethod for Route is chosen only via the uri match.
For version we will face the route duplication problem:
if (uriRoutes.size() > 1) { throw new DuplicateRouteException(requestPath, uriRoutes);
But should Micronaut be generic in route choosing behaviour?
1) Micronaut should include for route matching also request params / headers / producing value?
2) @Version annotation for this specific case?
- @Version(header="X-API-VERSION, value = "1")
- @Version(param="version", value="1")
- @Version(produces="application/vdn.company.app-v1+json")

I'll be glad to help with implementation via PR

Awesome. That sounds like a great solution.

Additional Comment
When I look at https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/GetMapping.html, it has support for request matching using costumes header as well.

PRs welcome. Thanks

Okay, will start on it as soon as possible.
So which of my proposals (Spring behavior VS dedicated @Version annotation) is more intuitive?
The @Version maybe is more apparent for developers?

+1 for @Version

+1 for @Version. Describes Intent more clearly.

A little status report for versioning:

  • [x] @Version annotation
  • [x] Choosing a route by version
  • [ ] Add route versioning to RouteBuilder
  • [ ] Informative exceptions with information about existing API route versions
  • [x] Add @Version support for @Client introductions (to send specific headers, params etc)
  • [x] Compile time validation for duplicating versions on same routes - should Micronaut have? I think it should.

@BogdanOros The micronaut side really shouldn't support any specific solution imo. You can define the contract for retrieving a version from the request and inject a collection of those beans. Then loop through those beans until one produces a version for you to compare with.

That way Micronaut will continue to perform as well as is does today for those who aren't using versioning and for those that do, they simply need to register a bean that returns the version using whatever strategy they wish. They can register multiple beans if they want to support multiple strategies.

You could perhaps add a couple beans that have to be specifically enabled via config that look at a header, content type, or perhaps the url. The version annotation should probably just accept a value @Version("3"), the beans would be responsible for comparing that to the request.

@jameskleeh, it seems reasonable, I will refactor my existing solution to remove extra configurations in the @Version annotation and move the resolving to a configurable set of beans.
It will not break the existing behavior .

@BogdanOros yes those seem like good ideas

@graemerocher, should @Version with @Client reuse the router versioning configuration (I mean use the same names for params/headers)?
Or maybe the header/parameter names should be configured the same way as cache configurations:
for each clientId a separate list of names.

Probably a default and then per client config makes sense

Was this fixed or closed?

It is fixed, here is a link to the snapshot documentation:
https://docs.micronaut.io/snapshot/guide/index.html#apiVersioning

Cool. Thanks.

I have tried this, but keep getting error in the test:

io.micronaut.http.client.exceptions.HttpClientResponseException: More than 1 route matched the incoming request. The following routes matched /greeter/hello: GET - /greeter/hello, GET - /greeter/hello

Same happens when tested with curl.

micronaut:
    router:
        versioning:
            header:
                enabled: true
                names:
                    - 'X-API-VERSION'
                    - 'Accept-Version'
    application:
        name: demo-app

Controller looks like this:

package demo.controllers;

import io.micronaut.core.version.annotation.Version;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.reactivex.Single;

@Controller("/greeter")
public class HelloWorld {

  @Version("1")
  @Get("/hello")
  public Single<String> sayHelloV1() {
    return Single.just("Hello World V1");
  }

  @Version("2")
  @Get("/hello")
  public Single<String> sayHelloV2() {
    return Single.just("Hello World V2");
  }
}

and test client:

package demo.controllers;

import io.micronaut.core.version.annotation.Version;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.Single;

@Version("1")
@Client("/greeter")
public interface HelloWorldClient {

  @Get("/hello")
  Single<String> sayHelloV1();

  @Version("2")
  @Get("/hello")
  Single<String> sayHelloV2();
}

Test itself is simple too:

package demo.controllers;

import io.micronaut.test.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import javax.inject.Inject;

import static org.junit.jupiter.api.Assertions.assertEquals;

@MicronautTest
class HelloWorldTest {

  @Inject HelloWorldClient helloWorldClient;

  @Test
  void testHello() {
    assertEquals("Hello World V1", helloWorldClient.sayHelloV1().blockingGet());
  }
}

1.1.0.M2 version
java 11

@dekstroza, The configuration in the documentation is a bit inconsistent:

micronaut:
    router:
        versioning:
            enabled: true
            header:
                enabled: true
                names:
                    - 'X-API-VERSION'
                    - 'Accept-Version'
    application:
        name: demo-app

Versioning should be enabled in application.yml

micronaut:
    router:
        versioning:
            enabled: true

or application.properties

micronaut.router.versioning.enabled=true

I will create a PR as soon as possible to fix the configuration example.

@BogdanOros That was it. Thanks for the help. Sent pull request with the update.

Does it also supports version in URL?

Example api.corp.com/v1/products

@raderio You can write your own resolver to pull the version from the URL

@raderio You can write your own resolver to pull the version from the URL

@jameskleeh you have to bind the actual version-specific controller to the actual api-specific uri/route /v1/products, isn't it? so any additional version-resolver is just pointless in this case, or am i missing something?

@hotzen The route could be /{version}/products and then the method could do what it wants to do based on the version, but you could do that without this specific version support. The @Version annotation for that use case wouldn't make much sense you're right.

Was this page helpful?
0 / 5 - 0 ratings