Quarkus for Architects who Sometimes Write Code - API Server & Client
This is the second post in a series that I started to take you on a journey with me writing Quarkus applications with cloud native runtimes.
The first post is here: Quarkus for Architects who Sometimes Write Code - Introduction
This week we’re going to write a simple API server and client. The client is going to use an internal scheduler to fire its calls to the server.
First: Install a helper script
I will also be using one of the utility scripts that I wrote for managing tasks in my own home lab. You don’t need to install the whole project right now, but you’ll need it later if you decide to set up your own OpenShift cluster and developer tooling.
The scripts and home lab configuration files are at: https://github.com/cgruver/kamarotos
The only script from that bundle that we need is: https://raw.githubusercontent.com/cgruver/kamarotos/main/bin/code
Do the following to install it:
mkdir -p ${HOME}/okd-lab/bin
curl -o ${HOME}/okd-lab/bin/code -fsSL https://raw.githubusercontent.com/cgruver/kamarotos/main/bin/code
chmod 700 ${HOME}/okd-lab/bin/code
Now, edit your ~/.zshrc
or ~/.bashrc
file and add ${HOME}/okd-lab/bin
to your $PATH
For example:
echo "PATH=$PATH:${HOME}/okd-lab/bin" >> ~/.zshrc
The script that you just grabbed is very opinionated toward the way I like to organize my code when I am prototyping. It also wraps the Quarkus CLI, and includes a couple of functions for adding properties and dependencies to a pom.xml
file. I really hate manually modifying the POM file… It’s just a quirk of mine… so I wrote a couple of functions to do the most common tasks.
Create the API Server and Client Projects
Let’s go ahead and use this script to bootstrap two projects for us:
-
Create the API server project scaffolding:
mkdir -p ${HOME}/okd-lab/quarkus-projects cd ${HOME}/okd-lab/quarkus-projects code --create -b -a=apiserver -g=fun.is.quarkus
-
Take a look at what the script did:
As I mentioned, this script wraps the Quarkus CLI to bootstrap your project structure.
I am using it in an opinionated way:
quarkus create app --maven --java=${JAVA_VER} --no-wrapper --no-code --package-name=${GROUP_ID}.${APP_NAME} --extensions=${BASE_EXTENSIONS}${ADD_EXTENSIONS} ${QUARKUS_VERSION} ${GROUP_ID}:${APP_NAME}:0.1
The base extensions that I include by default are:
resteasy-reactive-jackson
,rest-client-reactive-jackson
,smallrye-health
, andconfig-yaml
.I use
config-yaml
mainly just to remind folks that you can use YAML, and JSON for that matter, for your application config files. While it creates a less compact file than thepath.property=value
that you are likely used to, it does lend itself to automation.You’ll also notice that I’m using Maven, not Gradle. No particular reason, I just haven’t had a good reason to switch. I don’t have an opinion in the Gradle vs. Maven death match.
I’m also creating an opinionated directory structure:
apiserver ├── README.md ├── pom.xml └── src ├── main │ ├── docker │ │ ├── Dockerfile.jvm │ │ ├── Dockerfile.legacy-jar │ │ ├── Dockerfile.native │ │ └── Dockerfile.native-micro │ ├── java │ │ └── fun │ │ └── is │ │ └── quarkus │ │ └── apiserver │ │ ├── aop │ │ ├── api │ │ ├── collaborators │ │ ├── dto │ │ ├── event │ │ ├── mapper │ │ ├── model │ │ └── service │ └── resources │ └── application.yml └── test └── java └── fun └── is └── quarkus └── apiserver
The subdirectories under
src/main/java
are intended to be used as follows:Directory Description aop
AOP Interceptors api
Java Interfaces that define the API resources exposed by this application collaborators
Java Interfaces that define the API resources consumed by this application dto
Java Record Interfaces that define the JSON payloads produced or consumed by this application event
Messaging handlers, for example: Kafka or Event Bus mapper
Java Interfaces that define Mapstruct mappings from DTO to Entity model
The Entity objects that define the persistence model for this application service
The Objects that contain the business logic for this application Love it or hate it… This is how I like to organize my code. So, there you have it. ;-)
-
Now that we’ve reviewed the structure, create the API client application:
cd ${HOME}/okd-lab/quarkus-projects code --create -b -a=apiclient -g=fun.is.quarkus -x=scheduler
-
Add Mapstruct and Lombok dependencies to the POMs.
cd ${HOME}/okd-lab/quarkus-projects/apiserver code --dependency -g=org.mapstruct -a=mapstruct -v=1.5.2.Final code --dependency -g=org.mapstruct -a=mapstruct-processor -v=1.5.2.Final code --dependency -g=org.projectlombok -a=lombok -v=1.18.24 code --dependency -g=org.projectlombok -a=lombok-mapstruct-binding -v=0.2.0
cd ${HOME}/okd-lab/quarkus-projects/apiclient code --dependency -g=org.mapstruct -a=mapstruct -v=1.5.2.Final code --dependency -g=org.mapstruct -a=mapstruct-processor -v=1.5.2.Final code --dependency -g=org.projectlombok -a=lombok -v=1.18.24 code --dependency -g=org.projectlombok -a=lombok-mapstruct-binding -v=0.2.0
Note: We’re not going to use Lombok or Mapstruct this week, but they’ll be there when we add more functionality next week. Plus, I just wanted you to use my helper script to add some dependencies to your
pom.xml
… -
Make sure that the dependencies can be resolved and that everything is clean
cd ${HOME}/okd-lab/quarkus-projects/apiserver mvn clean package cd ${HOME}/okd-lab/quarkus-projects/apiclient mvn clean package
-
Now, import these two projects into your IDE, and let’s put some code in them.
Code the API Server
OK, let’s write our API server. Eventually we’ll get to some API first development with the OpenAPI spec. For now, what we’re doing is so simple, it’s not worth the extra lifting. However, if you want to take a look at some good resources, check out this link: Contract-First Development
We’re also going to ignore testing this week… So, not TDD…
-
Create the API interface:
Create a new file named
ServerApi.java
in theapi
folder:src/main/java/fun/is/quarkus/apiserver/api
Add the following content:
package fun.is.quarkus.apiserver.api; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import fun.is.quarkus.apiserver.dto.MessageDto; @Path("/api-test") public interface ServerApi { @POST @Path("/message") @Consumes(MediaType.APPLICATION_JSON) public Response receiveMessage(MessageDto message); }
-
Create the DTO:
Create a new Record named
MessageDto
in thedto
folder.Add the following content:
package fun.is.quarkus.apiserver.dto; import java.util.UUID; public record MessageDto(UUID messageId, String message){}
Note: We’re using the new
record
type introduced back in Java 14, I think? It’s now a supported part of the Java 17 LTE release. It’s a powerful and easy feature that eliminates a lot of boiler plate. -
Create the API implementation:
Create a new Class named
ApiService.java
in theservice
folder.Add the following content:
package fun.is.quarkus.apiserver.service; import java.util.UUID; import javax.enterprise.context.ApplicationScoped; import javax.ws.rs.core.Response; import org.jboss.logging.Logger; import fun.is.quarkus.apiserver.api.ServerApi; import fun.is.quarkus.apiserver.dto.MessageDto; @ApplicationScoped public class ApiService implements ServerApi { final Logger LOG = Logger.getLogger(ApiService.class); @Override public Response receiveMessage(MessageDto message) { LOG.info("Received Message: " + message); return(Response.ok(new MessageDto(UUID.randomUUID(), "Hello To You!")).build()); } }
-
Add the application configuration:
Modify the file
src/main/resources/application.yml
so that it looks like:quarkus: application: name: apiServer http: port: ${PORT} log: level: "INFO" console: enable: true
Note: We’ll be setting the http listen port via an environment variable. This will be very useful later when we’re deploying to OpenShift and setting our configuration via
ConfigMap
. -
Now let’s fire it up and test it out.
Yes… we could/should write tests… We’ll do that later. After all, this is Quarkus for Architects who sometimes write code. We preach about TDD, but do we practice it? I’m hoping to get better at it, and you will too.
cd ${HOME}/okd-lab/quarkus-projects/apiserver PORT=4080 quarkus dev --no-debug
-
Send a POST to the API resource:
curl -X POST localhost:4080/api-test/message -H 'Content-Type: application/json' -d "{\"messageId\":\"$(uuidgen)\",\"message\":\"Hello Quarkus\"}" | jq
Expect to see output similar to:
{ "messageId": "d9b6efbd-5f83-4141-93a5-b016326030bf", "message": "Hello To You!" }
Note: if you don’t have
jq
installed, you should. It is very handy for dealing with JSON on the CLI.
That’s it for the server. Now let’s build a client to call the server API resource.
Code the API Client
The API Client needs to know the contract for the API. If we had created an OpenAPI spec first, we could just use that. But since we didn’t, and we have the code for the server, that’s not a problem. We can literally copy it from the server.
-
Create the message payload, aka DTO.
Create a file named
MessageDTO.java
in the client’sdto
folder, and populate it as follows:package fun.is.quarkus.apiclient.dto; import java.util.UUID; public record MessageDto(UUID messageId, String message){}
Note: Other than the package name, it is identical to
MessageDto
in the server code. -
Create the API interface that the client will connect to.
OK. This is where my own personal convention comes in. The API server is a collaborator in the greater scheme of our little enterprise application. So, when I prototype stuff like this, I put the API interfaces in a
collaborators
package. For me, this makes it easy to distinguish between the APIs that a given piece of code is serving vs. consuming.Create a file named
ClientApi.java
in thecollaborators
folder, and populate it as follows:package fun.is.quarkus.apiclient.colaborators; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import fun.is.quarkus.apiclient.dto.MessageDto; @Path("/api-test") public interface ClientApi { @POST @Path("/message") @Consumes(MediaType.APPLICATION_JSON) public Response receiveMessage(MessageDto message); }
Note: Take a look at the similarities and differences between the
ClientApi
andServerApi
interfaces. The similarities are intentional. They both define the exact same API resource. The differences, like package name and interface name, are arbitrary. Other than changing the package name, I could have copied theServerApi
code exactly.Now our client application knows how to talk to the server application via its API resource. We’ll tell the client how to find the API resource when we create the configuration.
-
Create the client’s business logic:
The last thing that we need is some sort of logic that invokes the server’s API resource and does something useful. Or, in this case, not very useful at all. But maybe fun. For a given value of fun…
I’m going to reveal a little bit of what’s to come here so that you get a glimpse at why I’m doing this. I started out attempting to write a simple service for leader election across a multi-region app that needed a singleton processor for data or events. I quickly realized that I needed to learn a few things to make it work properly. Like I said, I write code but would never consider myself to be a software engineer. Maybe a software tinkerer.
So, in order to learn the new techniques that I needed, I started writing little snippets of working code that isolated the new idea that I was working on learning. Thus the genesis of this blog series. I decided to share the journey with you.
OK. Let’s create the logic for our client application.
Create a file named
ClientApp.java
in theapiclient
root folder, and populate it as follows.package fun.is.quarkus.apiclient; import java.net.URI; import java.util.UUID; import javax.inject.Singleton; import javax.ws.rs.core.Response; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.rest.client.RestClientBuilder; import org.jboss.logging.Logger; import fun.is.quarkus.apiclient.colaborators.ClientApi; import fun.is.quarkus.apiclient.dto.MessageDto; import io.quarkus.scheduler.Scheduled; @Singleton public class ClientApp { final Logger LOG = Logger.getLogger(ClientApp.class); @ConfigProperty(name = "api-server.url") private String url; @Scheduled(every = "{api-server.schedule}") public void sendMessage() { LOG.info("Scheduler Fired"); MessageDto message = new MessageDto(UUID.randomUUID(), "Hello There"); LOG.info("Sending message: " + message); ClientApi api = RestClientBuilder.newBuilder().baseUri(URI.create(this.url)).build(ClientApi.class); try { Response response = api.receiveMessage(message); LOG.info(response.getStatus()); MessageDto responseMessage = response.readEntity(MessageDto.class); LOG.info(responseMessage); } catch (Exception e) { LOG.error(e.getMessage()); } } }
Take a few minutes to study this class. There are a few things that I’ll point out.
-
I’m using
@Singleton
here for the scope of this class. It will be instantiated as soon as the app starts, and there will only be one of it.Since we are using imperative coding here, the singleton nature of the class could cause problems with concurrency and blocking if this class gets complicated.
We’ll resolve that next time by switching to reactive code.
-
The
sendMessage
method is going to be invoked on a schedule. The timing will be provided by external configuration. You’ll see that in a bit. -
I’m not using
@RegisterRestClient
or@RestClient
in this app. Instead I am usingRestClientBuilder
to dynamically build the API client resource.I’m doing this because the leader elector will have to be able to call regionally dispersed instances of itself at changing URLs.
-
-
Now, let’s configure that app.
Modify the file
src/main/resources/application.yml
so that it looks like:quarkus: application: name: apiClient http: port: ${PORT} log: level: "INFO" console: enable: true api-server: url: "${SERVER_URL}" schedule: "${HEARTBEAT}"
Note: We are externalizing the configuration values for the client’s HTTP port, the API Server’s URL, and the timing of the
sendMessage
method.
Fire it all Up
-
It’s time to see this little contraption at work. If you still have the API server running from the previous section, then leave it alone.
If you stopped the API server, go ahead and start it back up:
cd ${HOME}/okd-lab/quarkus-projects/apiserver PORT=4080 quarkus dev --no-debug
-
Open a new terminal window, and start the API client application:
cd ${HOME}/okd-lab/quarkus-projects/apiclient PORT=4090 SERVER_URL=http://localhost:4080 HEARTBEAT=10s quarkus dev --no-debug
-
You should see output from the Client app logs that looks like:
2022-08-02 09:35:59,004 INFO [fun.is.qua.api.ClientApp] (executor-thread-1) Scheduler Fired 2022-08-02 09:35:59,005 INFO [fun.is.qua.api.ClientApp] (executor-thread-1) Sending message: MessageDto[messageId=9de2b7d6-118a-4e72-8b24-51d0da8b24bb, message=Hello There] 2022-08-02 09:35:59,019 INFO [fun.is.qua.api.ClientApp] (executor-thread-1) 200 2022-08-02 09:35:59,020 INFO [fun.is.qua.api.ClientApp] (executor-thread-1) MessageDto[messageId=b5a459d4-0693-4139-9652-ec2b861d2d86, message=Hello To You!]
-
From the API server app you should see:
2022-08-02 09:36:19,013 INFO [fun.is.qua.api.ser.ApiService] (executor-thread-0) Received Message: MessageDto[messageId=acb292ee-4472-4daf-9baf-0858e6e4710b, message=Hello There]
-
The logs should progress with new request/response about every 10 seconds.
There you have it!
See you next time. We’ll switch from Imperative to Reactive.
Cheers.