API-First with Spring WebFlux and OpenAPI Generator

In this post, we’ll explore how to generate codes using the swagger-codegen from the defined OpenAPI file in a spring reactive application.

· What we need
· 
Overview
∘ 
What is API-First?
∘ 
Why API-First?
· 
OpenAPI Specification
· 
Spring Webflux Application
· 
Test the API
∘ 
Import OpenAPI definition in insomnia
∘ 
Test
· 
Conclusion
· 
References

Teams and organizations continue to embrace an API-first philosophy. — 2022 State of the API Report

In recent years, APIs have become the building blocks of software for companies and organizations. These companies use them to offer new services for their internal or external partners.
Therefore, it is necessary to provide service contracts, with documentation that constitutes an agreement between the differents stakeholders.

What we need

  • Spring Boot 3
  • Maven 3.8.+
  • Java 17
  • Postman or Insomnia

Overview

What is API-First?

API-first development is a development model in which applications are conceptualized and built by composing internal or external services delivered through APIs. This approach involves designing every API around a contract written in an API description language like OpenAPI.

Why API-First?

  • Increasing developer productivity: Developers do not have to wait for updates to an API to be released before moving on to the next API. Teams can mock APIs and test API dependencies based on the established API definition.
  • Reduces the risk of failure: API-First reduces the risk of failure by ensuring that APIs are reliable, consistent, and easy for developers to use.
  • Reduces development cost: APIs specifications can be reused on many different projects. When a development team wants to build a new app, they don’t have to start from scratch which is time-consuming and costly. API-First design also allows most problems to be solved before any code is even written which helps prevent problems when it is time to integrate APIs with applications.
  • Increases speed: API-First makes it possible to add new services and technologies to applications without having to re-architect the entire system. It allows businesses to optimize their speed to market by reusing existing software.
  • Improve developer Experience: API-First ensures that developers have positive experiences using your APIs. Well-designed, well-documented, consistent APIs provide positive developer experiences because it’s easier to reuse code and onboard developers, and it reduces the learning curve.

OpenAPI Specification

The OpenAPI Specification (OAS) defines a standard, programming language-agnostic interface description for HTTP APIs, which allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code, additional documentation, or inspection of network traffic.

The first thing to do is to define our API specification. In this story, we will create a library API, which will expose service contracts for CRUD operations.

An OpenAPI document that conforms to the OpenAPI Specification is itself a JSON object, which may be represented either in JSON or YAML format. For this story, we will use the YAML format.

Our YAML specifications file:

