How to Implement OpenAPI in a Java Spring Boot Project

In this article, we will explore how to implement OpenAPI in a Java Spring Boot project to create documented and interactive REST APIs. OpenAPI (formerly Swagger) has become the de facto standard for REST API documentation.

1. What is OpenAPI?

OpenAPI is a standardized specification that allows you to describe REST APIs in a structured way. It enables you to:

  • Automatically document your API
  • Generate client and server code
  • Validate API contracts
  • Create an interactive interface to test the API

2. API-First Approach

The API-First approach consists of defining the OpenAPI specification first before implementing the code. This method offers several advantages:

  • Up-to-date documentation: Documentation is always synchronized with implementation
  • Code generation: Java code is automatically generated from the specification
  • Contract validation: Changes are automatically validated
  • Collaboration: Frontend and backend teams can work in parallel

3. Project Configuration

3.1 Gradle Dependencies

Add the following dependencies to your build.gradle file:


plugins {
    id "org.openapi.generator" version "7.0.1"
}

dependencies {
    implementation "io.swagger.core.v3:swagger-annotations:2.2.8"
    implementation "org.webjars:swagger-ui:4.15.5"
    implementation "org.webjars:webjars-locator-lite:0.1.0"
    implementation "org.openapitools:jackson-databind-nullable:0.2.6"
}

3.2 OpenAPI Generator Configuration

Create a gradle/openapi.gradle file with the following configuration:


sourceSets {
    main {
        java {
            srcDir file("${buildDir}/generated-api/src/main/java")
        }
    }
}

openApiValidate {
    inputSpec = "$rootDir/src/main/resources/openapi/api-v1.0.yaml"
}

openApiGenerate {
    generatorName = "spring"
    library = "spring-boot"
    validateSpec = true
    inputSpec = "$rootDir/src/main/resources/openapi/api-v1.0.yaml"
    outputDir = "$buildDir/generated-api"
    invokerPackage = "com.example.api.v1.config"
    apiPackage = "com.example.api.v1.web"
    modelPackage = "com.example.api.v1.web.model"
    modelNameSuffix = "DTO"
    configOptions = [
        useSpringBoot3: "true",
        dateLibrary: "java8",
        useOptional: "false",
        hideGenerationTimestamp: "true",
        interfaceOnly: "true",
        useTags: "true",
        swaggerDocketConfig: "false",
        swaggerAnnotations: "false",
        apiFirst: "false"
    ]
    typeMappings = [
        "Date": "LocalDate",
        "DateTime": "LocalDateTime"
    ]
    importMappings = [
        "LocalDate": "java.time.LocalDate",
        "LocalDateTime": "java.time.LocalDateTime"
    ]
}

tasks.openApiGenerate.dependsOn tasks.openApiValidate
compileJava.dependsOn tasks.openApiGenerate

4. OpenAPI File Structure

Organize your OpenAPI files in a modular way:


src/main/resources/openapi/
├── api-v1.0.yaml              # Main specification
├── paths/                     # Endpoint definitions
│   ├── users.yaml
│   ├── products.yaml
│   └── orders.yaml
├── schemas/                   # Data models
│   ├── User.yaml
│   ├── Product.yaml
│   └── Order.yaml
├── responses/                 # Standardized responses
│   ├── BadRequest.yaml
│   ├── NotFound.yaml
│   └── InternalServerError.yaml
└── swagger-ui/               # Swagger UI configuration
    └── swagger-ui-config.js

5. OpenAPI Specification Definition

5.1 Main File (api-v1.0.yaml)


openapi: "3.0.0"
info:
    version: "1.0"
    title: "My REST API"
    description: "REST API for user and product management"
    contact:
        name: "Development Team"
        email: "dev@example.com"

servers:
    - url: "https://api.example.com/v1"
      description: "Production server"
    - url: "https://localhost:8080/v1"
      description: "Development server"

tags:
    - name: "Users"
      description: "User management"
    - name: "Products"
      description: "Product management"

paths:
    /users:
        summary: "List users"
        description: "Retrieves the list of all users"
        $ref: "./paths/users.yaml"
    /products:
        summary: "List products"
        description: "Retrieves the list of all products"
        $ref: "./paths/products.yaml"

components:
    schemas:
        User:
            $ref: "./schemas/User.yaml"
        Product:
            $ref: "./schemas/Product.yaml"
    
    responses:
        BadRequest:
            $ref: "./responses/BadRequest.yaml"
        NotFound:
            $ref: "./responses/NotFound.yaml"
    
    securitySchemes:
        bearerAuth:
            type: http
            scheme: bearer
            bearerFormat: JWT

security:
    - bearerAuth: []

5.2 Endpoint Definition (paths/users.yaml)


get:
    tags:
        - "Users"
    summary: "List users"
    description: "Retrieves the list of all users with pagination"
    operationId: "getUsers"
    parameters:
        - name: "page"
          in: "query"
          description: "Page number"
          required: false
          schema:
            type: "integer"
            default: 0
        - name: "size"
          in: "query"
          description: "Page size"
          required: false
          schema:
            type: "integer"
            default: 20
    responses:
        '200':
            description: "Users list retrieved successfully"
            content:
                application/json:
                    schema:
                        type: "object"
                        properties:
                            content:
                                type: "array"
                                items:
                                    $ref: "../schemas/User.yaml"
                            totalElements:
                                type: "integer"
                            totalPages:
                                type: "integer"
        '401':
            $ref: '../responses/Unauthorized.yaml'
        '500':
            $ref: '../responses/InternalServerError.yaml'

