React & Spring Boot

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:

  1. Ensure the dist result is added to the JAR
  2. Generate during the generate-resources phase also the content for the dist folder
  3. 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));
    }

Paul Sterl has written 55 articles

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>