openapi: 3.0.3
info:
  title: Library API - OpenAPI 3.0
  description: |-
    This is a sample Library Server based on the OpenAPI 3.0 specification.  You can find out more about
    Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the Library, we've switched to the design first approach!
    You can now help us improve the API whether it's by making changes to the definition itself or to the code.
    That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
    
    Some useful links:
    - [The library repository](https://github.com/anicetkeric/spring-reactive-openapi-codegen)
    - [The source API definition for the library](https://github.com/anicetkeric/spring-reactive-openapi-codegen/blob/main/src/main/resources/openapi/yaml/library-openapi.yaml)
  termsOfService: http://swagger.io/terms/
  contact:
    email: boottechnologies.ci@gmail.com
  license:
    name: Apache 2.0
    url: http://www.apache.org/licenses/LICENSE-2.0.html
  version: '1.0.11'
externalDocs:
  description: Find out more about Swagger
  url: http://swagger.io
servers:
  - url: http://localhost:8080/api/v1
    description: Development server
  - url: http://localhost:8081/api/v1
    description: Staging server
  - url: http://localhost:8082/api/v1
    description: Production server
tags:
  - name: Book
    description: Operations about book
    externalDocs:
      description: Find out more
      url: http://swagger.io
  - name: Author
    description: Operations about author
    externalDocs:
      description: Find out more
      url: http://swagger.io

paths:
  /book:
    get:
      tags:
        - Book
      summary: Get all list book
      operationId: getAllBook
      responses:
        '200':
          description: return successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: You are not authorized to view the resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Accessing the resource you were trying to reach is forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The resource you were trying to reach is not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '405':
          description: Method Not Allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: 'something went wrong! '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
        - Book
      summary: Create a new book.
      description: Create new book in library
      operationId: createBook
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Book'
        required: true
      responses:
        '201':
          description: Created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '400':
          description: 'something went wrong! '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: You are not authorized to view the resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Accessing the resource you were trying to reach is forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The resource you were trying to reach is not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '405':
          description: Method Not Allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /book/{id}:
    get:
      tags:
        - Book
      summary: get the "id" author.
      description: Returns a single record
      operationId: getOneBook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "500":
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "405":
          description: Method Not Allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "400":
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: entity by id not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "200":
          description: Successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
    put:
      tags:
        - Book
      summary: Updates an existing book.
      operationId: updateBook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Book'
        required: true
      responses:
        "500":
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "405":
          description: Validation exception
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "400":
          description: Invalid ID supplied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
    delete:
      tags:
        - Book
      summary: Delete an existing book
      operationId: deleteBook
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            description: book id
      responses:
        "500":
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "405":
          description: Validation exception
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "400":
          description: Invalid ID supplied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
  /author:
    get:
      tags:
        - Author
      summary: get all the authors.
      operationId: getAllAuthor
      responses:
        '200':
          description: return successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: You are not authorized to view the resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Accessing the resource you were trying to reach is forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The resource you were trying to reach is not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '405':
          description: Method Not Allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: 'something went wrong! '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
    post:
      tags:
        - Author
      summary: Create a new author.
      description: Create new Author in library.
      operationId: createAuthor
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Author'
        required: true
      responses:
        201:
          description: Created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '400':
          description: 'something went wrong! '
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: You are not authorized to view the resource
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Accessing the resource you were trying to reach is forbidden
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: The resource you were trying to reach is not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '405':
          description: Method Not Allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /author/{id}:
    get:
      tags:
        - Author
      summary: get the "id" author.
      description: Returns a single record for Author
      operationId: getOneAuthor
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        "500":
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "405":
          description: Method Not Allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "400":
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: entity by id not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "200":
          description: Successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
    put:
      tags:
        - Author
      summary: Updates an existing author.
      operationId: updateAuthor
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Author'
        required: true
      responses:
        "500":
          description: Internal Server Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "405":
          description: Validation exception
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "400":
          description: Invalid ID supplied
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "404":
          description: Contact not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        "200":
          description: successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'

components:
  schemas:
    ErrorResponse:
      type: object
      properties:
        code:
          type: string
        message:
          type: string
        description:
          type: string
        errors:
          type: object
    SuccessResponse:
      type: object
      properties:
        data:
          type: object
        message:
          type: string
    Book:
      required:
        - title
        - isbn
      type: object
      properties:
        id:
          type: string
        title:
          maxLength: 255
          minLength: 1
          type: string
          description: book title
        isbn:
          maxLength: 13
          minLength: 10
          type: string
          description: International Standard Book Number
        page:
          type: integer
          description: number of page
        description:
          type: string
          description: book summary
        price:
          type: number
          format: float
          description: book price

    Author:
      required:
        - lastname
      type: object
      properties:
        id:
          type: string
        firstname:
          maxLength: 255
          minLength: 1
          type: string
          description: firstname of author.
        lastname:
          maxLength: 255
          minLength: 1
          type: string
          description: lastname of author.

The Swagger Editor offers an easy way to get started with the OpenAPI Specification (formerly known as Swagger) as well as the AsyncAPI specification, with support for Swagger 2.0, OpenAPI 3.0, and AsyncAPI 2.* versions.

The OpenAPI specification’s header contains some metadata about the API.

openapi: 3.0.3
info:
  title: Library API - OpenAPI 3.0
  description: |-
    This is a sample Library Server based on the OpenAPI 3.0 specification.  You can find out more about
    Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the Library, we've switched to the design first approach!
    You can now help us improve the API whether it's by making changes to the definition itself or to the code.
    That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
    
    Some useful links:
    - [The library repository](https://github.com/anicetkeric/spring-reactive-openapi-codegen)
    - [The source API definition for the library](https://github.com/anicetkeric/spring-reactive-openapi-codegen/blob/main/src/main/resources/openapi/yaml/library-openapi.yaml)
  termsOfService: http://swagger.io/terms/
  contact:
    email: boottechnologies.ci@gmail.com
  license:
    name: Apache 2.0
    url: http://www.apache.org/licenses/LICENSE-2.0.html
  version: '1.0.11'
externalDocs:
  description: Find out more about Swagger
  url: http://swagger.io
servers:
  - url: http://localhost:8080/api/v1
    description: Development server
  - url: http://localhost:8081/api/v1
    description: Staging server
  - url: http://localhost:8082/api/v1
    description: Production server
tags:
  - name: Book
    description: Operations about book
    externalDocs:
      description: Find out more
      url: http://swagger.io
  - name: Author
    description: Operations about author
    externalDocs:
      description: Find out more
      url: http://swagger.io

Spring Webflux Application

Now that we have completed the OpenAPI specification, we can move on to using it in our project. Let’s start by creating a simple Spring Reactive project from start.spring.io, with the following dependencies: Spring Reactive Web and Lombok.

Add the OpenAPI specification to thesrc/main/resources/openapi/yaml directory with name library-openapi.yaml.

Then, we’ll be using the Open API Generator Maven plugin in the build section of the pom file.

<plugin>
                <groupId>org.openapitools</groupId>
                <artifactId>openapi-generator-maven-plugin</artifactId>
                <!-- RELEASE_VERSION -->
                <version>5.4.0</version>
                <!-- /RELEASE_VERSION -->
                <executions>
                    <execution>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <inputSpec>${project.basedir}/src/main/resources/openapi/yaml/library-openapi.yaml</inputSpec>
                            <generatorName>spring</generatorName>
                            <packageName>${project.groupId}.springreactiveopenapicodegen</packageName>
                            <apiPackage>${project.groupId}.springreactiveopenapicodegen.api.v1</apiPackage>
                            <modelPackage>${project.groupId}.springreactiveopenapicodegen.dto</modelPackage>
                            <generateApiTests>false</generateApiTests>
                            <generateModelTests>false</generateModelTests>
                            <modelNameSuffix>DTO</modelNameSuffix>
                            <library>spring-boot</library>
                            <skipValidateSpec>false</skipValidateSpec>
                            <configOptions>
                                <reactive>true</reactive>
                                <delegatePattern>true</delegatePattern>
                                <interfaceOnly>true</interfaceOnly>
                                <dateLibrary>java8</dateLibrary>
                                <java8>true</java8>
                                <sourceFolder>src/main/java</sourceFolder>
                                <!--suppress UnresolvedMavenProperty -->
                                <additionalModelTypeAnnotations>@lombok.Builder @lombok.NoArgsConstructor @lombok.AllArgsConstructor</additionalModelTypeAnnotations>
                                <useTags>true</useTags>
                            </configOptions>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

In order to fix compilation issues, we need to add additional dependencies to the pom.xml file:

  <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>2.2.6</version>
        </dependency>
        <dependency>
            <groupId>org.openapitools</groupId>
            <artifactId>jackson-databind-nullable</artifactId>
            <version>0.2.4</version>
        </dependency>
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
        </dependency>

Run the command: mvn clean compile.The generator will generate the endpoint interfaces and DTO classes under the target folder.

For sample, the generated interface for the Book Controller API looks as follows:

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-12-12T10:52:41.290045336+01:00[Europe/Madrid]")
@Validated
@Tag(name = "Book", description = "the Book API")
public interface BookApi {

    /**
     * POST /book : Create a new book.
     * Create new book in library
     *
     * @param bookDTO  (required)
     * @return Created successfully (status code 201)
     *         or something went wrong!  (status code 400)
     *         or You are not authorized to view the resource (status code 401)
     *         or Accessing the resource you were trying to reach is forbidden (status code 403)
     *         or The resource you were trying to reach is not found (status code 404)
     *         or Method Not Allowed (status code 405)
     *         or Internal Server Error (status code 500)
     */
    @Operation(
        operationId = "createBook",
        summary = "Create a new book.",
        tags = { "Book" },
        responses = {
            @ApiResponse(responseCode = "201", description = "Created successfully", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  SuccessResponseDTO.class))),
            @ApiResponse(responseCode = "400", description = "something went wrong! ", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "401", description = "You are not authorized to view the resource", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "403", description = "Accessing the resource you were trying to reach is forbidden", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "404", description = "The resource you were trying to reach is not found", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "405", description = "Method Not Allowed", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class)))
        }
    )
    @RequestMapping(
        method = RequestMethod.POST,
        value = "/book",
        produces = { "application/json" },
        consumes = { "application/json" }
    )
    default Mono<ResponseEntity<SuccessResponseDTO>> _createBook(
        @Parameter(name = "BookDTO", description = "", required = true, schema = @Schema(description = "")) @Valid @RequestBody Mono<BookDTO> bookDTO,
        @Parameter(hidden = true) final ServerWebExchange exchange
    ) {
        return createBook(bookDTO, exchange);
    }

    // Override this method
    default  Mono<ResponseEntity<SuccessResponseDTO>> createBook(Mono<BookDTO> bookDTO,  final ServerWebExchange exchange) {
        Mono<Void> result = Mono.empty();
        exchange.getResponse().setStatusCode(HttpStatus.NOT_IMPLEMENTED);
        for (MediaType mediaType : exchange.getRequest().getHeaders().getAccept()) {
            if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
                String exampleString = "{ \"data\" : \"{}\", \"message\" : \"message\" }";
                result = ApiUtil.getExampleResponse(exchange, mediaType, exampleString);
                break;
            }
        }
        return result.then(Mono.empty());

    }


    /**
     * DELETE /book/{id} : Delete an existing book
     *
     * @param id  (required)
     * @return Internal Server Error (status code 500)
     *         or Unauthorized (status code 401)
     *         or Validation exception (status code 405)
     *         or Invalid ID supplied (status code 400)
     *         or Contact not found (status code 404)
     *         or successful operation (status code 200)
     */
    @Operation(
        operationId = "deleteBook",
        summary = "Delete an existing book",
        tags = { "Book" },
        responses = {
            @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "405", description = "Validation exception", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "400", description = "Invalid ID supplied", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "404", description = "Contact not found", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "200", description = "successful operation", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  SuccessResponseDTO.class)))
        }
    )
    @RequestMapping(
        method = RequestMethod.DELETE,
        value = "/book/{id}",
        produces = { "application/json" }
    )
    default Mono<ResponseEntity<SuccessResponseDTO>> _deleteBook(
        @Parameter(name = "id", description = "", required = true, schema = @Schema(description = "")) @PathVariable("id") String id,
        @Parameter(hidden = true) final ServerWebExchange exchange
    ) {
        return deleteBook(id, exchange);
    }

    // Override this method
    default  Mono<ResponseEntity<SuccessResponseDTO>> deleteBook(String id,  final ServerWebExchange exchange) {
        Mono<Void> result = Mono.empty();
        exchange.getResponse().setStatusCode(HttpStatus.NOT_IMPLEMENTED);
        for (MediaType mediaType : exchange.getRequest().getHeaders().getAccept()) {
            if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
                String exampleString = "{ \"data\" : \"{}\", \"message\" : \"message\" }";
                result = ApiUtil.getExampleResponse(exchange, mediaType, exampleString);
                break;
            }
        }
        return result.then(Mono.empty());

    }


    /**
     * GET /book : Get all list book
     *
     * @return return successfully (status code 200)
     *         or Bad Request (status code 400)
     *         or You are not authorized to view the resource (status code 401)
     *         or Accessing the resource you were trying to reach is forbidden (status code 403)
     *         or The resource you were trying to reach is not found (status code 404)
     *         or Method Not Allowed (status code 405)
     *         or something went wrong!  (status code 500)
     */
    @Operation(
        operationId = "getAllBook",
        summary = "Get all list book",
        tags = { "Book" },
        responses = {
            @ApiResponse(responseCode = "200", description = "return successfully", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  SuccessResponseDTO.class))),
            @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "401", description = "You are not authorized to view the resource", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "403", description = "Accessing the resource you were trying to reach is forbidden", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "404", description = "The resource you were trying to reach is not found", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "405", description = "Method Not Allowed", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "500", description = "something went wrong! ", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class)))
        }
    )
    @RequestMapping(
        method = RequestMethod.GET,
        value = "/book",
        produces = { "application/json" }
    )
    default Mono<ResponseEntity<SuccessResponseDTO>> _getAllBook(
        @Parameter(hidden = true) final ServerWebExchange exchange
    ) {
        return getAllBook(exchange);
    }

    // Override this method
    default  Mono<ResponseEntity<SuccessResponseDTO>> getAllBook( final ServerWebExchange exchange) {
        Mono<Void> result = Mono.empty();
        exchange.getResponse().setStatusCode(HttpStatus.NOT_IMPLEMENTED);
        for (MediaType mediaType : exchange.getRequest().getHeaders().getAccept()) {
            if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
                String exampleString = "{ \"data\" : \"{}\", \"message\" : \"message\" }";
                result = ApiUtil.getExampleResponse(exchange, mediaType, exampleString);
                break;
            }
        }
        return result.then(Mono.empty());

    }


    /**
     * GET /book/{id} : get the \&quot;id\&quot; author.
     * Returns a single record
     *
     * @param id  (required)
     * @return Internal Server Error (status code 500)
     *         or Unauthorized (status code 401)
     *         or Method Not Allowed (status code 405)
     *         or Bad Request (status code 400)
     *         or entity by id not found (status code 404)
     *         or Successful operation (status code 200)
     */
    @Operation(
        operationId = "getOneBook",
        summary = "get the \"id\" author.",
        tags = { "Book" },
        responses = {
            @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "405", description = "Method Not Allowed", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "400", description = "Bad Request", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "404", description = "entity by id not found", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "200", description = "Successful operation", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  SuccessResponseDTO.class)))
        }
    )
    @RequestMapping(
        method = RequestMethod.GET,
        value = "/book/{id}",
        produces = { "application/json" }
    )
    default Mono<ResponseEntity<SuccessResponseDTO>> _getOneBook(
        @Parameter(name = "id", description = "", required = true, schema = @Schema(description = "")) @PathVariable("id") String id,
        @Parameter(hidden = true) final ServerWebExchange exchange
    ) {
        return getOneBook(id, exchange);
    }

    // Override this method
    default  Mono<ResponseEntity<SuccessResponseDTO>> getOneBook(String id,  final ServerWebExchange exchange) {
        Mono<Void> result = Mono.empty();
        exchange.getResponse().setStatusCode(HttpStatus.NOT_IMPLEMENTED);
        for (MediaType mediaType : exchange.getRequest().getHeaders().getAccept()) {
            if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
                String exampleString = "{ \"data\" : \"{}\", \"message\" : \"message\" }";
                result = ApiUtil.getExampleResponse(exchange, mediaType, exampleString);
                break;
            }
        }
        return result.then(Mono.empty());

    }


    /**
     * PUT /book/{id} : Updates an existing book.
     *
     * @param id  (required)
     * @param bookDTO  (required)
     * @return Internal Server Error (status code 500)
     *         or Unauthorized (status code 401)
     *         or Validation exception (status code 405)
     *         or Invalid ID supplied (status code 400)
     *         or Contact not found (status code 404)
     *         or successful operation (status code 200)
     */
    @Operation(
        operationId = "updateBook",
        summary = "Updates an existing book.",
        tags = { "Book" },
        responses = {
            @ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "401", description = "Unauthorized", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "405", description = "Validation exception", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "400", description = "Invalid ID supplied", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "404", description = "Contact not found", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  ErrorResponseDTO.class))),
            @ApiResponse(responseCode = "200", description = "successful operation", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  SuccessResponseDTO.class)))
        }
    )
    @RequestMapping(
        method = RequestMethod.PUT,
        value = "/book/{id}",
        produces = { "application/json" },
        consumes = { "application/json" }
    )
    default Mono<ResponseEntity<SuccessResponseDTO>> _updateBook(
        @Parameter(name = "id", description = "", required = true, schema = @Schema(description = "")) @PathVariable("id") String id,
        @Parameter(name = "BookDTO", description = "", required = true, schema = @Schema(description = "")) @Valid @RequestBody Mono<BookDTO> bookDTO,
        @Parameter(hidden = true) final ServerWebExchange exchange
    ) {
        return updateBook(id, bookDTO, exchange);
    }

    // Override this method
    default  Mono<ResponseEntity<SuccessResponseDTO>> updateBook(String id, Mono<BookDTO> bookDTO,  final ServerWebExchange exchange) {
        Mono<Void> result = Mono.empty();
        exchange.getResponse().setStatusCode(HttpStatus.NOT_IMPLEMENTED);
        for (MediaType mediaType : exchange.getRequest().getHeaders().getAccept()) {
            if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
                String exampleString = "{ \"data\" : \"{}\", \"message\" : \"message\" }";
                result = ApiUtil.getExampleResponse(exchange, mediaType, exampleString);
                break;
            }
        }
        return result.then(Mono.empty());

    }

}

