Spring Boot with built-in ReactJS application
published: ⏰ 4Recently, I needed to create a web application using Spring + ReactJS. Usually, this involves two separate containers: a web server serving static files (NGINX, NodeJS, or something else) and a BFF (or the API backend itself). However, in simple cases, everything can be combined into a single service to avoid overcomplicating deployment.
Or rather, this is the story of how to build a Self-contained system using mainstream frameworks.
Introduction
As mentioned in the preamble, it’s not always necessary to have a scalable solution with separate containers. In simple cases, you can combine Spring Boot + ReactJS into a single service. At minimum, this simplifies deployment and configuration. If needed later, they can be separated.
In this approach we get:
- UI within the backend in the context of one project.
- Both UI and backend in a single container, as one deployment unit.
- A current UI without any restrictions, enabling fast development thanks to a vast number of ready-made components and a wealth of community knowledge. Plus, significant development potential through reuse of internal component libraries.
- A tailored backend, again for a specific task, with necessary integrations and the ability to leverage your own developments without worrying about external constraints.
- The freedom to use any framework, not limited by platform architecture or design principles. In other words, full flexibility in choosing technologies according to the project and client needs.
The example will use Maven + Spring Boot, Vite + ReactJS.
UI
Everything here is quite simple. The application itself is built using any framework – in my case, it was ReactJS +
Vite, but that doesn’t really matter. What’s important is to get a dist folder with all the static files.
It will look something like this:
assets
├─favicon-NfJJaaRo.ico
├─index-Bkn1a5ql.js
├─index-BzxSu595.css
├─vendor-BSpWnBBm.css
└─vendor-gTVBeMpW.js
locales
├─en
└─ru
index.html
logo.pngThese files need to be packaged into a JAR file so that they can later be included as a dependency during backend build.
In the case of Maven, this can be done using
the exec-maven-plugin by invoking the build during the
generate-sources phase.
Suppose our artifact will be built with coordinates dev.sample.myapp:myapp-ui:0.1.0. Below is an example of plugin
configuration.
exec-maven-plugin configuration
<build>
<resources>
<resource>
<directory>dist</directory>
<targetPath>static</targetPath>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<id>install-frontend-dependencies</id>
<phase>generate-sources</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<workingDirectory>src</workingDirectory>
<executable>npm</executable>
<arguments>
<argument>install</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>build-and-include-frontend</id>
<phase>generate-sources</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<workingDirectory>src</workingDirectory>
<executable>npm</executable>
<arguments>
<argument>run</argument>
<argument>build</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>During the project build, the web application will be built and included into the JAR file.
Server
The server setup is a bit more complex because we also need to configure how static resources are handled.
This can be done via WebMvcConfigurer by adding handlers to ResourceHandlerRegistry. Here’s an example of the code:
WebMvcConfigurer
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final KeycloakConfig keycloakConfig;
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
this.serveDirectory(registry, "/", "classpath:/static/");
}
private void serveDirectory(
final ResourceHandlerRegistry registry, final String endpoint,
final String location
) {
final var endpointPatterns = endpoint.endsWith("/")
? new String[]{endpoint.substring(0, endpoint.length() - 1), endpoint, endpoint + "**"}
: new String[]{endpoint, endpoint + "/", endpoint + "/**"};
registry
.addResourceHandler(endpointPatterns)
.addResourceLocations(location.endsWith("/") ? location : location + "/")
.resourceChain(false)
.addResolver(new PathResourceResolver() {
@Override
public Resource resolveResource(
final HttpServletRequest request, final String requestPath,
final List<? extends Resource> locations, final ResourceResolverChain chain
) {
if (requestPath.equals("static/runtime.js")) {
return new InMemoryResource(
"window.keycloak = { url: \"%s\", realm: \"%s\", clientId: \"%s\"}"
.formatted(keycloakConfig.getUrl(), keycloakConfig.getRealm(), keycloakConfig.getClientId())
.getBytes(StandardCharsets.UTF_8),
"static/runtime.js");
}
final var resource = super.resolveResource(request, requestPath, locations, chain);
if (nonNull(resource)) {
return resource;
}
return super.resolveResource(request, "/index.html", locations, chain);
}
});
}
}Here we intercept all requests to /static and check if the corresponding file exists in the resources. If not – we
return index.html, so that React Router can handle the route on the client side.
Additionally, as an example, InMemoryResource is used to pass dynamically generated JavaScript with Keycloak
connection parameters. On the UI side, in the index.html file, it’s necessary to include the script
/static/runtime.js.
InMemoryResource.java
public static final class InMemoryResource extends ByteArrayResource {
private final String filename;
private final long lastModified;
public InMemoryResource(final byte[] content, final String filename) {
super(content);
this.filename = filename;
this.lastModified = System.currentTimeMillis();
}
@Override
public long lastModified() throws IOException {
return lastModified;
}
@Override
public String getFilename() {
return filename;
}
}ℹ️ Note
It is important to override the
lastModifiedandgetFilenamemethods. Spring uses these methods to determine the resource’s last modification time andContent-Type(based on file extension).
index.html
<!DOCTYPE html>
<html lang="en">
<head>
...
<script src="/static/runtime.js"></script>
</head>
<body>
...
</body>
</html>Building
To include our application in the final build, we need to add the previously built JAR as a dependency (
dev.sample.myapp:myapp-ui:0.1.0) to the project and configure the application’s build.
maven-dependency-plugin configuration
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.8.1</version>
<executions>
<execution>
<id>unpack-dependencies</id>
<phase>process-resources</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<includeGroupIds>org.tekblueprint.kbase</includeGroupIds>
<includeArtifactIds>kbase-ui</includeArtifactIds>
<includes>static/**</includes>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>When building the application, the required static assets will be unpacked into the application resources (/static),
and the Spring Boot application will be able to use them as static files.
Conclusion
That’s it. You can now build, for example with spring-boot:build-image, and deploy it as a self-contained system (
SCS). I don’t want to call this application a monolith, because many people fear them and that’s the name you shouldn’t
even mention :)
ℹ️ Note
Sample project available at GitHub.