Thursday, September 5, 2024

Creating REST APIs with OpenAPI, Spring Boot 3.3.3, Java 21, and Jakarta

 Introduction

In today's software landscape, designing robust and scalable REST APIs is a crucial aspect of application development. With the growing complexity of APIs, ensuring consistency and maintainability across different services becomes challenging. This is where OpenAPI (formerly known as Swagger) comes into play. OpenAPI provides a standard way to define your API specifications, making it easier to generate client SDKs, server stubs, and documentation automatically.

In this post, we’ll explore how to design REST APIs using OpenAPI and generate a RestController class from an OpenAPI YAML file. We’ll leverage Spring Boot 3.3.3 and Java 21 to build a modern, efficient, and maintainable API.



Setting Up Your Spring Boot Project

For this example, we’ll use Spring Boot 3.3.3 and Java 21. 


Project Structure

Once your project is set up, the basic structure will look like this:

demo-open-api-swager
|-- src
|   |-- main
|       |-- java
|           |-- com
|               |-- henry.openapi
|                   |-- service
|-- src
|   |-- test
|       |-- java
|           |-- com
|               |-- henry
|                   |-- ...
|-- pom.xml

Defining the OpenAPI Specification

Now, let’s define our API using an OpenAPI YAML file. This file will describe the API’s endpoints, request and response models, and other relevant details.


1. Create the YAML File

In the src/main/resources directory, create a file named api.yaml and define your API specification. Here’s a simple example:


openapi: 3.0.0
info:
  title: Sample API
  version: 1.0.0
paths:
  /items:
    get:
      summary: Get all items
      operationId: getAllItems
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Item'
    post:
      summary: Create a new item
      operationId: createItem
      requestBody:
        description: Item to create
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Item'
      responses:
        '201':
          description: Item created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Item'
  
  /items/{id}:
    get:
      summary: Get an item by ID
      operationId: getItemById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Item'
        '404':
          description: Item not found
    put:
      summary: Update an item by ID
      operationId: updateItemById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      requestBody:
        description: Item to update
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Item'
      responses:
        '200':
          description: Item updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Item'
        '404':
          description: Item not found
    delete:
      summary: Delete an item by ID
      operationId: deleteItemById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '204':
          description: Item deleted successfully
        '404':
          description: Item not found

components:
  schemas:
    Item:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
      required:
        - name

2. YAML Content Explanation

info: Provides metadata about the API, such as title and version.

paths: Defines the available API endpoints. In this case, we have a /items endpoint with a CRUD operation.

components: Specifies reusable components like schemas. Here, Item is defined with id and name properties.


Configuring Swagger Codegen Plugin

To generate Java classes from the YAML file, we’ll configure the Swagger Codegen Maven plugin.


1. Maven Plugin Configuration

Add the following configuration to your pom.xml:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.henry</groupId>
    <artifactId>demo-open-api-swager</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo-open-api-swager</name>
    <description>Designing Robust REST APIs with OpenAPI in Spring Boot</description>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <!-- Spring Boot Starter for Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Boot DevTools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <!-- Lombok for reducing boilerplate -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Swagger Models and Annotations -->
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-models</artifactId>
            <version>2.2.4</version>
        </dependency>
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations-jakarta</artifactId>
            <version>2.2.22</version>
        </dependency>
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>2.2.16</version>
        </dependency>

        <!-- Nullable support for Jackson -->
        <dependency>
            <groupId>org.openapitools</groupId>
            <artifactId>jackson-databind-nullable</artifactId>
            <version>0.2.6</version>
        </dependency>

        <!-- Springdoc OpenAPI for Spring Boot 3 instead of springfox dependencies -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.6.0</version>
        </dependency>

        <!-- Jakarta Validation API -->
        <dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
        </dependency>

        <!-- Jakarta Annotations API -->
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
        </dependency>

        <!-- Spring Boot Starter for Testing -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Spring Boot Maven Plugin -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>

            <!-- OpenAPI Generator Plugin -->
            <plugin>
                <groupId>org.openapitools</groupId>
                <artifactId>openapi-generator-maven-plugin</artifactId>
                <!-- Till  6.4.0 we can use jakarta instead of javax with <useJakartaEe>true</useJakartaEe>-->
                <version>7.8.0</version>
                <executions>
                    <execution>
                        <id>generate-sources</id>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                        <configuration>
                            <inputSpec>${project.basedir}/src/main/resources/api.yaml</inputSpec>
                            <output>${project.basedir}/src/main/generated-sources</output>
                            <generatorName>spring</generatorName>
                            <apiPackage>com.henry.openapi.controller</apiPackage>
                            <modelPackage>com.henry.openapi.model</modelPackage>
                            <invokerPackage>com.henry.openapi</invokerPackage>
                            <generateSupportingFiles>false</generateSupportingFiles>
                            <skipOperationExample>true</skipOperationExample>
                            <generateApis>true</generateApis>
                            <generateModelTests>false</generateModelTests>
                            <generateModelDocumentation>false</generateModelDocumentation>
                            <configOptions>
                                <useJakartaEe>true</useJakartaEe>
                                <serializableModel>true</serializableModel>
                                <dateLibrary>legacy</dateLibrary>
                                <java21>true</java21>
                                <library>spring-boot</library>
                                <delegatePattern>true</delegatePattern>
                                <useBeanValidation>true</useBeanValidation>
                                <useOptional>false</useOptional>
                                <hideGenerationTimestamp>true</hideGenerationTimestamp>
                            </configOptions>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <!-- Maven Compiler Plugin for Java 21 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>21</source>
                    <target>21</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>


