Microservices API Documentation with Springdoc OpenAPI


I have already written about documentation for microservices more than two years ago in my article Microservices API Documentation with Swagger2. In that case I used project SpringFox for auto-generating Swagger documentation for Spring Boot applications. Since that time the SpringFox library is not being actively developed by the maintainers – the latest version has been released on June 2018. Currently, the most important problems with this library are a lack of support for OpenAPI in the newest version 3, and for Spring reactive APIs built using WebFlux. All these features are implemented by Springdoc OpenAPI library. Therefore, it may threaten as a replacement for SpringFox as Swagger and OpenAPI 3 generation tool for Spring Boot applications.

Example

As a code example in this article we will use a typical microservices architecture built with Spring Cloud. It consists of Spring Cloud Config Server, Eureka discovery, and Spring Cloud Gateway as API gateway. We also have three microservices, which expose REST API and are hidden gateway for external client. Each of them are exposing OpenAPI documentation, that may accessed on the gateway using Swagger UI. The repository with source code is available on GitHub: https://github.com/piomin/sample-spring-microservices-new.git. This repository has been used as an example in some other article, so it contains a code not only for Springdoc library demo. The following picture shows an architecture of our system.

microservices-api-documentation-springdoc-openapi.png

Implementation

The first good news related to Springdoc OpenAPI library is that it may exists together with SpringFox library without any conflicts. This may simplify your migration into a new tool if anybody is using your Swagger documentation, for example for code generation of contract tests. To enable Springdoc for standard Spring MVC based application you need to include the following dependency into Maven pom.xml.

<dependency>
	<groupId>org.springdoc</groupId>
	<artifactId>springdoc-openapi-webmvc-core</artifactId>
	<version>1.2.32</version>
</dependency>

Each of our Spring Boot microservices is built on top of Spring MVC, and provides endpoints for standard synchronous REST communication. However, the API gateway, which is built of top of Spring Cloud Gateway uses Netty as an embedded server and is based on reactive Spring WebFlux. It is also providing Swagger UI for accessing documentation exposed by all the microservices, so it must include library that enables UI. The following two libraries must be included to enable Springdoc support for reactive application based on Spring WebFlux.

<dependency>
	<groupId>org.springdoc</groupId>
	<artifactId>springdoc-openapi-webflux-core</artifactId>
	<version>1.2.31</version>
</dependency>
<dependency>
	<groupId>org.springdoc</groupId>
	<artifactId>springdoc-openapi-webflux-ui</artifactId>
	<version>1.2.31</version>
</dependency>

We can customize a default behaviour of this library by setting properties in Spring Boot configuration file or using @Beans. For example, we don’t want to generate OpenAPI manifests for all HTTP endpoints exposed by the application like Spring specific endpoint, so we may define a base package property for scanning as shown below. In our source code example each application YAML configuration file is located inside config-service module.

springdoc:
  packagesToScan: pl.piomin.services.department

Here’s the main class of employee-service. We use @OpenAPIDefinition annotation to define description for the application displayed on the Swagger site. As you see we can still have SpringFox enabled with @EnableSwagger2.

@SpringBootApplication
@EnableDiscoveryClient
@EnableSwagger2
@OpenAPIDefinition(info =
	@Info(title = "Employee API", version = "1.0", description = "Documentation Employee API v1.0")
)
public class EmployeeApplication {

	public static void main(String[] args) {
		SpringApplication.run(EmployeeApplication.class, args);
	}

}

Once you start every microservice it will expose endpoint /v3/api-docs. We can customize that context by using property springdoc.api-docs.path in Spring configuration file. Since it is not required we may proceed to the implementation on the Spring Cloud Gateway. Springdoc doesn’t provide the similar class to SpringFox SwaggerResource, that has been used for exposing multiple APIs from different microservices in the previous article. Fortunately, there is a grouping mechanism that allows to split OpenAPI definitions into different groups with a given name. To use it we need to declare a list of GroupOpenAPI beans.
Here’s the fragment of code inside gateway-service responsible for creating list of OpenAPI resources handled by the gateway. First, we to get all defined routes for services using RouteDefinitionLocator bean. Then we are fetching the id of each route and set it as a group name. As a result we have multiple OpenAPI resources under path /v3/api-docs/{SERVICE_NAME}, for example /v3/api-docs/employee.

