How to deploy a Java and React webapp in one JAR/WAR

Updated: 2024-01-08

Goal of this post is to produce a JAR (or with minor changes a WAR) that includes a React Frontend with a Spring Boot backend.

Frontend and backend can be started separately for development but only 1 instruction has to compile and build the artifact.

UPDATE: Node 20, React 18.2 and Spring Boot 3.2

As reminder, I think that a more elegant architecture is the separation between backend, frontend and delivery with fewer dependencies.

But ... I had a lot of requests and questions about solution presented here (and many friends prefer this one!!!), I updated it to the latest frameworks.

The alternative solution is available here, currently is only for (cough! cough!) Angular, but I am preparing the React version. I plan to add more React posts to this blog:
https://marmo.dev/angular-with-java

Here you can find the sources of the project on GitHub: https://github.com/marco76/java-react-basic

There is no best solution. You should adopt the architecture chosen by your team.

Quick demo

A quick (2 minutes) video tutorial is here, the video is based on the Angular project, but it applies for the React code too.

I will prepare the React example soon.

One JAR with Spring Boot

Your project contains 3 modules, the parent and 2 children (frontend and backend).

In this example we use Spring Boot, but you can use the same structure with other Java frameworks.

We are building a Spring fat jar that contains the backend and the frontend.

Here you can see how the project is structured:

react java file structure

The backend project artifact will contain the Java compiled classes and the React application:

react backend

To build and start the project locally:

 
git clone https://github.com/marco76/java-react-basic.git 
 
cd ./java-react-basic.git 
 
mvn clean package 
 
cd ./backend/target 
 
java -jar backend-0.1-SNAPSHOT.jar 

You should have the following result on http://localhost:8080

Console message received by react

What is doing Maven?

Maven will do the following:

  • install the missing libraries (node) and dependencies
  • build the frontend and the frontend static/public directory of Spring Boot
  • build the final Fat Jar that can be started or deployed in a container

parent pom.xml

The parent pom.xml declares the 2 modules to build. frontend for React and backend for Java.

The 2 modules can be started separately during the development. The React app will use the port 3000, the Java application will use the port 8080.

<?xml version="1.0" encoding="UTF-8"?> 
<project ...> 
  <properties> 
    <!-- this defines the java version for the project --> 
    <java.version>21</java.version> 
  </properties> 
 
  <parent> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-parent</artifactId> 
    <version>3.2.1</version> 
  </parent> 
   
  <groupId>dev.marco</groupId> 
  <artifactId>java-react-example</artifactId> 
  <packaging>pom</packaging> 
  <version>0.1-SNAPSHOT</version> 
   
  <parent> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-parent</artifactId> 
    <version>3.2.1</version> 
  </parent> 
 
  <modules> 
    <module>frontend</module> 
    <module>backend</module> 
  </modules> 
   
</project> 

Frontend pom.xml

The frontend module contains a standard React application.

During the development you can start the application with a standard npm start

The heavy lifting is done by the plugin https://github.com/eirslett/frontend-maven-plugin.

This plugin download node, install the libraries and build the project.

If you are in an Enterprise environment, probably you don't need this plugin, the libraries have to be installed using internal tools.

The result is a jar file that contains the Angular application compiled.

<?xml version="1.0" encoding="UTF-8"?> 
<project ...> 
    <parent> 
        <groupId>dev.marco</groupId> 
        <artifactId>java-react-example</artifactId> 
        <version>0.1-SNAPSHOT</version> 
    </parent> 
    <modelVersion>4.0.0</modelVersion> 
    <artifactId>frontend</artifactId> 
    <packaging>jar</packaging> 
    <build> 
      <plugins> 
          <!-- clean the dist directory used by the fronted --> 
          <plugin> 
              <artifactId>maven-clean-plugin</artifactId> 
              ... 
          </plugin> 
 
          <plugin> 
            <groupId>com.github.eirslett</groupId> 
            <artifactId>frontend-maven-plugin</artifactId> 
            <version>1.15.0</version> 
 
            <executions> 
              <!-- Install node and npm --> 
              <execution> 
                <id>Install Node and NPM</id> 
                <goals> 
                  <goal>install-node-and-npm</goal> 
                </goals> 
                <configuration> 
                  <nodeVersion>v20.10.0</nodeVersion> 
                </configuration> 
              </execution> 
 
              <!-- clean install --> 
              <execution> 
                <id>npm install</id> 
                <goals> 
                  <goal>npm</goal> 
                </goals> 
              </execution> 
 
              <!-- build app --> 
              <execution> 
                <id>npm run build</id> 
                <goals> 
                  <goal>npm</goal> 
                </goals> 
                <configuration> 
                  <arguments>run build</arguments> 
                </configuration> 
              </execution> 
            </executions> 
          </plugin> 
      </plugins> 
 
      <resources> 
        <resource> 
          <!-- we copy the content of the frontend directory in the final artifact --> 
          <directory>build</directory> 
        </resource> 
      </resources> 
  </build> 
</project> 

The Java Backend pom.xml: build, unpack and replace

You have to import the frontend module, the frontend code will be included in your final package.

There a dependency with the frontend module. This solution is not ideal because the backend module is not completely independent, as mentioned I prefer the solution with 3 modules (backend, frontend, deployment).

We use the maven-dependency-plugin to unpack the content of the frontend and copy the content in our backend artifact.

