Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Hybrid Spring Boot and React or Angular: A Better Way

DZone's Guide to

Hybrid Spring Boot and React or Angular: A Better Way

In this post, a developer presents a better solution for creating a hybrid development project using React or Angular and Spring Boot.

· Web Dev Zone ·
Free Resource

Learn how error monitoring with Sentry closes the gap between the product team and your customers. With Sentry, you can focus on what you do best: building and scaling software that makes your users’ lives better.

This article presents a better solution for a hybrid React or Angular and Spring Boot project. It uses create-react-app or ng new and overlays a Spring Boot Maven project on top, which is the inverse of other approaches. The goal is to retain a first-class React or Angular dev environment alongside Spring Boot and attempt to find a no-comprises approach for both.

If you want to see the solution, please jump ahead in this article. All source code is available at https://github.com/murphye/hybrid-react-angular-spring-boot-apps

First, I want to give further background and insights which could be helpful.

Why a Hybrid Project?

There are several reasons why a team might want to combine their Spring Boot and React or Angular code together into one project. In an ideal world, they would remain separate, but the reality on the ground is different in many cases. Here are a few big reasons:

  • Need a homogenous Spring Boot deployment across all applications; adding Node or static asset deployments (i.e. S3), or a web server (i.e. NGINX) into the mix increases complexity or may not even be feasible
  • Need guarantees that the frontend and backend API are on the same version with a consolidated deployment; no API gateway or mature API versioning scheme is available
  • Lack of maturity or simply lack of time and resources to have separate deployments
  • Undergoing modernization effort to move to React or Angular and want to keep things together before separating the deployments; taking small steps and may need to mix old and new views together during the process

Notice that all of these have nothing to do, in particular, with the code. These are operational problems that are very real for many teams. There is likely to be very little synergy between your Java and JavaScript/TypeScript code when put together into a hybrid project. That being said, it can be done and many solutions have been offered in the past.

The Big Challenge

Spring Boot (via Maven or Gradle), and frontend SPA code, in the past, have been very clunky to integrate. Tools like Grunt required a lot of configuration and had their limits. The SPA framework may have had unbendable rules for directory structure. Maven is expecting a certain directory structure that has been generally incompatible with front-end code. This forced you into moving the front-end project down in src/main/resources and make configuration changes that could be haphazard and unreliable as compared to a native, Node-based front-end project. In effect, the front-end code was a second-class citizen in such hybrid projects.

Additionally, running automated builds and tests of both the Java and JavaScript code could be problematic, and there could be operational problems with installing and maintaining Node on a build server, especially with the constantly changing JavaScript libraries and new iterations of Node and its tools.

Taking the Inverse Approach

What if we could start with an unblemished React or Angular project and build a Spring Boot/Maven project into it, leaving the front-end code and structure untouched and independent from the rest? Then a front-end developer would work as normal, with a Node-based project, just with some Spring Boot/Maven stuff interwoven into the project. In the past, this may not have been possible, but in today’s world, with React and Angular, it is.

React Approach

When you run:

npx create-react-app react-app

You get an opinionated, runnable React starter application with a defined structure, which looks like (not comprehensive):

react-app/
    package.json
    package-lock.json
    yarn.lock
    public/
        index.html
    src/
        App.js
        App.css
        index.js
        Index.css

Overlay Spring Boot

Next, let’s overlay Spring Boot onto the react-app React project.

curl https://start.spring.io/starter.zip -d dependencies=web,devtools -d bootVersion=1.5.10.RELEASE -o spring-app.zip; unzip -B spring-app.zip -d react-app; rm spring-app.zip

So, now we have new additions to our project, which are (not comprehensive):

react-app/
    pom.xml
    src/
        main/
            java/
            resources/
                static/
                templates/
                application.properties
        test/

The biggest takeaway here is that src/ contains source code for both React and Spring Boot, but there is still clear separation as the Java code is under main/ and test/.

If you don’t care for this mixing of code, then please consider the multi-module section later in this article.

Note: Be sure to merge your .gitignore with .gitignore~ which was backed up by the unzip command.

Building Independently

Both React and Spring Boot should be able to build independently without conflict. You can test this theory with:

