API-first design is a commonly used approach where you define the interfaces for your application before providing an actual implementation. This approach gives you a lot of benefits. For example, you can test whether your API has the right structure before investing a lot of time implementing it, and you can share your ideas with other teams early to get valuable feedback. Later in the process, delays in the back-end development will not affect front-end developers dependent on your service so much, because it's easy to create mock implementations of a service from the API definition.
Much has been written about the benefits of API-first design, so this article will instead focus on how to efficiently take an OpenAPI definition and bring it into code with Red Hat Fuse.
Imagine an API has been designed that is used for exposing a beer API. As you can see in the JSON file describing the API, it's an OpenAPI definition and each operation is identified by an operationId. That will prove to be handy when doing the actual implementation. The API is pretty simple and consists of three operations:
- GetBeer—Get a beer by name.
- FindBeersByStatus—Find a beer by its status.
- ListBeers—Get all beers in the database.
Keep generated code separate from the implementation
We don’t want to code all the DTOs and boilerplate code, because that’s very time-consuming and trivial as well. Therefore, we'll use the Camel REST DSL Swagger Maven Plug-in for generating all of that.
We want to keep the code generated by the swagger plugin separate from our implementation for several reasons, including:
- Code generation consumes time and resources. Separating code generation from compiling allows us to spend less time waiting and thus more time drinking coffee with colleagues and being creative in all sorts of ways.
- We don't have to worry that a developer will accidentally put some implementation stuff in an autogenerated class and thus lose valuable work the next time the stub is regenerated. Of course, we have everything under version control, but it's still time-consuming to resolve what was done, moving code, etc.
- Other projects can refer to the generated artifacts independently of the implementation.
To keep the generated stub separate from the implementation, we have the following initial structure:
. +-- README.md │-- fuse-impl │ +-- pom.xml │ `-- src │ │-- main │ │ │-- java │ │ `-- resources │ `-- test │ │-- java │ `-- resources `-- stub │-- pom.xml `-- src `-- spec
The folder stub
contains the project for the generated artifacts. The folder fuse-impl
contains our implementation of the actual service.
Setting up code generation with Swagger
First, configure the Swagger plugin by adding the following in the pom.xml
file for the stub project:
… <dependencies> <dependency> <groupId>org.apache.camel</groupId> <artifactId>camel-swagger-java-starter</artifactId> </dependency> … </dependencies> … <plugins> <plugin> <groupId>org.apache.camel</groupId> <artifactId>camel-restdsl-swagger-plugin</artifactId> <version>2.23.0</version> <executions> <execution> <goals> <goal>generate-xml-with-dto</goal><!-- 1 --> </goals> </execution> </executions> <configuration> <specificationUri><!-- 2 --> ${project.basedir}/src/spec/beer-catalog-API.json </specificationUri> <fileName>camel-rest.xml</fileName><!-- 3 --> <outputDirectory><!-- 4 --> ${project.build.directory}/generated-sources/src/main/resources/camel-rest </outputDirectory> <modelOutput> ${project.build.directory}/generated-sources </modelOutput> <modelPackage>com.example.beer.dto</modelPackage><!-- 5 --> </configuration> </plugin> </plugins> ...
The plugin is pretty easy to configure:
- The goal is set to
generate-xml-with-dto
, which means that a rest DSL XML file is generated from the definition together with my Data Transfer Objects. There are other options, including one to generate a Java client for the interface. specificationUri
points to the location of my API definition.- The name of the rest DSL XML file to generate.
- Where to output the generated rest DSL XML file. If placed in this location, Camel will automatically pick it up if included in a project.
- Package name for the DTOs.
In pom.xml
, we also need to change the location of the source and resource files for the compiler. Finally, we need to copy the API specification to the location we chose previously. This isn't described here because it's known stuff, but you can refer to the source code for the specifics as needed. Now, we're ready to generate the stub for the REST service.
So far, we have the following file structure in the stub
project:
. `-- stub +-- pom.xml `-- src `-- spec `-- beer-catalog-API.json
Run mvn install
in the stub
dir and the stub is automatically generated, compiled, put in a jar file, and put into the local Maven repository. The DTOs are generated in the package we chose previously. Furthermore, an XML file is created for the REST endpoint.
Contents of file stub/target/generated-sources/src/main/resources/camel-rest/camel-rest.xml
:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <rests xmlns="http://camel.apache.org/schema/spring"> <restConfiguration component="servlet"/> <rest> <get id="GetBeer" uri="/beer/{name}"> <description>Get beer having name</description> <param dataType="string" description="Name of beer to retrieve" name="name" required="true" type="path"/> <to uri="direct:GetBeer"/> </get> <get id="FindBeersByStatus" uri="/beer/findByStatus/{status}"> <description>Get beers having status</description> <param dataType="string" description="Status of beers to retrieve" name="status" required="true" type="path"/> <param dataType="number" description="Number of page to retrieve" name="page" required="false" type="query"/> <to uri="direct:FindBeersByStatus"/> </get> <get id="ListBeers" uri="/beer"> <description>List beers within catalog</description> <param dataType="number" description="Number of page to retrieve" name="page" required="false" type="query"/> <to uri="direct:ListBeers"/> </get> </rest> </rests>
The important thing to note is that each REST operation is routing to a uri
named direct:operatorId
, where operatorId
is the same operator as in the API definition file. This enables us to easily provide an implementation for each operation.
Providing an implementation of the API
For the example implementation, we chose Fuse running in a Spring Boot container to make it easily deployable in Red Hat OpenShift.
Besides the usual boilerplate code, the only thing we have to do is add a dependency to the project containing the stub in our pom.xml
file of the fuse-impl
project:
<dependency> <groupId>com.example</groupId> <artifactId>beer</artifactId> <version>1.0</version> </dependency>
Now we're all set, and we can provide our implementation of the three operations. As an example of an implementation, consider the following.
Contents of fuse-impl/src/main/java/com/example/beer/routes/GetBeerByNameRoute.java:
package com.example.beer.routes; import org.apache.camel.Exchange; import org.apache.camel.Processor; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.model.dataformat.JsonLibrary; import org.springframework.stereotype.Component; import java.math.BigDecimal; import com.example.beer.service.BeerService; import com.example.beer.dto.Beer; import org.apache.camel.BeanInject; @Component public class GetBeerByNameRoute extends RouteBuilder { @BeanInject private BeerService mBeerService; @Override public void configure() throws Exception { from("direct:GetBeer").process(new Processor() { @Override public void process(Exchange exchange) throws Exception { String name = exchange.getIn().getHeader("name", String.class); if (name == null) { throw new IllegalArgumentException("must provide a name"); } Beer b = mBeerService.getBeerByName(name); exchange.getIn().setBody(b == null ? new Beer() : b); } }).marshal().json(JsonLibrary.Jackson); } }
Here we inject a BeerService
, which holds information about the different beers. Then we define a direct endpoint, which provides the endpoint, to which the REST call is routed (remember the operationId
mentioned earlier?). The processor tries to look up the beer. If no beer is found, an empty beer object is returned. To try the example, you can run:
cd fuse-impl mvn package java -jar target/beer-svc-impl-1.0-SNAPSHOT.jar #in a separate terminal curl http://localhost:8080/rest/beer/Carlsberg {"name":"Carlsberg","country":"Denmark","type":"pilsner","rating":5,"status":"available"}
We might have to do this over and over again. In that case, we can create a Maven archetype for the two projects. Alternatively, we can clone a template project containing all the boilerplate code and do the necessary changes from there. That will be a bit more work, though, because we'll have to rename Maven modules as well as Java classes, but it's not too much of a hassle.
Conclusion
With an API-first approach, you can design and test your API before doing the actual implementation. You can get early feedback on your API from the people using it, without having to provide an actual implementation. In this way, you can save time and money.
Going from design to actual implementation is easy with Red Hat Fuse. Just use the Camel REST DSL Swagger Maven Plugin to generate a stub and you are set for providing the actual implementation. If you want to try it for yourself, use the example code as a starting point.
Last updated: June 23, 2023