Skip to content

Contract First Approach in API Design With OpenAPI and Java 17 + Spring Boot

Posted on:February 8, 2024 at 07:00 PM

Table of Contents

Open Table of Contents

Introduction

In my first week as an entry-level back-end engineer in a company I worked for previously, the Software Engineers chapter lead was very kind in helping us understand their expectations from us -as new hires, in the first month of work. Among them, he stressed the importance of understanding the contract-first approach in API development. Like everyone else in the meeting, this was the first time I had heard the term and a myriad of other words that followed, typical of the orientation week. However, weeks after we started working and shipping features, I thoroughly appreciated the ingenious nature of the approach. So, I hope that by the end of this article, you will also see the gains of the approach and try it out in your development work.

What is the Contract-First Approach?

Contract-first approach, sometimes called API-First development, is an approach to developing software where the design of the API precedes the implementation of the business logic. Typically, this process is encapsulated in the design phase within the software development life cycle. With the contract-first approach, a solution architect or a designer, creates a draft API contract document, the team that is involved in the development of the software then reviews the contract, and makes/proposes changes to it before all the stakeholders start their work in parallel.

The primary benefit of this approach is that the document provides a harmonious language among the team members in terms of the expected API objective, and the time taken before the work is done (Agile Done). In addition, the benefit of parallel working between the team members cannot be underscored enough as the front-end, QA and other stakeholders do not have to wait for the business logic of the API to be shipped before they start working. Let us look at what makes up for the aforementioned harmonious language.

OpenAPI Specification

OpenApI specification (OAS) defines a standard for creating an API design document or a language-independent interface to HTTP APIs. The standard allows computers and humans to understand and interact with the API document with a minimal amount of business logic implemented.

The OpenAPI definition is then used by documentation generation tools to display the API, and code generation tools to generate servers and clients in various testing tools, programming languages, and other dynamic use cases. In this article, we will use Swagger, Java 17 programming language, and OpenAPI generator tool that will be configured with maven in a POM file.

API Definition

Regularly, HTTP APIs usually implement HTTP methods such as GET, POST, DELETE, PUT, etc. In our illustration, we will hypothesize of an application where you can add - POST, employees, and retrieve - GET an employee’s data by their employee ID.

Smartbear provides an intuitive web-based Swagger Editor for creating OpenAPI APIs. You can check it out and use it in adding and editing the code snippets that follow.

API Metadata

Our API design document uses YAML language, the standard from OpenAPI specification. After opening the online Swagger Editor in your browser, add the following code snippet:

openapi: 3.0.0
info:
  title: Demo API
  description: |-
    This API specification is a demo for Contract-First/API-First approach in building Java based APIs
    - POST employee into the system
    - GET an employee's details by their employee id
  version: 1.0.0
servers:
  - url: /api/v1
tags:
  - name: employees
    description: Operations about employees

Metadata Keywords

Exposing a POST and GET Path

When the employee’s details have been added successfully, then we expect a 204 HTTP status code, CREATED, followed by a short message describing the operation. On the other hand, when retrieving an employee’s data by their employee ID, a successful operation should return status code 200, OKAY, accompanied by an array of the respective employee’s data. After the metadata section, add the following:

paths:
  /employee:
    post:
      tags:
        - employees
      summary: Create an employee
      description: Creates an employee with given details
      operationId: createEmployee
      requestBody:
        description: Create a new employee with an employee id, first name and last name
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmployeeData"
        required: true
      responses:
        "204":
          description: Employee data posted successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HttpCreatedResponse"
        "405":
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HttpInputErrorResponse"
      security:
        - employeeOpenId:
            - write_employee
            - admin
  /employee/{employeeId}:
    get:
      tags:
        - employees
      summary: Fetch an employee's data
      description: Fetch an employee details by their employee id
      operationId: getEmployeeById
      parameters:
        - in: path
          name: employeeId
          description: The employee id
          required: true
          schema:
            type: string
            example: 1
      responses:
        "200":
          description: Employee data fetched successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/EmployeeData"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HttpUnauthorizedErrorResponse"
      security:
        - employeeApiKey: []

Paths Keywords

Data Model Components

