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:?https://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 :)

1 年

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>

回复

要查看或添加评论,请登录

Jonathan Manera的更多文章

  • Service Discovery Patterns with Netflix Eureka

    Service Discovery Patterns with Netflix Eureka

    Modern distributed systems assign network locations dynamically, so you don’t know what IP address or port each service…

  • Spring Cloud Circuit Breaker Resilience4j

    Spring Cloud Circuit Breaker Resilience4j

    A distributed system, which comprises many services interacting to achieve business goals, is prone to failures in the…

  • Building Microservices with Spring Boot and?gRPC

    Building Microservices with Spring Boot and?gRPC

    gRPC Remote Procedure Call is a binary message based protocol for writing cross-language applications. As part of this…

  • Aspect Oriented Programming for?Babies

    Aspect Oriented Programming for?Babies

    Aspect Oriented Programming is a powerful tool that enables a clean modularization of “crosscutting” concerns of…

  • 7 Best Practices for Java API Documentation

    7 Best Practices for Java API Documentation

    The Javadoc Tool simplifies the documentation process in Java, allowing developers to document their code seamlessly as…

  • The Holy Trinity: JUnit5, Mockito, and Instancio

    The Holy Trinity: JUnit5, Mockito, and Instancio

    Unit testing is a software testing technique where individual components of an application (units) are tested in…

  • Code Quality Analysis with Sonar

    Code Quality Analysis with Sonar

    SonarQube is an open-source platform for continuous code quality inspection. This tool provides a detailed analysis of…

  • Mapping Bidirectional Object Associations using MapStruct

    Mapping Bidirectional Object Associations using MapStruct

    In object-oriented programming, an association is a relationship between two entities that defines how two objects…

  • Testing Microservices with Testcontainers

    Testing Microservices with Testcontainers

    When writing integration tests for Spring Boot applications, we usually need to access external resources such as…

  • 10 Meditations for Creative Professionals

    10 Meditations for Creative Professionals

    Sometimes, in our day-to-day life, we feel that something is not quite right. We find it hard to be as productive as we…

社区洞察

其他会员也浏览了