Generating REST Controllers

With the YAML file and Maven plugin configured, we can now generate the REST controllers.


1. Run the Maven Command

Execute the following command in your project directory:


mvn clean install
This command will generate the REST controller interfaces based on the OpenAPI YAML file.

2. Review the Generated Code


After running the command, you’ll find the generated code in the ${project.basedir}/src/main/generated-sources directory (or another location depending on your configuration). 
















Here’s an example of what the generated ItemsApiDelegate interface might look like:

package com.henry.openapi.controller;

import com.henry.openapi.model.Item;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.multipart.MultipartFile;

import jakarta.validation.constraints.*;
import jakarta.validation.Valid;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import jakarta.annotation.Generated;

/**
 * A delegate to be called by the {@link ItemsApiController}}.
 * Implement this interface with a {@link org.springframework.stereotype.Service} annotated class.
 */
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", comments = "Generator version: 7.8.0")
public interface ItemsApiDelegate {

    default Optional<NativeWebRequest> getRequest() {
        return Optional.empty();
    }

    /**
     * POST /items : Create a new item
     *
     * @param item Item to create (required)
     * @return Item created successfully (status code 201)
     * @see ItemsApi#createItem
     */
    default ResponseEntity<Item> createItem(Item item) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

    /**
     * DELETE /items/{id} : Delete an item by ID
     *
     * @param id  (required)
     * @return Item deleted successfully (status code 204)
     *         or Item not found (status code 404)
     * @see ItemsApi#deleteItemById
     */
    default ResponseEntity<Void> deleteItemById(Long id) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

    /**
     * GET /items : Get all items
     *
     * @return Successful response (status code 200)
     * @see ItemsApi#getAllItems
     */
    default ResponseEntity<List<Item>> getAllItems() {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

    /**
     * GET /items/{id} : Get an item by ID
     *
     * @param id  (required)
     * @return Successful response (status code 200)
     *         or Item not found (status code 404)
     * @see ItemsApi#getItemById
     */
    default ResponseEntity<Item> getItemById(Long id) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

    /**
     * PUT /items/{id} : Update an item by ID
     *
     * @param id  (required)
     * @param item Item to update (required)
     * @return Item updated successfully (status code 200)
     *         or Item not found (status code 404)
     * @see ItemsApi#updateItemById
     */
    default ResponseEntity<Item> updateItemById(Long id,
        Item item) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

}

Customizing the Generated Code

Although the generated code provides a good starting point, you may want to customize it to suit your application's specific needs.


1. Implementing the Interface

You can create a class that implements the ItemsApiDelegate interface and adds custom logic:

package com.henry.openapi.service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import com.henry.openapi.controller.ItemsApiDelegate;
import com.henry.openapi.model.Item;

@Service
public class ItemsServiceImpl implements ItemsApiDelegate{

	 // Mock in-memory database
    private final List<Item> items = new ArrayList<>();
    private Long currentId = 1L;

    public ItemsServiceImpl() {
        // Initialize with some mock data
    	var it1 = new Item();
    	it1.setId(1L);
    	it1.setName("Item 1");
        items.add(it1);
        
        var it2 = new Item();
        it2.setId(2L);
        it2.setName("Item 2");
        items.add(it2);
    }

