Red Hat Developer image

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:

  1. 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.
  2. specificationUri points to the location of my API definition.
  3. The name of the rest DSL XML file to generate.
  4. 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.
  5. 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