The React application is copied in the folder /classes/static, this folder is used to serve static content in Spring Boot.

<?xml version="1.0" encoding="UTF-8"?> 
<project ...> 
  <parent> 
    <groupId>dev.marco</groupId> 
    <artifactId>java-react-example</artifactId> 
    <version>0.1-SNAPSHOT</version> 
    <relativePath>../pom.xml</relativePath> 
  </parent> 
  ... 
  <artifactId>backend</artifactId> 
   
   <dependencies> 
     <dependency> 
       <groupId>${project.groupId}</groupId> 
       <artifactId>frontend</artifactId> 
       <version>${project.version}</version> 
       <type>jar</type> 
       <!-- we add optional to work with the backend package without building the frontend --> 
       <optional>true</optional> 
     </dependency> 
        ... 
    </dependencies> 
 
    <build> 
        <plugins> 
            <plugin> 
                <groupId>org.springframework.boot</groupId> 
                <artifactId>spring-boot-maven-plugin</artifactId> 
                <version>3.2.1</version> 
                <configuration> 
                    <mainClass>dev.marco.example.springboot.Application</mainClass> 
                </configuration> 
                <executions> 
                    <execution> 
                        <goals> 
                            <goal>repackage</goal> 
                        </goals> 
                    </execution> 
                </executions> 
            </plugin> 
 
            <plugin> 
              <groupId>org.apache.maven.plugins</groupId> 
              <artifactId>maven-dependency-plugin</artifactId> 
              <version>3.6.0</version> 
              <executions> 
                <execution> 
                    <id>merge</id> 
                    <phase>initialize</phase> 
                    <goals> 
                        <goal>unpack</goal> 
                    </goals> 
                    <configuration> 
                        <artifactItems> 
                            <artifactItem> 
                                <groupId>${project.groupId}</groupId> 
                                <artifactId>frontend</artifactId> 
                                <type>jar</type> 
                                <overWrite>true</overWrite> 
                                <outputDirectory>${project.build.directory}/classes/static</outputDirectory> 
                        </artifactItem> 
                            ... 
</project> 

Spring

In our example we added a REST controller to deploy and execute the basic application.

During the development phase we are starting the Spring backend application and the Angular application in separated instances.

To allow the communication between the HttpClient of Angular and the RestController of Spring we have to enable a Cross Origin Resource Sharing in Spring Boot or our requests will be refused.

@RestController 
// we allow localhost:3000 for testing purposes 
@CrossOrigin(origins = "http://localhost:3000") 
public class HelloController { 
 
    @RequestMapping(value = "/message", produces = MediaType.APPLICATION_JSON_VALUE) 
    public Map<String, String> index() { 
        return Collections.singletonMap("message", "Greetings from Spring Boot!"); 
    } 
 
} 

React

Our React application doesn't do anything special, we simply call the backend service, and we show the message received.

In this project we use TypeScript.

import React from 'react'; 
 
type MyProps = { }; 
type MyState = { data: string }; 
class HelloSpring extends React.Component<MyProps, MyState> { 
 
    constructor(props: any) { 
        super(props); 
 
        this.state = { data: ""} 
    } 
    componentDidMount() { 
        const apiURL = 'http://localhost:8080/message'; 
        fetch(apiURL) 
            .then((response) => response.json()) 
            .then((json) => { 
                this.setState({ 
                    data: json.message 
                }) 
            }) 
    } 
    render() { 
        return ( 
            <p> 
                <div> 
                    It worked, this is the message from the backend: <br/> 
        {this.state.data} 
        </div> 
        </p> 
    ) 
    } 
} 
 
export default HelloSpring; 

Deploy a WAR with Tomcat

If you need to deploy a WAR using Tomcat you can give a peek to the following branch of the Angular project.

https://github.com/marco76/java-angular-basic/tree/feature/war-for-tomcat

Major changes in relation to the JAR version:

backend/pom.xml

We change the type of packaging and we add a dependency to spring-boot-starter-tomcat

<packaging>war</packaging> 
 
<dependency> 
  <groupId>org.springframework.boot</groupId> 
  <artifactId>spring-boot-starter-tomcat</artifactId> 
  <scope>provided</scope> 
</dependency> 

backend/src/main/java/dev/marco/example/springboot/Application.java

@SpringBootApplication 
// we extend SpringBootServletInitializer 
    public class Application extends SpringBootServletInitializer { 

Bonus: features showcase

To the original minimalistic example I started to add some features. I think the example project is more interesting as showcase than as 'bootstrap'.

I added OpenAPI with SpringDocs, when you start the project is available here: http://localhost:8080/swagger-ui

I added some tests to show the difference between RestTemplateTest and MockMvc.

I planned to add other features like WebSockets, Spring Data and Spring Security.

If you are interested to see how some features are implemented you can leave a comment or send me a message.

Request a feature

You can contact me or fill a feature request if you would like to see a feature included in this showcase.


You could be interested in

Right click context menu with Angular

Right click custom menu inside dynamic lists with Angular Material
2020-05-31

logo of the category: spring-big.svg Spring Boot OpenAPI generator example

How to generate your REST methods and Spring controllers using OpenAPI / Swagger
2022-02-15
WebApp built by Marco using SpringBoot 3.2.4 and Java 21, in a Server in Switzerland without 'Cloud'.