post:
    tags:
        - "Users"
    summary: "Create user"
    description: "Creates a new user"
    operationId: "createUser"
    requestBody:
        required: true
        content:
            application/json:
                schema:
                    $ref: "../schemas/User.yaml"
    responses:
        '201':
            description: "User created successfully"
            content:
                application/json:
                    schema:
                        $ref: "../schemas/User.yaml"
        '400':
            $ref: '../responses/BadRequest.yaml'
        '401':
            $ref: '../responses/Unauthorized.yaml'

5.3 Model Definition (schemas/User.yaml)


type: "object"
required:
    - "id"
    - "email"
    - "firstName"
    - "lastName"
properties:
    id:
        type: "integer"
        format: "int64"
        description: "Unique user identifier"
        example: 1
    email:
        type: "string"
        format: "email"
        description: "User email address"
        example: "john.doe@example.com"
    firstName:
        type: "string"
        description: "User first name"
        example: "John"
    lastName:
        type: "string"
        description: "User last name"
        example: "Doe"
    phone:
        type: "string"
        description: "Phone number"
        example: "+1234567890"
    createdAt:
        type: "string"
        format: "date-time"
        description: "Creation date"
        example: "2023-01-01T10:00:00Z"
    updatedAt:
        type: "string"
        format: "date-time"
        description: "Last modification date"
        example: "2023-01-01T10:00:00Z"

6. Swagger UI Configuration

6.1 Spring Configuration

Create a configuration class for Swagger UI:


@Configuration
@ConditionalOnProperty(prefix = "swagger-ui", name = "enabled", havingValue = "true")
public class SwaggerUiConfiguration implements WebMvcConfigurer {
    
    @Value("${swagger-ui.configuration-name:default}")
    private String configurationName;
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry
            .addResourceHandler("/openapi/**", "/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/", "classpath:/openapi/")
            .resourceChain(true)
            .addResolver(new LiteWebJarsResourceResolver())
            .addResolver(new PathResourceResolver())
            .addTransformer(new SwaggerUiTransformer());
    }
    
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/swagger-ui/")
                .setViewName("forward:/swagger-ui/index.html");
    }
}

6.2 JavaScript Configuration (swagger-ui-config.js)


window.onload = function () {
    const ui = SwaggerUIBundle({
        url: "/openapi/api-v1.0.yaml",
        dom_id: '#swagger-ui',
        deepLinking: true,
        presets: [
            SwaggerUIBundle.presets.apis,
            SwaggerUIStandalonePreset
        ],
        plugins: [
            SwaggerUIBundle.plugins.DownloadUrl
        ],
        layout: "StandaloneLayout",
        oauth2RedirectUrl: window.location.protocol + "//" + 
                         window.location.hostname + ":" + 
                         window.location.port + "/swagger-ui/oauth2-redirect.html",
        withCredentials: true
    });
    
    ui.initOAuth({
        clientId: "my-client-id",
        scopes: "read write",
        usePkceWithAuthorizationCodeGrant: true
    });
    
    window.ui = ui;
};

7. Controller Implementation

Implement the generated interfaces in your controllers:


@RestController
@RequiredArgsConstructor
@RequestMapping("/v1")
public class UserApiController implements UserApi {
    
    private final UserService userService;
    private final UserMapper userMapper;
    private final NativeWebRequest nativeWebRequest;
    
    @Override
    public Optional<NativeWebRequest> getRequest() {
        return Optional.ofNullable(nativeWebRequest);
    }
    
    @Override
    public ResponseEntity<UsersResponseDTO> getUsers(
            @Valid @RequestParam(value = "page", required = false, defaultValue = "0") Integer page,
            @Valid @RequestParam(value = "size", required = false, defaultValue = "20") Integer size) {
        
        Page<User> users = userService.findAll(PageRequest.of(page, size));
        UsersResponseDTO response = userMapper.toResponseDTO(users);
        
        return ResponseEntity.ok(response);
    }
    
    @Override
    public ResponseEntity<UserDTO> createUser(@Valid @RequestBody UserDTO userDTO) {
        User user = userMapper.toEntity(userDTO);
        User savedUser = userService.save(user);
        UserDTO response = userMapper.toDTO(savedUser);
        
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
}

8. Application Configuration

Add the following configuration to your application.yml:


swagger-ui:
    enabled: true
    configuration-name: default

spring:
    security:
        oauth2:
            resourceserver:
                jwt:
                    issuer-uri: ${OIDC_ISSUER_URI}

9. Benefits of This Approach

  • Automatic documentation: Documentation is always up-to-date
  • Code generation: Reduces errors and development time
  • Contract validation: Changes are automatically validated
  • Interactive interface: Swagger UI allows easy API testing
  • Collaboration: Teams can work in parallel
  • Maintenance: Changes propagate automatically

10. Best Practices

  • Modular organization: Separate paths, schemas, and responses
  • Use references: Avoid duplication with $ref
  • Versioning: Manage your API versions
  • Validation: Validate your OpenAPI specification
  • Testing: Test your API with generated tools
  • Security: Clearly define security schemes

Conclusion

Implementing OpenAPI in a Java Spring Boot project offers many advantages in terms of productivity, quality, and maintenance. The API-First approach allows you to create well-documented and easy-to-use APIs while reducing errors and improving collaboration between teams.

By following the steps described in this article, you will be able to set up a robust and scalable solution for your REST APIs.

Useful Resources

Post a Comment

0 Comments