API-First Design with OpenAPI Generator
Images by OpenAPI Generator and Spring

API-First Design with OpenAPI Generator

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 design 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.

openapi: 3.0.3	info: title: Order Service API version: 1.0.0	paths: /orders: post: summary: creates an order operationId: createOrder requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/OrderRequest' responses: 201: description: Order created. 400: description: Malformed syntax of the request params. content: application/problem+json: schema: $ref: '#/components/schemas/ErrorDetails' /orders/{id}: get: summary: Returns the order information operationId: getOrder parameters: - in: path name: id allowEmptyValue: false description: Order guid. required: true schema: type: string pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$' example: 'e06bf865-312c-4e2a-85c3-cc20db4a4c1d' responses: 200: description: Order details. content: application/json: schema: $ref: '#/components/schemas/OrderResponse' 400: description: Malformed syntax of the request params. content: application/problem+json: schema: $ref: '#/components/schemas/ErrorDetails' 404: description: The requested resource doesn't exists or was removed.	components: schemas: OrderRequest: description: Order placed from a consumer. type: object properties: notes: description: Notes from consumer. type: string maxLength: 1000 example: add mayonnaise orderItems: type: array items: $ref: '#/components/schemas/OrderItemRequest' consumer: $ref: '#/components/schemas/ConsumerRequest' OrderItemRequest: description: Item in the order. type: object properties: name: description: Item name. type: string minLength: 3 maxLength: 32 example: Royale with Cheese quantity: description: Item quantity. type: integer minimum: 1 maximum: 100 example: 2 ConsumerRequest: description: Consumer information. type: object properties: name: description: Consumer name. type: string minLength: 5 maxLength: 64 example: Vincent Vega address: description: Consumer address. type: string minLength: 5 maxLength: 64 example: 1234 Big Kahuna St, Los Angeles CA phone: description: Consumer phone number. type: string minLength: 10 maxLength: 12 pattern: ^[+]?[0-9]*$ example: +1223334444 OrderResponse: description: Order placed from a consumer. type: object properties: id: description: Order guid. type: string example: 'e06bf865-312c-4e2a-85c3-cc20db4a4c1d' state: description: Order state. type: string enum: [ 'APPROVAL_PENDING','APPROVED','REJECTED','CANCEL_PENDING','CANCELLED','REVISION_PENDING' ] example: 'APPROVAL_PENDING' notes: description: Notes from consumer. type: string example: add mayonnaise orderItems: type: array items: $ref: '#/components/schemas/OrderItemResponse' consumer: $ref: '#/components/schemas/ConsumerResponse' OrderItemResponse: description: Item in the Order. type: object properties: name: description: Item name. type: string example: Royale with Cheese quantity: description: Item quantity. type: integer example: 2 ConsumerResponse: description: Consumer information. type: object properties: name: description: Consumer name. type: string example: Vincent Vega address: description: Consumer address. type: string example: 123 Big Kahuna St, Los Angeles CA phone: description: Consumer phone number. type: string example: +1223334444 ErrorDetails: type: object properties: code: description: Application error code. type: integer nullable: false example: 400 detail: description: A short, summary of the problem type. type: string nullable: false example: 'size must be between 10 and 12.' field: description: The field that caused the error. type: string example: 'consumer.phone' value: description: The value of the field that caused the error. type: object example: null location: description: The location of the field that caused the error. type: string enum: [ 'BODY','PATH','QUERY','HEADER' ] example: 'BODY' 

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.

No alt text provided for this image

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.

Adrian Vrabie

and I'm also curious :)

2y

also add these dependencies: <dependency> <groupId>javax.annotation</groupId> <artifactId>javax.annotation-api</artifactId> <version>${javax.annotation.javax.annotation-api.version}</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>${javax.servlet.javax.servlet-api.version}</version> <scope>provided</scope> </dependency>

To view or add a comment, sign in

More articles by Jonathan Manera

Others also viewed

Explore content categories