Problem
We want to combine Spring Boot with a simple React frontend. Use the React development features but also be able to use the Spring IDE. In the end, everything should be built using Maven and nicely packed into a JAR, just like any other Microservice. Let’s get started.
Multi-Module project
First we create a spring project here backend
folder and a react project in the frontend
folder. The root pom should help us to build both projects in one run:
Root pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.foo.bar</groupId> <artifactId>project-root</artifactId> <version>0.1.0-SNAPSHOT</version> <packaging>pom</packaging> <properties> <java.version>21</java.version> <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <modules> <module>frontend</module> <module>backend</module> </modules> <!-- feel free to use spring as root instead of this --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>3.3.6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>
Backend pom.xml
This is straight forward the generated pom from spring with just the dependency to the frontend
project.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.foo.bar</groupId> <artifactId>project-root</artifactId> <version>0.1.0-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <artifactId>project-backend</artifactId> <dependencies> <!-- thats the important part --> <dependency> <groupId>org.foo.bar</groupId> <artifactId>project-frontend</artifactId> <version>${project.version}</version> </dependency> <!-- thats the important part end --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Frontend pom.xml
Now we have todo more stuff:
- Ensure the
dist
result is added to the JAR - Generate during the
generate-resources
phase also the content for thedist
folder - Ensure that during a
mvn clean
all gets deleted
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.foo.bar</groupId> <artifactId>project-root</artifactId> <version>0.1.0-SNAPSHOT</version> <relativePath>../pom.xml</relativePath> </parent> <artifactId>project-frontend</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <!-- Ensure the dist result is added to the JAR --> <resources> <resource> <directory>dist</directory> </resource> </resources> <plugins> <!-- Generate during the generate-resources phase also the content for the dist folder --> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.5.0</version> <executions> <execution> <id>npm install</id> <goals> <goal>exec</goal> </goals> <phase>generate-resources</phase> <configuration> <executable>npm</executable> <arguments> <argument>install</argument> </arguments> </configuration> </execution> <execution> <id>npm build</id> <goals> <goal>exec</goal> </goals> <phase>generate-resources</phase> <configuration> <executable>npm</executable> <arguments> <argument>run</argument> <argument>build</argument> </arguments> </configuration> </execution> </executions> </plugin> <!-- Ensure that during a mvn clean all gets deleted --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-clean-plugin</artifactId> <version>3.4.0</version> <configuration> <filesets> <fileset> <directory>node_modules</directory> </fileset> <fileset> <directory>dist</directory> </fileset> </filesets> </configuration> </plugin> </plugins> </build> </project>
Ensure the result in the dist folder is compatible to Spring
Spring will pick up automatically any static resources which are placed in the static
folder. Here is an example for vite to do so in the vite.config.js
:
export default defineConfig({ plugins: [react()], build: { outDir: './dist/static', emptyOutDir: true, // also necessary } })
Going beyond
Sometime it is useful to host the UI in a sub path lets say my-ui
, so the url would be http://localhost:8080/my-ui
For that we have to adjust the vite.config.js
:
export default defineConfig({ plugins: [react()], base: 'my-ui', build: { outDir: './dist/static/my-ui', emptyOutDir: true, } })
Now it would also be helpful if we don’t have to directly type the index.html
but tell Spring always to re-route.
For that we have to add a Spring Configuration Class:
package org.foo.bar; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class MyFrontedConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/my-ui").setViewName("/my-ui/index.html"); registry.addRedirectViewController("/my-ui/", "/my-ui"); } }
Add cache control to static react assets
In the end int would also be nice that the generated static resources are cached by the client e.g. for a year. We will extend the MyFrontedConfig
class with:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/my-ui/assets/**") .addResourceLocations("classpath:/static/my-ui/assets/") .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); }