How to add Swagger to Quarkus

How to add Swagger to Quarkus

OpenAPI or Swagger specification is the de facto standard for creating REST APIs. Almost every developer expects to see some Swagger documentation when works with APIs or doing integration. Apps made with Quarkus are not exceptions either. In this article, I go through how to add Swagger to Quarkus apps.

In the past two articles, we’ve covered how to create REST APIs and how to use MySQL with Quarkus. We implemented CRUD functionality for the User resource that persists data to a MySQL database. The next step is to create API documentation for it.

Fortunately, the process is rather straightforward in Quarkus thanks to smallrye-openapi library. So let’s begin.

Add Open API dependency

The very first step is to add the quarkus-smallrye-openapi to the project. This library is built on top of the Eclipse MicroProfile OpenAPI specification. It generates OpenAPI dynamic schema and also has built-in Swagger UI.

<dependencies>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-openapi</artifactId>
  </dependency>
</dependencies>

Now if you run the code and go to /openapi, you should be able to download the specification in YML format. To change the path, add this configuration to application.properties,

quarkus.smallrye-openapi.path=/swagger

Creating SwaggerConfig class

We don’t necessarily need to have this file, but it’s highly recommended. It contains annotations that provide information about the APIs purpose, maintainer, licensing, etc. Note that to make the annotations working, the SwaggerConfig.java file should extend Application from javax.ws.rs.core.Application. That’s odd but necessary. Since the class is for the configuration only, it doesn’t require any implementation. An example of it would be like this,

package com.madadipouya.quarkus.example.config;

import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition;
import org.eclipse.microprofile.openapi.annotations.info.Contact;
import org.eclipse.microprofile.openapi.annotations.info.Info;
import org.eclipse.microprofile.openapi.annotations.info.License;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;

import javax.ws.rs.core.Application;

@OpenAPIDefinition(
        tags = {
                @Tag(name = "user", description = "User operations."),
        },
        info = @Info(
                title = "User API with Quarkus",
                version = "0.0.1",
                contact = @Contact(
                        name = "Kasra Madadipouya",
                        url = "http://geekyhacker.com/contact",
                        email = "[email protected]"),
                license = @License(
                        name = "MIT",
                        url = "https://opensource.org/licenses/MIT"))
)
public class SwaggerConfig extends Application {

}

Enabling Swagger UI

Let’s enable the Swagger UI before proceeding further. For that open application.properties file and enable Swagger UI as follows,

quarkus.swagger-ui.always-include=true

After that, Swagger UI is accessible at /swagger-ui.

To provide a custom path, just override this parameter,

quarkus.swagger-ui.path=/swagger-ui.html

Document the APIs

For many developers, plain Swagger documentation is good enough. Though, in complex projects where many business terminologies are used, having more documented APIs are highly appreciated. One can achieve that by applying a set of annotations on controllers. In our case, we can enrich the User APIs with details such as status codes, description of APIs, and required fields as follows,

package com.madadipouya.quarkus.example.controller;

import com.madadipouya.quarkus.example.entity.User;
import com.madadipouya.quarkus.example.exception.UserNotFoundException;
import com.madadipouya.quarkus.example.exceptionhandler.ExceptionHandler;
import com.madadipouya.quarkus.example.service.UserService;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.media.Content;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.jboss.resteasy.annotations.jaxrs.PathParam;

import javax.inject.Inject;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;

@Path("/v1/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserController {

    private final UserService userService;

    @Inject
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GET
    @Operation(summary = "Gets users", description = "Lists all available users")
    @APIResponses(value = @APIResponse(responseCode = "200", description = "Success",
                    content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))))
    public List<User> getUsers() {
        return userService.getAllUsers();
    }

    @GET
    @Path("/{id}")
    @Operation(summary = "Gets a user", description = "Retrieves a user by id")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "Success",
                    content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
            @APIResponse(responseCode = "404", description="User not found",
                    content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionHandler.ErrorResponseBody.class)))
    })
    public User getUser(@PathParam("id") int id) throws UserNotFoundException {
        return userService.getUserById(id);
    }

    @POST
    @Operation(summary = "Adds a user", description = "Creates a user and persists into database")
    @APIResponses(value = @APIResponse(responseCode = "200", description = "Success",
                    content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))))
    public User createUser(@Valid UserDto userDto) {
        return userService.saveUser(userDto.toUser());
    }

    @PUT
    @Path("/{id}")
    @Operation(summary = "Updates a user", description = "Updates an existing user by id")
    @APIResponses(value = {
            @APIResponse(responseCode = "200", description = "Success",
                    content = @Content(mediaType = "application/json", schema = @Schema(implementation = User.class))),
            @APIResponse(responseCode = "404", description="User not found",
                    content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionHandler.ErrorResponseBody.class)))
    })
    public User updateUser(@PathParam("id") int id, @Valid UserDto userDto) throws UserNotFoundException {
        return userService.updateUser(id, userDto.toUser());
    }

    @DELETE
    @Path("/{id}")
    @Operation(summary = "Deletes a user", description = "Deletes a user by id")
    @APIResponses(value = {
            @APIResponse(responseCode = "204", description = "Success"),
            @APIResponse(responseCode = "404", description="User not found",
                    content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionHandler.ErrorResponseBody.class)))
    })
    public Response deleteUser(@PathParam("id") int id) throws UserNotFoundException {
        userService.deleteUser(id);
        return Response.status(Response.Status.NO_CONTENT).build();
    }

    @Schema(name="UserDTO", description="User representation to create")
    public static class UserDto {

        @NotBlank
        @Schema(title="User given name", required = true)
        private String firstName;

        @NotBlank
        @Schema(title="User surname", required = true)
        private String lastName;

        @Min(value = 1, message = "The value must be more than 0")
        @Max(value = 200, message = "The value must be less than 200")
        @Schema(title="User age between 1 to 200", required = true)
        private int age;

        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public User toUser() {
            User user = new User();
            user.setFirstName(firstName);
            user.setLastName(lastName);
            user.setAge(age);
            return user;
        }
    }
}

Check here to see a list of existing annotations.

Fully functional example

As usual, you can find the fully functional example on my GitHub page at the link below,

https://github.com/kasramp/quarkus-rest-example

Thanks for reading and hope you’ve learned how to add Swagger to your Quarkus app.

Inline/featured images credits