In the above code snippet, you might have noticed this line $ref: "#/components/schemas/. This statement uses the keyword ref that references a schema object. A schema object usually defines the input and output when a specified HTTP method of a given API endpoint is called. For instance, when the POST method is called on the /employee endpoint, the client should input data corresponding to the EmployeeData schema. However, if the data does not correspond to the schema, an HTTP status code 405 is produced, followed by an error message mapped out in the HttpInputErrorResponse schema.

Schema Object Request and Response

components:
  schemas:
    EmployeeData:
      required:
        - employeeId
        - firstName
        - lastName
      type: object
      properties:
        employeeId:
          type: integer
          description: The employee id
          example: dev123
        firstName:
          type: string
          description: The first name of the employee
          example: John
        lastName:
          type: string
          description: The last name of the employee
          example: Doe
    HttpCreatedResponse:
      type: object
      required:
        - status
        - message
      properties:
        status:
          description: Name of the Http Status
          type: string
          example: CREATED
        message:
          description: Description of the error thrown
          type: string
          example: Employee data posted successfully
    HttpInputErrorResponse:
      type: object
      required:
        - status
        - message
      properties:
        status:
          description: Name of the Http Status
          type: string
          example: METHOD_NOT_ALLOWED
        message:
          description: Description of the error thrown
          type: string
          example: Invalid Input request
    HttpUnauthorizedErrorResponse:
      type: object
      required:
        - status
        - message
      properties:
        status:
          description: Name of the Http Status
          type: string
          example: UNAUTHORIZED
        message:
          description: Description of the error thrown
          type: string
          example: User not Authorized

Security Schema Object

OAS supports several types of authentication and authorization schemes which are specified by the security scheme. The security schemes define the access level of different users of API resources based on a predefined authentication type. We will use API Keys and OpenId Connect Protocol with Keycloak as the identity provider. I will be running another article on how to configure Keycloak as an identity broker and provider for your application’s security layer.

    securitySchemes:
        employeeOpenId:
          type: openIdConnect
          openIdConnectUrl: https://{keycloak_server}/auth/realms/{realm_name}/.well-known/openid-configuration
        employeeApiKey:
          type: apiKey
          in: header
          name: API_KEY

API Document Review

If you have been using the online Swagger Editor, errors are expected as you build up the project

Below is the full API design document:

openapi: 3.0.0
info:
  title: Demo API
  description: |-
    This API specification is a demo for Contract-First/API-First approach in building Java based APIs
    - POST employee into the system
    - GET an employee's details by their employee id
  version: 1.0.0
servers:
  - url: /api/v1
tags:
  - name: employees
    description: Operations about employees
paths:
  /employee:
    post:
      tags:
        - employees
      summary: Create an employee
      description: Creates an employee with given details
      operationId: createEmployee
      requestBody:
        description: Create a new employee with an employee id, first name and last name
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/EmployeeData"
        required: true
      responses:
        "204":
          description: Employee data posted successfully
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HttpCreatedResponse"
        "405":
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HttpInputErrorResponse"
      security:
        - employeeOpenId:
            - write_employee
            - admin
  /employee/{employeeId}:
    get:
      tags:
        - employees
      summary: Fetch an employee's data
      description: Fetch an employee details by their employee id
      operationId: getEmployeeById
      parameters:
        - in: path
          name: employeeId
          description: The employee id
          required: true
          schema:
            type: string
            example: 1
      responses:
        "200":
          description: Employee data fetched successfully
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/EmployeeData"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HttpUnauthorizedErrorResponse"
      security:
        - employeeApiKey: []
components:
  schemas:
    EmployeeData:
      required:
        - employeeId
        - firstName
        - lastName
      type: object
      properties:
        employeeId:
          type: integer
          description: The employee id
          example: 1
        firstName:
          type: string
          description: The first name of the employee
          example: John
        lastName:
          type: string
          description: The last name of the employee
          example: Doe
    HttpCreatedResponse:
      type: object
      required:
        - status
        - message
      properties:
        status:
          description: Name of the Http Status
          type: string
          example: CREATED
        message:
          description: Description of the error thrown
          type: string
          example: Employee data posted successfully
    HttpInputErrorResponse:
      type: object
      required:
        - status
        - message
      properties:
        status:
          description: Name of the Http Status
          type: string
          example: METHOD_NOT_ALLOWED
        message:
          description: Description of the error thrown
          type: string
          example: Invalid Input request
    HttpUnauthorizedErrorResponse:
      type: object
      required:
        - status
        - message
      properties:
        status:
          description: Name of the Http Status
          type: string
          example: UNAUTHORIZED
        message:
          description: Description of the error thrown
          type: string
          example: User not Authorized
  securitySchemes:
    employeeOpenId:
      type: openIdConnect
      openIdConnectUrl: https://{keycloak_server}/auth/realms/{realm_name}/.well-known/openid-configuration
    employeeApiKey:
      type: apiKey
      in: header
      name: API_KEY

