How to use contract first approach with Open API
What is contract first approach ?
A way to develop your services, where the specification of the service is defined before the service themselves. In short, you first define the contract, and then implement the service.
What is Open API ?
As per swagger,
The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined, a consumer can understand and interact with the remote service with a minimal amount of implementation logic.
For more information please check: https://swagger.io/specification/ and https://www.openapis.org/
A sample open API specification:
How to generate server and client code from OAS ?
Once your contract is defined, it's time to generate the server side and client side code. We will be using "openapi-generator-maven-plugin" for this.
The idea is to create a contract using OAS and two APIs: service and client. The client API will be consuming the service API using the client library. We will be using the openapi-generator-maven-plugin to generate server (interface only to be implemented by the service API) and client side code (to be used by client API to consume the service API). We will use maven profile to specify the configurations to generate the server and client code respectively.
Follow along the steps to get more clarity:
Create a spring-boot maven project: https://start.spring.io/
Add the Open API contract created to the resources folder.
Open the pom.xml and add the following properties and dependencies:
properties:
<properties> <swagger-annotations.version>1.6.0</swagger-annotations.version> <jackson-databind.version>0.2.1</jackson-databind.version> <spring-fix.version>3.0.0</spring-fix.version> <project.build.directory>src/main/resources</project.build.directory> <server.suffix>server</server.suffix> <client.suffix>client</client.suffix> <api-version>v1</api-version> <base-package>com.oas.contract.first</base-package> <openapi-generator-maven-plugin.version>5.0.1</openapi-generator-maven-plugin.version> <inputspec>${project.basedir}/src/main/resources/contract-first.openapi.yaml</inputspec> </properties>
dependencies:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-commons</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.oas.contract.first.sample.server</groupId> <artifactId>contract-first-server-v1</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <!-- OAS dependencies --> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> <version>1.6.0</version> </dependency> <dependency> <groupId>org.openapitools</groupId> <artifactId>jackson-databind-nullable</artifactId> <version>0.2.1</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-core</artifactId> <version>${spring-fix.version}</version> </dependency> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> </dependency> </dependencies>
Also, create 2 different maven profile one for server and other for client code generation:
<profiles> <profile> <id>server-code</id> <build> <plugins> <plugin> <groupId>org.openapitools</groupId> <artifactId>openapi-generator-maven-plugin</artifactId> <version>${openapi-generator-maven-plugin.version}</version> <executions> <execution> <id>java-server-code-generation</id> <goals> <goal>generate</goal> </goals> <configuration> <inputSpec>${inputspec}</inputSpec> <output>${project.build.directory}/generated-sources/java</output> <generatorName>spring</generatorName> <modelPackage>${base-package}.${server.suffix}.model</modelPackage> <apiPackage>${base-package}.${server.suffix}.api</apiPackage> <invokerPackage>${base-package}.${server.suffix}.utils</invokerPackage> <groupId>${project.groupId}.${server.suffix}</groupId> <artifactId>${project.artifactId}-server-${api-version}</artifactId> <artifactVersion>${project.version}</artifactVersion> <configOptions> <library>spring-boot</library> <basePackage>${base-package}</basePackage> <configPackage> ${base-package}.${server.suffix}.config </configPackage> <interfaceOnly>true</interfaceOnly> <unhandledException>true</unhandledException> <useOptional>true</useOptional> <useTags>true</useTags> <hideGenerationTimestamp>true</hideGenerationTimestamp> <dateLibrary>java8</dateLibrary> <java8>true</java8> <booleanGetterPrefix>is</booleanGetterPrefix> </configOptions> </configuration> </execution> </executions> </plugin> </plugins> </build> </profile> <profile> <id>client-code</id> <build> <plugins> <plugin> <groupId>org.openapitools</groupId> <artifactId>openapi-generator-maven-plugin</artifactId> <version>${openapi-generator-maven-plugin.version}</version> <executions> <execution> <id>java-client-code-generation</id> <goals> <goal>generate</goal> </goals> <configuration> <inputSpec>${inputspec}</inputSpec> <output>${project.build.directory}/generated-sources/java</output> <generatorName>java</generatorName> <library>resttemplate</library> <modelPackage> ${base-package}.${client.suffix}.model </modelPackage> <apiPackage> ${base-package}.${client.suffix}.api </apiPackage> <invokerPackage>${base-package}.${client.suffix}.utils</invokerPackage> <groupId>${project.groupId}.${client.suffix}</groupId> <artifactId>${project.artifactId}-client-${api-version}</artifactId> <artifactVersion>${project.version}</artifactVersion> <generateModelTests>false</generateModelTests> <generateApiTests>false</generateApiTests> <generateApiDocumentation>false</generateApiDocumentation> <generateModelDocumentation>false</generateModelDocumentation> <configOptions> <basePackage>${base-package}</basePackage> <configPackage> ${base-package}.${client.suffix}.config </configPackage> <hideGenerationTimestamp>true</hideGenerationTimestamp> <dateLibrary>java8</dateLibrary> <java8>true</java8> <booleanGetterPrefix>is</booleanGetterPrefix> </configOptions> </configuration> </execution> </executions> </plugin> </plugins> </build> </profile> </profiles>
For server side we are using language as spring and library as spring-boot, while for client code we are using language as Java and library as resttemplate.
Once you complete the following configurations, run "mvn clean install -Pserver-code" and "mvn clean install -Pclient-code".
I have created a code-generator.sh shell script to generate the code and package the server and client libraries.
build_java() { profile=$1; service=$2 echo "building java packages: $profile $servicename" ( ./mvnw -N -q -P "$profile" clean generate-resources \ "-DartifactName=$service" \ "-Dmaven.test.skip=true" \ || { echo "cannot generate source code"; exit 0; } ( cd "target/generated-sources/java" || { echo "cannot cd to client library generated sources folder"; exit 1; } ../../../mvnw -Dmaven.test.skip=true -q clean install ) || exit 0; ) || exit 0
}
How to use the generated server library ?
Add the server side dependency to the service API's pom.xml.
Create a controller and implement the generated API interface.
Implement the business logic and run the server.
Download and import the postman-collection and hit the get-service-products endpoint.
How to use the generated client library ?
We will use the generated client library to consume the service API.
For this, create another maven spring-boot application: client API
Add the generated client library's dependency in this pom.xml
Create a rest controller with endpoint: /client/products
We need to configure the ProductsApi client with the resttemplate and set the base path of service API, as shown in the snapshot:
Now In the ProductsClientService use the ProductsApi client to getAllProducts from the service API as shown below:
Now download and import the postman-collection and hit the get-client-products endpoint.
This should return you the list of all products.
How to make publishing server and client artifacts as part of your CI/CD process ?
If you want to make the whole server and client side code generation as part of your CI/CD process then you can use the shell script and customise it as per your own needs. What we intend to do is, once the contract is designed or changed, a Jenkins job should be triggered. This job will run the code-generator shell script which will generate the client and server codes, package them and upload the artifacts to your maven repository.
These artifacts can then be used by adding the dependencies in the pom file when and wherever required.
Conclusion:
We understood what is contract first approach and open API specification. We created two APIs: one API which implements the generated server code and has the business logic, another API which uses the generated client library to consume the service API. We also understood how we can make this a part of our CI/CD process so that a developer can create/update the contracts and CI/CD tool can take care of generating the libraries.
For the whole code, please check github.