We can implement the BookApi interfaces created in our project like this:

/**
 * REST controller for managing {@link com.bootlabs.springreactiveopenapicodegen.dto.BookDTO}.
 */
@Validated
@Tag(name = "Book", description = "Book controller")
@RestController
@RequestMapping("/api/v1")
public class BookController implements BookApi {

    /**
     * {@inheritDoc}
     */
    @Override
    public Mono<ResponseEntity<SuccessResponseDTO>> _createBook(
            @Parameter(name = "BookDTO", description = "", required = true, schema = @Schema(description = "")) @Valid @RequestBody Mono<BookDTO> bookDTO,
            @Parameter(hidden = true) final ServerWebExchange exchange
    ) {
        return bookDTO
                .map(b -> ResponseEntity.created(URI.create("/api/v1/book/" + b.getId()))
                        .body(new SuccessResponseDTO(b, "book created Successfully")));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Mono<ResponseEntity<SuccessResponseDTO>> _deleteBook(
            @Parameter(name = "id", description = "", required = true, schema = @Schema(description = "")) @PathVariable("id") String id,
            @Parameter(hidden = true) final ServerWebExchange exchange
    ) {
        return Mono.just(ResponseEntity
                .ok()
                .body(new SuccessResponseDTO(null, "Book deleted Successfully")));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Mono<ResponseEntity<SuccessResponseDTO>> _getAllBook(
            @Parameter(hidden = true) final ServerWebExchange exchange
    ) {
       return Mono.just(ResponseEntity
                .ok()
                .body(new SuccessResponseDTO(Collections.emptyList(), "result found")));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Mono<ResponseEntity<SuccessResponseDTO>> _getOneBook(
            @Parameter(name = "id", description = "", required = true, schema = @Schema(description = "")) @PathVariable("id") String id,
            @Parameter(hidden = true) final ServerWebExchange exchange
    ) {
        return Mono.just(ResponseEntity
                .ok()
                .body(new SuccessResponseDTO(BookDTO.builder().id(id).build(), "result found")));
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Mono<ResponseEntity<SuccessResponseDTO>> _updateBook(
            @Parameter(name = "id", description = "", required = true, schema = @Schema(description = "")) @PathVariable("id") String id,
            @Parameter(name = "BookDTO", description = "", required = true, schema = @Schema(description = "")) @Valid @RequestBody Mono<BookDTO> bookDTO,
            @Parameter(hidden = true) final ServerWebExchange exchange
    ) {
        return updateBook(id, bookDTO, exchange);
    }

}

This implementation of the controller class is shortened to simply return static dummy data. We notice that our class BookController implements all the methods of the interface generated BookApi.

Test the API

Import OpenAPI definition in insomnia

Import data from a file > Select OpenAPI file > New > Request Collection

Test

Now we can run our application and test it.

POST /api/v1/book
GET /api/v1/book

Done. It works fine. 🙂

Conclusion

In this story, we learned how to use the OpenAPI specification in a spring Webflux project. At this point, the development team can start implementing the business logic.

The complete source code is available on GitHub.

References

👉 Link to Medium blog

Related Posts