yarn build
mvn clean install

Now you will have a successful build for each, with these new directories (not comprehensive):

node_modules/
build/
    static/
       css/
       js/
       media/
    index.html
target/
    classes/
        application.properties
        com/
    demo-0.0.1-SNAPSHOT.jar

Running Independently

Next, we need to fine tune where Spring Boot reads static assets from. Add this to your application.properties (or application.yml):

spring.resources.static-locations=file:build,classpath:/public,classpath:/static

Both React and Spring Boot should be able to run independently without conflict for development. You can test this theory (from separate terminals) with:

yarn start
mvn spring-boot:run

Go to both http://localhost:8080/ and http://localhost:3000/ to see the React welcome page. 8080 is being served from react-app/build while 3000 is the React/Node development build. You are not looking at the same front-end code, and this is likely the way you want it to be. Serving React assets from 8080 is meant for compiled React code, and, for development, you should take advantage of running on Node as a developer.

Running Tests Independently

Both React and Spring Boot should be able to run independently without conflict for development. You can test this theory with:

yarn test
mvn test

Bundling React Build With Spring Boot Package

Next, we begin to integrate the builds together. The first thing we can do is copy the build/ directory produced by yarn build to the target/classes/public directory.

Add the Maven Resources Plugin to your pom.xml as shown:

<plugin>
    <artifactId>maven-resources-plugin</artifactId>
    <version>3.0.2</version>
    <executions>
        <execution>
            <id>prepare-package</id>
            <phase>prepare-package</phase>
            <goals>
            <goal>copy-resources</goal>
            </goals>
            <configuration>
                <outputDirectory>${basedir}/target/classes/public</outputDirectory>
                <resources>
                    <resource>
                    <directory>${basedir}/build</directory>
                    </resource>
                </resources>
            </configuration>            
        </execution>
    </executions>
</plugin>

Now run mvn clean install to regenerate the build artifacts. Next, do cd target and java -jar demo-0.0.1-SNAPSHOT.jar. You should still see the React welcome screen, but this time it will be served from the classpath:/public directory. You can see public inside of the target/classes directory as well.

Integrate Testing

Next, we want to execute the JavaScript tests along with the Java tests when doing mvn test. This can be done by adding this to your pom.xml:

<plugin>
    <groupId>com.github.eirslett</groupId>
    <artifactId>frontend-maven-plugin</artifactId>
    <version>1.6</version>
    <executions>
        <execution>
            <id>install node and yarn</id>
            <goals>
                <goal>install-node-and-yarn</goal>
            </goals>
            <configuration>
                <nodeVersion>v9.9.0</nodeVersion>
                <yarnVersion>v1.5.1</yarnVersion>
            </configuration>
        </execution>
        <execution>
            <id>yarn</id>
            <goals>
                <goal>yarn</goal>
            </goals>
            <phase>prepare-package</phase>
            <configuration>
            </configuration>
        </execution>
        <execution>
            <id>yarn build</id>
            <goals>
                <goal>yarn</goal>
            </goals>
            <phase>prepare-package</phase>
            <configuration>
                <arguments>build</arguments>
            </configuration>
        </execution>
        <execution>
            <id>test</id>
            <goals>
                <goal>yarn</goal>
            </goals>
            <phase>test</phase>
            <configuration>
                <arguments>test</arguments>
                <environmentVariables>
                    <CI>true</CI>
                </environmentVariables>
            </configuration>
        </execution>
    </executions>
</plugin>

Not only will the yarn test execute, but Node and Yarn will be downloaded and installed. Please note that yarn test for Jest will spit out results as [ERROR] when they are not actually errors. This is a known “feature” and is not a bug.

While not necessary, adding the Node and Yarn installation capability is a good idea for developers to have more control over what’s run on CI/CD servers in regards to Node and associated tools (i.e. Yarn). Versions change frequently and Ops probably doesn’t want to manually install these tools on the CI/CD server anyways. That being said, Ops should be aware that this has been added, as it can slow down the build process. This capability almost demands having a localized repository for Node artifacts (i.e. Nexus) for speed of downloading npm artifacts, so that is worth discussing if one is currently not in place. Then the Maven plugin can be configured to use that localized repository with <npmDownloadRoot>.