Quick Code

At this point, we have successfully created an API document that stakeholders can rely on when implementing the business logic of the API and the subsequent testing. We can implement one of the operations (operationId) using Maven as our build automation tool and Java as follows.

Configuration in Spring Boot Application

You can initialize a Spring Boot application with Spring Initializr as expounded on Building a REST API with Spring Boot. In the resources package, create a directory and name it openapi. Then within that directory, add our employee-spec.yaml file with the OpenAPI definition we have created so far.

Open our POM file, then add open api maven generator tool dependency that will automatically generate the skeleton of our API.

<dependency>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>{openapi-generator-version}</version>
    <scope>provided</scope>
</dependency>

We need to make sure that the Maven tool can discover the employee-spec.yaml file and execute it when Maven commands are ran. Under build and plugins in our POM file add the following:

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.2.0</version>
    <executions>
        <execution>
            <id>employee-API</id>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <skipValidateSpec>true</skipValidateSpec>
                <inputSpec>src/main/resources/openapi/employee-spec.yaml</inputSpec>
                <generatorName>spring</generatorName>
                <apiPackage>com.example.employeeDemo.api</apiPackage>
                <modelPackage>com.example.employeeDemo.model</modelPackage>
                <typeMappings>
                    <typeMapping>OffsetDateTime=LocalDateTime</typeMapping>
                </typeMappings>
                <configOptions>
                    <openApiNullable>false</openApiNullable>
                    <interfaceOnly>true</interfaceOnly>
                    <useSpringBoot3>true</useSpringBoot3>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

What the above code does is, the execution configuration finds the specification file for OpenAPI generator tool to execute when we run mvn clean install, then on execution, the contract is compiled and added to the target package of our application.

Note:

Business Logic Implementation.

We start by creating an EmployeeController.java class. The API interface generated in the target package is technically our Data Transfer Object (DTO). Therefore, we will have to implement the interface in our new class since the interface has abstracted the class.

import com.example.employeeDemo.api.EmployeeApi;
import com.example.employeeDemo.model.EmployeeData;
import com.example.employeeDemo.model.HttpCreatedResponse;

@Controller
@Slf4j
public class EmployeeController implements EmployeeApi {

    private final EmployeeService employeeService;

    public EmployeeController(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    @Override
    public ResponseEntity<HttpCreatedResponse> createEmployee(EmployeeData employeeData) {
        employeeService.postEmployee(toEmployeeData(employeeData));
        log.info("=====> Created an employee {}", toEmployeeData(employeeData));
        return new ResponseEntity<>(HttpStatus.CREATED);
    }

    @Override
    public ResponseEntity<List<EmployeeData>> getEmployeeById(String employeeId) {
        List<EmployeeEntity> list = employeeService.getEmployeeById(Long.parseLong(employeeId));
        List<EmployeeData> listData = list.stream().map(data -> toEmployeeEntity(data)).collect(Collectors.toList());
        log.info("=====> Fetched an employee {}", listData.stream().toList());
        return new ResponseEntity<>(listData, HttpStatus.OK);
    }
}

Conclusion

In this article, we have looked into how to create an API design document with OpenAPI specification by following a contract-first approach. In addition, we have looked at how we can implement the business logic of the API design using Maven and Java.

I believe that with this approach in software development, teams can be more agile and productive in their delivery of software as stakeholders do not need to rely on the developer to finish the code and expose the endpoints before starting their work. Furthermore, while the back-end developers would be working on the business logic of an API, the front-end developers would also be piecing together parts of the expected logic based on the exposed paths. Therefore, stakeholders would not waste time when developing an API.

Testing on Postman

Project Code