1. Overview

In this tutorial, we’ll talk about how we can leverage the Micronaut Framework capabilities to implement evolving REST APIs.

In the ever-evolving landscape of software development projects, sometimes purely based on REST APIs, maintaining backward compatibility while introducing new features and improvements is a crucial challenge. One of the fundamental aspects of achieving this is that we must implement a technique called  API versioning.

We’ll explore the concept of API versioning in the context of Micronaut, a popular microservices framework for building efficient and scalable applications. We’ll delve into the importance of API versioning, different strategies to implement it in Micronaut, and best practices to ensure smooth version transitions.

2. Importance of API Versioning

API versioning is the practice of managing and evolving an application programming interface (API) to allow clients to continue using the existing version while also adopting newer versions when they are ready. It is essential for several reasons.

2.1. Maintaining Compatibility

As our application evolves, we may need to change our APIs to introduce new features, fix bugs, or improve performance. However, it’s also necessary to ensure that such changes do not disrupt existing clients. API versioning enables us to introduce changes while maintaining compatibility with previous versions.

2.2. Allowing Gradual Adoption

The clients of our APIs may have different timelines for adopting new versions. Therefore, providing multiple versions of our APIs allows clients to update their code with reasonable adoption time, reducing the risk of breaking their applications.

2.3. Facilitating Collaboration

It also facilitates collaboration between development teams. When different teams work on other parts of a system, or third-party developers integrate with our APIs, versioning allows each team to have a stable interface, even as changes are made elsewhere.

3. API Versioning Strategies in Micronaut

Micronaut offers different strategies for implementing API versioning. We aren’t going to discuss which one is the best one as it pretty much depends on the use case and the reality of the project. Nonetheless, we can discuss the specifics of each one of them.

3.1. URI Versioning

In the URI versioning, the version of the API is defined in the URI. This approach makes it clear which version of the API the client is consuming. Although the URL may not be as user-friendly as it can be, it clarifies to the client which version it uses.

@Controller("/v1/sheep/count")
public class SheepCountControllerV1 {

    @Get(
        uri = "{?max}",
        consumes = {"application/json"},
        produces = {"application/json"}
    )
    Flowable<String> countV1(@Nullable Integer max) {
        // implementation

Although it may not be practical, our clients are sure about the version used, which means transparency. From the development side, it’s easy to implement any business rules specific to a particular version, meaning a good level of isolation. However, one could argue it’s intrusive as the URI may change frequently. It may require hard coding from the client side and adds extra context not precisely specific to the resource.

3.2. Header Versioning

Another option to implement API versioning is to leverage the header to route the request to the right controller. Here is an example:

@Controller("/dog")
public class DogCountController {

    @Get(value = "/count", produces = {"application/json"})
    @Version("1")
    public Flowable<String> countV1(@QueryValue("max") @Nullable Integer max) {
        // logic
    }

    @Get(value = "/count", produces = {"application/json"})
    @Version("2")
    public Flowable<String> countV2(@QueryValue("max") @NonNull Integer max) {
        // logic  
    }
}

By simply using the @Version annotation, Microunat can redirect the request to the proper handler based on the header’s value. However, we still need to change some configurations, as we see next:

micronaut:
  router:
    versioning:
      enabled: true
      default-version: 2
      header:
        enabled: true
        names:
          - 'X-API-VERSION'

Now we just enabled versioning via Micronaut, defining version 2 as the default one in case no version is specified. The strategy used will be header-based, and the header X-API-VERSION will be used to determine the version. Actually, this is the default header Micronaut looks at, so in this case, there wouldn’t be a need to define it, but in the case we want to use another header, we could specify it like this.

Using headers, the URI remains clear and concise, we can preserve backward compatibility, the URI is purely resource-based, and it allows for more flexibility in the evolution of the API. However, it’s less intuitive and visible. The client has to be aware of the version he wants to use, and it’s a bit more error-prone. There is another similar strategy that consists of the use of MineTypes for this.

3.3. Parameter Versioning

This strategy leverages query parameters in the URI to do the routing. In terms of implementation in Mircronaut, it’s exactly like the previous strategy. We just need to add the @Version in our controllers. However, we need to change some properties:

micronaut:
  router:
    versioning:
      enabled: true
      default-version: 2
      parameter:
        enabled: true
        names: 'v,api-version'

With this, the client only needs to pass either v or api-version as query parameters in each request, and Micronat will take care of the routing for us.

When using this strategy once again, the URI will have no resource-related information, although less than having to change the URI itself. Besides that, the versioning is less explicit and more error-prone as well. This is not RESTful, and documentation is needed to avoid confusion. However, we can also appreciate the simplicity of the solution.

3.4. Custom Versioning

Micronaut also offers a custom way to implement API versioning where we can implement a versioning route resolver and show Micronaut which version to use. The implementation is simple, and we only need to implement an interface, like in the following example:

@Singleton
@Requires(property = "my.router.versioning.enabled", value = "true")
public class CustomVersionResolver implements RequestVersionResolver {

    @Inject
    @Value("${micronaut.router.versioning.default-version}")
    private String defaultVersion;

    @Override
    public Optional<String> resolve(HttpRequest<?> request) {
        var apiKey = Optional.ofNullable(request.getHeaders().get("api-key"));

        if (apiKey.isPresent() && !apiKey.get().isEmpty()) {
            return Optional.of(Integer.parseInt(apiKey.get())  % 2 == 0 ? "2" : "1");
        }

        return Optional.of(defaultVersion);
    }

}

Here, we can see how we can leverage any information in the request to implement a routing strategy, and Micronaut does the rest. This is powerful, but we need to be cautious because this may lead to poor and less intuitive forms of implementing versioning.

4. Conclusion

In this article, we saw how API versioning can be implemented using Micronaut. Moreover, we also discussed the different strategies existent for applying this technique and some of their nuances.

It’s also clear that selecting the right strategy involves weighing the importance of URI cleanliness, explicitness of versioning, ease of use, backward compatibility, RESTful adherence, and the specific needs of clients consuming the API. The optimal approach depends on our project’s unique requirements and constraints.

As usual, all code samples used in this article are available over on GitHub.