Finally, if the yarn test fails, the mvn test phase will also fail. This is probably what you want to happen as it will instill confidence that both the front-end and backend are ready to go to production as part of a build.

Legacy Front-end Code

If you are making a transition to React from something else, you can keep your older assets in src/main/resources/static and they will also be served up by Spring Boot. Be aware, however, that you will likely need to disable the Service Worker which is installed by default with React, otherwise your HTTP links to the legacy assets may not work.

CORS for React Development

While not needed for production, you may want to enable CORS for localhost:3000 so your frontend can call your backend APIs running on localhost:8080 when you are doing local development. Here is a guide on how this can be done. Otherwise, you can just do yarn build to see updated assets in ./build via Spring Boot (see diagram below). The developer workflow between the front-end and backend really depends on your team’s preferences.

Summary for React and Spring Boot

What has been presented here is a hybrid Spring Boot and React project that is unencumbered for either React development or Spring Development, has integrated testing, and has integrated building of React assets with the Spring project. Here is a diagram that represents the workflow of this configuration:

You can see the end result of the React and Spring Boot integration on GitHub here.

Angular 5 Approach

This approach, using the latest version of Angular, is virtually identical to the React approach.

Angular now supports Yarn (since Angular CLI 31), which you should consider using because it’s faster and more reliable for downloading and installing dependencies than npm. This is especially important when running your code through CI/CD processes. You can enable Yarn as the Angular package manager like this:

ng set — global packageManager=yarn

Now, you can do the following to create an Angular project:

ng new ng-app 

You will get a structure like this (not comprehensive) that is quite similar to React:

ng-app/
    package.json
    yarn.lock
    src/
        index.html
        main.ts
        styles.css
        app/
        assets/
        environments/

When you do yarn build, it will put the compiled assets in dist/ (rather than build/, like React). As such, the only real difference, as opposed to the React approach, is to use this for your application.properties:

spring.resources.static-locations=file:dist,classpath:/public,classpath:/static

Also, add the Maven Resources Plugin to your pom.xml as shown:

<plugin>
    <artifactId>maven-resources-plugin</artifactId>
    <version>3.0.2</version>
    <executions>
    <execution>
        <id>prepare-package</id>
        <phase>prepare-package</phase>
        <goals>
        <goal>copy-resources</goal>
        </goals>
        <configuration>
            <outputDirectory>${basedir}/target/classes/public</outputDirectory>
            <resources>
                <resource>
                <directory>${basedir}/dist</directory>
                </resource>
            </resources>
        </configuration>            
    </execution>
    </executions>
</plugin>

Follow along with the React instructions, and you should be good to go to run on 8080. Note that when you run yarn start, Angular will run on port 4200 rather than 3000, which will be obvious when you do it.

Also, if you can’t use Yarn, you should be able to make simple amendments to the approach to use npm and any other tools which you depend on, as long as they are supported by the Frontend Maven Plugin.

Finally, in order to run in a CI/CD environment, and through mvn test, you will need to convert the Karma tests to use PhantomJS. You can see an example of how I did this here. To add the Karma PhantomJS dependency for yarn, run this command:

yarn add — dev karma-phantomjs-launcher@^1.0.4

Summary for Angular 5 and Spring Boot

With a bit of additional work for converting the tests to use PhantomJS, a hybrid Angular 5 and Spring Boot project is almost exactly the same as the React version. You can see the end result on GitHub here.

Multi-Module Maven Approach

The prior instructions assumed that you are working with a flat Maven project for your application. What is possible for a hybrid React or Angular and Spring Boot project when using Maven modules?

Well, the goal here would be to separate out the code for the front-end and backend into different modules, but still have the test and build phases remain functional. Using what we’ve learned from the single module approach, we should be able to apply that towards a multi-module approach.

Let’s say that we structured our project like so (using React):

multi-module-app/
    pom.xml
    frontend/
        pom.xml
        package.json
        package-lock.json
        yarn.lock
        public/
            index.html
        src/
            App.js
            App.css
            index.js
            Index.css
    backend/
        pom.xml
        src/
            main/
                java/
                resources/
                    static/
                    templates/
                    application.properties
            test/

