API-First Design with OpenAPI Generator

Keep your OpenAPI files up-to-date with the OpenAPI Generator library for Java

Jonathan Manera
4 min readMar 28, 2023
Images by OpenAPI Generator and Spring

APIs are contracts between services and their clients. These contracts are usually documented using an interface description language (IDL). Nowadays, the most popular IDL for RESTful interfaces is the OpenAPI Specification.

Unfortunately, even in small projects, it often happens that some of the interfaces don’t match what’s actually implemented. To solve this problem, we can adopt an “API-First Design” approach.

“An API-first approach involves developing APIs that are consistent and reusable, which can be accomplished by using an API description language to establish a contract for how the API is supposed to behave. Establishing a contract involves spending more time thinking about the design of an API. It also often involves additional planning and collaboration with the stakeholders providing feedback on the design of an API before any code is written.”

Swagger

This approach can be summarized in three simple steps:

  • First step: Define and desing the API interface.
  • Second step: Review the definition with clients and API stakeholders.
  • Third step: Implement the service.

In this article, we’ll look at how OpenAPI Generator can help us enforce this approach when building Spring Boot applications.

Setting Up the Project

The OpenAPI Generator Plugin

A Maven plugin supports the OpenAPI generator project.

Add the following plugin in the pom.xml file:

<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<!-- RELEASE_VERSION -->
<version>${openapi-generator-maven-plugin.version}</version>
<!-- /RELEASE_VERSION -->
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<!-- specify the openapi description file -->
<inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec>
<!-- target to generate java server code -->
<generatorName>spring</generatorName>
<!-- pass any necessary config options -->
<configOptions>
<documentationProvider>springdoc</documentationProvider>
<modelPackage>org.company.model</modelPackage>
<apiPackage>org.company.api</apiPackage>
<openApiNullable>false</openApiNullable>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>

You need to configure the inputSpec tag value with the full path to your OpenAPI description file.

All the plugin configuration parameters are contained in the configOptions tag. Make sure you set the modelPackage and apiPackage tags with the package names in your project.

Dependencies

Models and APIs are generated using SpringDoc, as well as Bean Validation 2.0 (JSR 380).

In your pom.xml file, include the following dependencies:

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- Bean Validation API support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>

First Step: Define and Design

Our plan is to create two endpoints for the Orders Service API:

  • POST /orders — creates an order.
  • GET /orders/:id — returns the order information.

We use OpenAPI Specification 3.0.3 in this tutorial. At the time I’m writing this article, most Specification 3.0 features are supported by OpenAPI Generator. However, Specification 3.1 will be supported shortly.

Below is an example of an openapi.yml file that you can use as a reference for creating your OpenAPI files.

Second Step: Review with Stakeholders

Stakeholders need to validate the API definition once it has been created. To generate the API stub, compile the application with the command below.

mvn clean compile

Next, run the application.

mvn spring-boot:run

You can access the Swagger UI by opening the following URL in your browser: http://localhost:8080/swagger-ui/index.html.

Third Step: Implement

As a next step, let’s implement the service in accordance with the definition.

OrderController

package org.company.rs;

import org.company.model.*;
import org.company.api.OrdersApi;

@RestController
public class OrderController implements OrdersApi {

private final OrderService service;
private final OrderControllerMapper mapper;

public OrderController(OrderService service, OrderControllerMapper mapper) {
this.service = service;
this.mapper = mapper;
}

@Override
public ResponseEntity<Void> createOrder(OrderRequest orderRequest) {
final UUID id = service.createOrder(
mapper.orderRequestToOrder(orderRequest)
);

URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}").buildAndExpand(id)
.toUri();

return ResponseEntity.created(location).build();
}

@Override
public ResponseEntity<OrderResponse> getOrder(String id) {
Order order = service.getOrder(
UUID.fromString(id)
);
return ResponseEntity.ok(
mapper.orderToOrderResponse(order)
);
}
}

Here, we have created the OrdersController class that implements the generated org.company.api.OrdersApi interface.

Additionally, we have imported org.company.model.*, which includes all generated request and response objects.

ExceptionController

As mentioned earlier, OpenAPI Generator supports Bean Validation. Hence, we can handle exceptions thrown by these validations and send descriptive error responses to clients.

package org.company.rs;

import org.company.rs.model.ErrorDetails;

@ControllerAdvice
public class ExceptionController {

@ExceptionHandler(BindException.class)
ResponseEntity<List<ErrorDetails>> handleBindException(BindException ex) {
List<ErrorDetails> errors = ex.getBindingResult().getFieldErrors().stream()
.map(fieldError -> {
ErrorDetails errorDetails = new ErrorDetails();
errorDetails.setCode(400);
errorDetails.setDetail(fieldError.getDefaultMessage());
errorDetails.setField(fieldError.getField());
errorDetails.setValue(fieldError.getRejectedValue());
errorDetails.setLocation(ErrorDetails.LocationEnum.BODY);
return errorDetails;
}).toList();

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}

@ExceptionHandler(ConstraintViolationException.class)
ResponseEntity<List<ErrorDetails>> handleConstraintViolationException(ConstraintViolationException ex) {
List<ErrorDetails> errors = ex.getConstraintViolations().stream()
.map(constraintViolation -> {
ErrorDetails errorDetails = new ErrorDetails();
errorDetails.setCode(400);
errorDetails.setDetail(constraintViolation.getMessage());
errorDetails.setField(constraintViolation.getPropertyPath().toString());
errorDetails.setValue(constraintViolation.getInvalidValue());
errorDetails.setLocation(ErrorDetails.LocationEnum.PATH);
return errorDetails;
}).toList();

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}

@ControllerAdvice is an annotation that allows us to handle exceptions in one place across the entire application.

To handle errors on the client side, a handler method annotated with @ExceptionHandler is defined for BindException.class and ConstraintValidationException.class.

Thanks for reading. I hope this was helpful!

The example code is available on GitHub.

--

--

Jonathan Manera

If you wish to make a Java app from scratch, you must first invent the universe.