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
- openapi - The version of OAS that is used. 3.0.x is the latest version
- info - Contains a short title for the API and a description
- version - The current version of our API
- servers - How to access the API
- tags - Unique identifiers that group API operations
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
- paths - Specifies the API resources endpoints, i.e.
/employee
and/employee/{employeeId}
.post
andget
verbs define the HTTP method - tags - Group the API resource belongs. Corresponds to the tag defined in the API metadata
- summary - Information about the objective of the exposed endpoint
- description - Further information beyond the summary
- operationId - Distinct identification of the operation. This becomes the function name in the business logic implementation.
- parameters - Input of the operationId
- in - Specifies that the parameter should be scoped in the exposed path. Can be required or not, including data type in the schema section
- requestBody - Payload for the request. Included the content schema of the payload
- responses - Includes a list of HTTP status codes that are available
- content - The HTTP content type of the response message
- security - Defines the authentication or authorization required
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:
- inputSpec - Absolute path to the specification file.
- apiPackage - Creates a package,
{api}
within the target package where the API interface is generated. - modelPackage - Creates a package,
{model}
within the target package where the Schema Objects, are added. - useSpringBoot3 - Workaround for dependencies that have not bumped
javax validation API
imports tojakarta validation API
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
- Google Cloud Platform production Url ->
https://spring-boot-project-410813.uc.r.appspot.com/
- POST ->
https://spring-boot-project-410813.uc.r.appspot.com/employee
- GET ->
https://spring-boot-project-410813.uc.r.appspot.com/employee/{employeeID}
- Business logic for security configuration yet to be implemented