To get started, you may run the following series of commands to put the React application in frontend/ and the Spring Boot application in backend/.

mkdir multi-module-app
cd multi-module-app/
npx create-react-app frontend
mkdir backend
curl https://start.spring.io/starter.zip -d dependencies=web,devtools -d bootVersion=1.5.10.RELEASE -o spring-app.zip; unzip -B spring-app.zip -d backend; rm spring-app.zip

Next, we need to tie these two projects together into a multi-module Maven project by adding a root pom.xml, like so:

<?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>
    <groupId>com.example</groupId>
    <artifactId>multi-module-app</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
        <module>frontend</module>
        <module>backend</module>
    </modules>
</project>

In backend/pom.xml change the artifactId to be:

<artifactId>backend</artifactId>

Create a frontend/pom.xml to be:

<?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>

    <groupId>com.example</groupId>
    <artifactId>frontend</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.10.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

</project>

At this point, from the top level of the project (multi-module-app), you should be able to run mvn clean install to verify the project is now setup correctly.

Next, to the frontend/pom.xml, we are going to add the same exact maven-resources-plugin and frontend-maven-plugin code as used previously:

<build>
    <plugins>
        <plugin>
            <artifactId>maven-resources-plugin</artifactId>
            <version>3.0.2</version>
            <executions>
                <execution>
                    <id>prepare-package</id>
                    <phase>prepare-package</phase>
                    <goals>
                        <goal>copy-resources</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>${basedir}/target/classes/public</outputDirectory>
                        <resources>
                            <resource>
                                <directory>${project.basedir}/build</directory>
                            </resource>
                        </resources>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>com.github.eirslett</groupId>
            <artifactId>frontend-maven-plugin</artifactId>
            <version>1.6</version>
            <executions>
                <execution>
                    <id>install node and yarn</id>
                    <goals>
                        <goal>install-node-and-yarn</goal>
                    </goals>
                    <configuration>
                        <nodeVersion>v9.9.0</nodeVersion>
                        <yarnVersion>v1.5.1</yarnVersion>
                    </configuration>
                </execution>
                <execution>
                    <id>yarn</id>
                    <goals>
                        <goal>yarn</goal>
                    </goals>
                    <phase>prepare-package</phase>
                    <configuration>
                    </configuration>
                </execution>
                <execution>
                    <id>yarn build</id>
                    <goals>
                        <goal>yarn</goal>
                    </goals>
                    <phase>prepare-package</phase>
                    <configuration>
                        <arguments>build</arguments>
                    </configuration>
                </execution>
                <execution>
                    <id>test</id>
                    <goals>
                        <goal>yarn</goal>
                    </goals>
                    <phase>test</phase>
                    <configuration>
                        <arguments>test</arguments>
                        <environmentVariables>
                            <CI>true</CI>
                        </environmentVariables>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

This will allow Maven to run and produce the frontend/build directory from Yarn and then copy to target/classes. Run mvn clean install from the root level to verify.

To finalize the multi-module approach, we need to realize that we are going to bundle the front-end compiled resources with the backend fat JAR. How can this be done? Well, it’s as simple as adding this to the backend/pom.xml:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>frontend</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

Basically, the front-end has become a WebJar that can simply be added as a Maven dependency to the backend project, which then becomes the executable application. You can verify this by running spring-boot:run and checking http://localhost:8080 for the React welcome screen.

You can see the end result on GitHub here.

Conclusion

It’s easier than ever to have a hybrid Spring Boot and React or Angular project. The tooling is so much better than in years past, and putting together this hybrid integration is fairly painless. However, if you choose to go down this route with a hybrid application, try to not deviate from the standard conventions for React or Angular. Doing so may cause much heartburn later.

Please feel free to contribute Pull Requests to my project for any additional improvements or errors that you might find.

What’s the best way to boost the efficiency of your product team and ship with confidence? Check out this ebook to learn how Sentry's real-time error monitoring helps developers stay in their workflow to fix bugs before the user even knows there’s a problem.

Topics:
spring boot ,angular ,maven ,web dev ,react.js

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}