@Autowired
RouteDefinitionLocator locator;

@Bean
public List<GroupedOpenApi> apis() {
	List<GroupedOpenApi> groups = new ArrayList<>();
	List<RouteDefinition> definitions = locator.getRouteDefinitions().collectList().block();
	definitions.stream().filter(routeDefinition -> routeDefinition.getId().matches(".*-service")).forEach(routeDefinition -> {
		String name = routeDefinition.getId().replaceAll("-service", "");
		GroupedOpenApi.builder().pathsToMatch("/" + name + "/**").setGroup(name).build();
	});
	return groups;
}

The API path like /v3/api-docs/{SERVICE_NAME} is not exactly we want to achieve, because our routing to the downstream services is based on the service name fetched from discovery. So if you call address like http://localhost:8060/employee/** it is automatically load balanced between all registered instances of employee-service. Here’s the routes definition in gateway-service configuration.

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
      - id: employee-service
        uri: lb://employee-service
        predicates:
        - Path=/employee/**
        filters:
        - RewritePath=/employee/(?<path>.*), /$\{path}
      - id: department-service
        uri: lb://department-service
        predicates:
        - Path=/department/**
        filters:
        - RewritePath=/department/(?<path>.*), /$\{path}
      - id: organization-service
        uri: lb://organization-service
        predicates:
        - Path=/organization/**
        filters:
        - RewritePath=/organization/(?<path>.*), /$\{path}

Since, Springdoc doesn’t allow to customize the default behaviour of grouping mechanism to change the generated paths, we need to provide some workaround. My proposition is just to add a new route definition inside gateway configuration dedicated for Open API path handling. It rewrites path /v3/api-docs/{SERVICE_NAME} into /{SERVICE_NAME}/v3/api-docs, which is handled by the another routes responsible for interacting with Eureka discovery.

  - id: openapi
	uri: http://localhost:${server.port}
	predicates:
	- Path=/v3/api-docs/**
	filters:
	- RewritePath=/v3/api-docs/(?<path>.*), /$\{path}/v3/api-docs

Testing

To test our sample simple we need to run all microservice, config server, discovery and gateway. While microservices are available under dynamically generated port, config server is available under 8888, discovery under 8061, and gateway under 8060. We can access each microservice by calling http://localhost:8060/{SERVICE_PATH}/**, for example http://localhost:8060/employee/**. The Swagger UI is available under address http://localhost:8060/swagger-ui.html. Before let’s take a look on Eureka after running all required Spring Boot applications.

microservice-api-documentation-with-springdoc-openapi

After accessing Swagger UI exposed on the gateway you may see that we can choose between all three microservices registered in the discovery. This is exactly what we wanted to achieve.

microservice-api-documentation-with-springdoc-openapi-ui

Conclusion

Springdoc OpenAPI is compatible with OpenAPI 3, and support Spring WebFlux, while SpringFox is not. Therefore, it seems that the choice is obvious especially if you are using reactive APIs or Spring Cloud Gateway. In this article I demonstrated you how to use Springdoc in microservices architecture with gateway pattern.

21 thoughts on “Microservices API Documentation with Springdoc OpenAPI

  1. Hi Piotr , i am following your blogs as it is and implemented same in my application but am getting error

    http://localhost:8989/order/ (8989 is my api gateway port & order is my microservice)

    when am trying this , am getting 404 , apart from that when i opened my swagger html

    http://localhost:8989/swagger-ui.html

    it is giving me below error

    Failed to load API definition.

    Errors

    Fetch errorNot Found /v3/api-docs/order
    Fetch errorNot Found /v3/api-docs/order

    Like

  2. Hello Bro, Working with microservices and I was using rest doc for monolithic solutions. Is it possible to use spring rest doc also for micro services

    Like

  3. I think this is a great way of bundeling all API-Docs in one swagger. Makes handling with third-party-developers a lot easier.

    Is it possible to refresh the Groups when i start new microservices? With this configuration I need to restart the gateway everytime…
    I also changed that each service only exposes its API-DOCs and not the swagger-ui…

    Like

    1. Hi. Yes, if you are adding a new application you need to restart the gateway. But in that case how would you like to add a new route definition without restart? You can do it using actuator endpoints for that – they allows to add a new route dynamically. If you prefer such solution you can annotate this bean List with @RefreshScope i force refreshing using also actuactor endpoint.

      Like

  4. With an eventlistener on RefreshRoutesEvent.class the gateway will refresh the swagger-ui when new services come up or go down.

    Just reuse the apis()-Method and call a SwaggerUiConfigProperties.setSwaggerUrls(new HashSet()); before that.

    Maybe there is an easier way to do this?

    Liked by 1 person

    1. I think I have already answered in your previous question. If you are starting a new instance of application it is supported by default. If you are adding a new application you need to add a new route definition. That’s the case.

      Like

      1. Well i forgot to say that I use the autodiscovery-mode for my services. But the EventListener I described here helped to solve my problem.

        Like

    1. Hi. Thanks for this comment. I took a look on it one more time, and you are right – I’m returning the empty list of GroupedOpenApi in apis() method – of course it is oversight, because I forgottten to call add method. But the truth is that it works even without this add, probably because of that line inside GroupedOpenApi constructor: SwaggerUiConfigProperties.addGroup(this.group). Interesting. I changed it in the code and call add method on the list, but of course it is not changing abything. Probably I could just call GroupedOpenApi.builder()…build() in the loop without returning anything and it will also work.

      Like

      1. Hey Piotr,

        thanks for the article first of all. I can’t get it to work on my end though. Do you have the code somehwere publicly available? I feel like I’m missing a piece.

        As for me, the groups are not recognized (the dropdown is not showing up in the UI), although they’re being built up by the Bean method.
        A group shows up if I provide a single Bean of GroupedOpenAPI though.. I’m using the latest version of spring-open docs (1.3.9). Maybe that changed something.

        Like

      2. It really is that constructor call. As for versions 1.3 they removed the addGroup call from the GroupedOpenAPI.
        This works then together with the rewriting:

        @Bean
        public void apis(RouteDefinitionLocator locator, SwaggerUiConfigProperties swaggerUiConfig) {
        List definitions = locator.getRouteDefinitions().collectList().block();
        definitions.stream()
        .filter(routeDefinition -> routeDefinition.getId().matches(“.*-service”))
        .forEach(routeDefinition -> {
        String name = routeDefinition.getId().replaceAll(“-service”, “”);
        swaggerUiConfig.addGroup(name);
        });
        }

        Cheers

        Liked by 3 people

  5. Hello,
    Very interesting article.
    I have a API gateway written with Zuul (verison 1). Do you think that we could implement a similar solution with Zuul ?
    My API gateway doesn’t expose all of the micro-services end-points. Only part of it (the other ones are internal). Do you know a solution to filter the micro-services end-points in the documentation to match the ones exposed by the API Gateway ?

    Like

  6. Hello, thank you for the article. Helped me a lot!
    I do have one question/problem…

    When I start the gateway and go to the swagger url everything works fine and I do see all swaggers from my routed services…however I can not call any of the service through this swagger becase it goes here: http://localhost:45003http,http://server.xyz/service/endpoint

    One more question…how can i get api-docs to contain all services api-docs…now I only have failback endpoint in my gateway…did I not configure something correctly?

    I am using the same versions as you in this example.

    Thank you.

    Like

  7. How Would you handle if the “springdoc.api-docs.path” has been over-ridden by department microservice service to something like “department-application/api-document” , instead of simply using “v3/api-docs”

    Like

    1. I would change that address on gateway from /v3/api-docs to another. I’m assuming you have the same path across all your microservices. If every microservice would use different path, the situation become more complicated. In that case you might use discovery server for that, where you would have to register the address of your Swagger endpoint.

      Like

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.