React + Quarkus integration using Maven
Introduction
In this post I will show you how to serve a ReactJS (or any other JavaScript SPA) front-end application using a Java Quarkus Application as a back-end and static page server. The post also includes instructions on how to configure Maven to perform the front-end build tasks along with your application packaging.
This article is very similar to my 2017 Angular + Spring Boot integration using Gradle blog post, with some added features to enable React’s router to work.
Requirements
The technology stack required for this example is:
The main goal is to achieve an npm install + build that integrates with the Maven resources plugin and deploys the distribution files to a directory where Quarkus can serve them. Quarkus will then act both as the back-end service and the gateway API to serve the front-end too.
You can find the code for these configurations in my YAKC – Kubernetes Dashboard quickstart application for YAKC (Yet Another Kubernetes Client).
Back-end (Quarkus)
In this new Microservice world, it makes sense that the front-end and back-end(s) are different services that are usually served and orchestrated by a gateway API. It’s hard to find those former web applications where front-end and back-end were part of the same service.
This distributed architecture makes sense for large enterprise projects, but sometimes we just want to prototype or quickly build a simple web application. That’s the time when you start missing those old monoliths and their simplicity that allowed you to quickly create and especially deploy your application.
The main goal is to achieve that Quarkus acts as a gateway API. So in addition to provide the REST API, it serves the front-end files too. This way we get to keep ReactJS as our main front-end technology, Quarkus as our back-end, and avoid all of the extra complexities needed for a 2 (or more) separate service deployment.
You may think that Quarkus already provides a way to serve static resources and that the solution is as simple as storing the front-end built files in this directory before building the back-end. However, this will not allow the React router to work and you wouldn’t be able to route /nested/front-end/routed/paths
(Just try to refresh your browser in one of those nested paths).
GatewayResource class
1@Path("/")
2public class GatewayResource {
3
4 private static final String FALLBACK_RESOURCE = "/frontend/index.html";
5 private final ApiResource apiResource;
6
7 @Inject
8 public GatewayResource(ApiResource apiResource) {
9 this.apiResource = apiResource;
10 }
11
12 @Path("/api/v1")
13 public ApiResource getApiResource() {
14 return apiResource;
15 }
16
17 @GET
18 @Path("/")
19 public Response getFrontendRoot() throws IOException {
20 return getFrontendStaticFile("index.html");
21 }
22
23 @GET
24 @Path("/{fileName:.+}")
25 public Response getFrontendStaticFile(@PathParam("fileName") String fileName) throws IOException {
26 final InputStream requestedFileStream = GatewayResource.class.getResourceAsStream("/frontend/" + fileName);
27 final InputStream inputStream = requestedFileStream != null ?
28 requestedFileStream :
29 GatewayResource.class.getResourceAsStream(FALLBACK_RESOURCE);
30 final StreamingOutput streamingOutput = outputStream -> IOUtils.copy(inputStream, outputStream);
31 return Response
32 .ok(streamingOutput)
33 .cacheControl(CacheControl.valueOf("max-age=900"))
34 .type(URLConnection.guessContentTypeFromStream(inputStream))
35 .build();
36 }
37
38}
This class is the main entry point for any HTTP request that the application receives.
The first method, getApiResource
delegates any request to /api/v1
to the ApiResource
subresource instance that will take care of it.
The getFrontendRoot
will take care of any request to the root path /
by requesting the index.html
file to the getFrontendStaticFile
which I’ll describe next.
Finally the getFrontendStaticFile
will take care of the rest of the requests. The method first tries to locate any file with the provided file name in the /frontend
resource directory. This directory should be packaged during the application build phase and contains the ReactJS built application.
If no file is found with this name, it will serve the index.html
file. This is the part that allows the React router to work. Any request not matched either by the API subresource or a front-end file will serve the React application which will then take care of performing a “front-end” routing.
Front-end (React)
Your complete front-end application may be located anywhere within your project, in my case I’ve located it under src/main/frontend
. Besides noting the path location for your React application, you don’t really need any extra configuration for this procedure to work.
Maven build
For the final step to integrate the React front-end with the Quarkus back-end you need to tune the Maven build.
Resources
1<!-- ... -->
2 <build>
3 <resources>
4 <resource>
5 <directory>src/main/resources</directory>
6 </resource>
7 <resource>
8 <directory>src/main/frontend/build</directory>
9 <targetPath>frontend</targetPath>
10 </resource>
11 </resources>
12 <!-- ... -->
13 </build>
14 <!-- ... -->
In this first excerpt, I’ve added a new entry to the build resources configuration. Besides the standard src/main/resources
, the directory where the React application is built src/main/frontend/build
will also be assembled and copied to target/classes/frontend
directory. If you recall, in the GatewayResource
class, I was loading static resources from this directory.
Npm build + install
1<!-- ... -->
2 <profiles>
3 <profile>
4 <id>build-frontend</id>
5 <build>
6 <plugins>
7 <plugin>
8 <groupId>org.codehaus.mojo</groupId>
9 <artifactId>exec-maven-plugin</artifactId>
10 <version>${version.exec-maven-plugin}</version>
11 <executions>
12 <execution>
13 <id>npm-install</id>
14 <phase>generate-resources</phase>
15 <goals>
16 <goal>exec</goal>
17 </goals>
18 <configuration>
19 <workingDirectory>src/main/frontend</workingDirectory>
20 <executable>npm</executable>
21 <arguments>
22 <argument>install</argument>
23 </arguments>
24 </configuration>
25 </execution>
26 <execution>
27 <id>npm-build</id>
28 <phase>generate-resources</phase>
29 <goals>
30 <goal>exec</goal>
31 </goals>
32 <configuration>
33 <workingDirectory>src/main/frontend</workingDirectory>
34 <executable>npm</executable>
35 <arguments>
36 <argument>run</argument>
37 <argument>build</argument>
38 </arguments>
39 </configuration>
40 </execution>
41 </executions>
42 </plugin>
43 </plugins>
44 </build>
45 </profile>
46 <!-- ... -->
47 </profiles>
The second excerpt is used to run npm install
and npm run build
from Maven. I added this as a separate profile because I usually perform these steps manually while in my local machine. I only invoke them in the CI, so my CI build step looks like mvn -Pbuild-frontend clean package
. However, there’s no problem in adding these configurations directly to your pom.xml
build section.
As you can see I configured 2 executions for the Exec Maven Plugin. The first one will invoke npm install
using the front-end home directory (src/main/frontend
) as its working directory. The second execution has a similar configuration but runs npm run build
instead.
Execution
Once your application is packaged, you can run it either by running the generated jar (java -jar ./target...
) file or by running mvn quarkus:dev
.
Development mode
If you want to enjoy and benefit from both Quarkus and React hot/live reloading you will need some extra configuration.
In this case, we will run the front-end using the standard npm start
and pointing our browser to http://localhost:3000
. And we’ll run our back-end using mvn quarkus:dev
in http://localhost:8080
.
Back-end
Since the front-end is served from a different URL, we need to enable CORS in Quarkus.
We can do this by modifying our application.properties
to include:
1quarkus.http.cors=true
2quarkus.http.cors.origins=http://localhost:3000
3quarkus.http.cors.headers=accept, origin, authorization, content-type, x-requested-with
4quarkus.http.cors.methods=GET,POST,DELETE,OPTIONS
These properties enable CORS to allow requests from the front-end dev origin. You can modify the entries to make it more or less restrictive depending on your needs.
Front-end
For the front-end part, we need to tune our React application to hit different back-end endpoints depending on the environment we’re running it on.
In my case, since I’m using create-react-app, I added two environment property files:
.env
for production:
1REACT_APP_API_URL=/api/v1
.env.development
for dev:
1REACT_APP_API_URL=http://localhost:8080/api/v1
Once these two files are ready, create-react-app takes care of the appropriate replacements in your application depending on your target environment. In order to use those values, you’ll need to reference the URL using the process.env.REACT_APP_API_URL
variable:
1const requestDelete = async pod => {
2 await fetch(
3 `${process.env.REACT_APP_API_URL}/pods/${metadata.selectors.namespace(pod)}/${metadata.selectors.name(pod)}`,
4 {method: 'DELETE'}
5 );
6 }
Conclusion
In this post, I showed you how to serve both a JavaScript front-end and a Java back-end application using Quarkus. Quarkus acts as an API gateway and either serves the front-end static files or processes the back-end request using resources and services. I’ve also shown you how to combine and use hot reload in React and Quarkus by tuning the settings for each application.
I’ve extracted the code for this post from my YAKC – Kubernetes Dashboard quickstart. You can learn more by visiting YAKC’s GitHub project site. You can also see the related code here.
Comments in "React + Quarkus integration using Maven"
Is this still considered microservices?
Could you expand on this topic a bit more on how the different services would be deployed individually using the quarkus rest client?
Thank you for sharing this article.
I wouldn't consider what I depict in this post a microservice on its own. However, if this monolithic application (frontend+backend) was to be deployed as part of a distributed system and the architecture was designed according to a microservice philosophy (do one thing and do it well), I guess you could consider that as a microservice (micromonolith of sorts ;p).
From my experience, front-ends are usually deployed as separate microservices. An API gateway (such as Zuul, NGINX, Traefik) is then used to redirect traffic to one service or another depending on certain parameters (e.g. HTTP path). Any of the resources served by the
ApiResource
class could be "easily" deployed as separate services (the left part of the diagram). You could then tune the gateway to serve traffic to those services by using a pattern matcher on the HTTP path.In this example the
GatewayResource
class represents this otherwise external API gateway. So another thing you could do is use this service as the front-end + API gateway server and use the Quarkus REST client to redirect traffic to other microservices (I've also seen this approach in some projects). The advantage of this is that you could use programmatic rules to compute where traffic should get redirected. However, this doesn't follow the microservice philosophy since any change to the front-end would require a redeploy of the gateway (not doing a single thing and doing it well). You'd also lose the option (or at least make it much harder) to do front-end canary deployments, etc.I'm not sure if this clarified your doubts or introduced new ones. Anyway, thanks for reading ;)
Thanks for sharing
If I start the React app independently it's working fine.
The root path of my quarkus app is /klon/. it's not the default /
What could be the issue?
thanks
In this example there is a
build-frontend
Maven profile that runsnpm install
andnpm run build
for you. Are you sure you're invoking the maven command correctly (mvn -Pbuild-frontend clean package
)?