    @Override
    public ResponseEntity<List<Item>> getAllItems() {
        return new ResponseEntity<>(items, HttpStatus.OK);
    }

    @Override
    public ResponseEntity<Item> getItemById(Long id) {
        Optional<Item> item = items.stream()
                .filter(i -> i.getId().equals(id))
                .findFirst();
        return item.map(i -> new ResponseEntity<>(i, HttpStatus.OK))
                .orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
    }

    @Override
    public ResponseEntity<Item> createItem(Item item) {
        item.setId(currentId++); // Simulate auto-increment ID
        items.add(item);
        return new ResponseEntity<>(item, HttpStatus.CREATED);
    }

    @Override
    public ResponseEntity<Item> updateItemById(Long id, Item item) {
        for (int i = 0; i < items.size(); i++) {
            if (items.get(i).getId().equals(id)) {
                item.setId(id); // Keep the same ID
                items.set(i, item);
                return new ResponseEntity<>(item, HttpStatus.OK);
            }
        }
        return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }

    @Override
    public ResponseEntity<Void> deleteItemById(Long id) {
        boolean removed = items.removeIf(i -> i.getId().equals(id));
        if (removed) {
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }
}

Testing the Generated API

Now that you have your controllers in place, it’s time to test the API.


1. Run the Application

Start your Spring Boot application using your preferred method:

mvn spring-boot:run


2. Access Swagger UI

Navigate to http://localhost:8080/swagger-ui.html in your browser. Swagger UI will display the endpoints defined in your OpenAPI specification, allowing you to interact with them directly.









3. curl requests for each method in the ItemsServiceImpl class

1. GET /items (Retrieve all items)


curl -X GET "http://localhost:8080/items" -H "accept: application/json"

2. GET /items/{id} (Retrieve an item by ID)

Replace {id} with the actual item ID you want to retrieve. For example, to get the item with ID 1:

curl -X GET "http://localhost:8080/items/1" -H "accept: application/json"


3. POST /items (Create a new item)

You need to provide the item details in JSON format. For example:

curl -X POST "http://localhost:8080/items" \
  -H "Content-Type: application/json" \
  -H "accept: application/json" \
  -d '{
        "name": "New Item"
      }'


4. PUT /items/{id} (Update an item by ID)

Replace {id} with the actual item ID you want to update. For example, to update the item with ID 1:

curl -X PUT "http://localhost:8080/items/1" \
  -H "Content-Type: application/json" \
  -H "accept: application/json" \
  -d '{
        "name": "Updated Item"
      }'


5. DELETE /items/{id} (Delete an item by ID)

Replace {id} with the actual item ID you want to delete. For example, to delete the item with ID 1:

curl -X DELETE "http://localhost:8080/items/1" -H "accept: application/json"














Conclusion


By leveraging OpenAPI and Swagger, you can automate the creation of REST APIs, ensuring consistency and reducing the likelihood of errors. The approach outlined in this post not only saves development time but also provides a clear contract for API consumers.

we covered the entire process of defining an API with OpenAPI, generating code using Swagger Codegen, and customizing the generated controllers in a Spring Boot project. This approach is highly scalable, making it easier to manage and maintain your APIs as they evolve.


Additional Configuration



IntelliJ IDEA  2024.2.1 Community 

File -> Project Structure 













Click On -> Apply Button 


Eclipse IDE for Enterprise Java and Web Developers 2024-06

1. Go to Windows->Preferences->XML(Wild Web Development)

 2. Check checkbox for "Download external resources like referenced DTD,XSD







Source code

Here on GitHub.

References

https://swagger.io/specification/

https://springdoc.org/

https://github.com/swagger-api/swagger-codegen

https://stackoverflow.com/questions/70692260/cvc-elt-1-a-cannot-find-the-declaration-of-element-project

https://stackoverflow.com/questions/70291226/how-to-remove-apiutil-java-from-openapi-geneate-task-with-openapi-generator-grad

https://github.com/springdoc/springdoc-openapi/issues/1977

https://stackoverflow.com/questions/74593513/is-there-a-way-to-configure-openapi-generator-to-use-jakarta-package-during-gene

No comments:

Post a Comment

Creating REST APIs with OpenAPI, Spring Boot 3.3.3, Java 21, and Jakarta

 Introduction In today's software landscape, designing robust and scalable REST APIs is a crucial aspect of application development. Wit...