Hopp til hovedinnhold

Teknologi / 8 minutter /

Webapp with Create React App and Spring Boot

This tutorial shows you how to combine Create React App with Spring Boot, giving you one single jar file to deploy.

Create React App is a real time saver for your React frontend. It speeds up the initial phase by stitching together everything you need to get your React frontend working with minimal effort on your part, and it shortens your feedback loop by enabling hot reload. That way, you can just save your files, and changes will be immediately visible in the browser.

Spring boot does the same thing for your Spring applications. It gets you up and running quickly, and continues to make your life easier as you move to production and start releasing incremental updates to your application.

This tutorial shows you how to combine Create React App with Spring Boot, giving you one single jar file to deploy.

Download the code or fork this project at https://github.com/kantega/react-and-spring/

Goal:

  • Easily deploy to test and production environments
  • Frontend and backend in a single jar
  • Keep the benefits using the Create React App environment including:
    • Hot reload
    • Built in ES6 to ES5 transpiler
    • Optimized production build

Getting started

First, create a spring boot project with https://start.spring.io. Add the Web dependency. Set the groupId and artifactId to whatever you want. In this example we chose no.kantega and spring-and-react.

The startpage of start.spring.io
The startpage of start.spring.io

Generate the project and unzip it into your project directory. You probably want to initialize git, add a .gitignore and make your initial commit at this point.

1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4    <modelVersion>4.0.0</modelVersion>
5 
6    <groupId>no.kantega</groupId>
7    <artifactId>spring-and-react</artifactId>
8    <version>0.0.1-SNAPSHOT</version>
9    <packaging>jar</packaging>
10 
11    <name>spring-and-react</name>
12    <description>Demo project for Spring Boot</description>
13 
14    <parent>
15        <groupId>org.springframework.boot</groupId>
16        <artifactId>spring-boot-starter-parent</artifactId>
17        <version>2.0.1.RELEASE</version>
18        <relativePath/> <!-- lookup parent from repository -->
19    </parent>
20 
21    <properties>
22        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
23        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
24        <java.version>1.8</java.version>
25    </properties>
26 
27    <dependencies>
28        <dependency>
29            <groupId>org.springframework.boot</groupId>
30            <artifactId>spring-boot-starter-web</artifactId>
31        </dependency>
32 
33        <dependency>
34            <groupId>org.springframework.boot</groupId>
35            <artifactId>spring-boot-starter-test</artifactId>
36            <scope>test</scope>
37        </dependency>
38    </dependencies>
39 
40    <build>
41        <plugins>
42            <plugin>
43                <groupId>org.springframework.boot</groupId>
44                <artifactId>spring-boot-maven-plugin</artifactId>
45            </plugin>
46        </plugins>
47    </build>
48</project>

Right now, there are no services in this app, so if you were to run it and navigate to http://localhost:8080, you would only get a 404 page. So let's add a controller called no.kantega.spring and react.HelloController:

1package no.kantega.springandreact;
2 
3import org.springframework.web.bind.annotation.GetMapping;
4import org.springframework.web.bind.annotation.RestController;
5 
6import java.util.Date;
7 
8@RestController
9public class HelloController {
10    @GetMapping("/api/hello")
11    public String hello() {
12        return "Hello, the time at the server is now " + new Date() + "\n";
13    }
14}

Now, let's run the application and try it out:

In one command line window, start the application with

1$ mvn spring-boot:run 
2 
3[...]
42018-04-11 09:35:47.566  INFO 88567 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
52018-04-11 09:35:47.573  INFO 88567 --- [           main] n.k.s.SpringAndReactApplication          : Started SpringAndReactApplication in 2.295 seconds (JVM running for 5.486)
62018-04-11 09:35:49.837  INFO 88567 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
72018-04-11 09:35:49.838  INFO 88567 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
82018-04-11 09:35:49.853  INFO 88567 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 15 ms
9

Then, in another window, fetch http://localhost:8080/api/hello with curl or your web browser:

1$ curl http://localhost:8080/api/hello

Hello, the time at the server is now Wed Apr 11 09:38:19 CEST 2018

That's great, we now have a rest service in Spring Boot up and running!

Adding React

This section uses the tool npx, which is included with newer versions of npm.

1$ npx create-react-app frontend
2 
3Creating a new React app in /Users/oven/git/react-and-spring/frontend.
4 
5Installing packages. This might take a couple of minutes.
6Installing react, react-dom, and react-scripts...
7[...]
8Success! Created frontend at /Users/oven/git/react-and-spring/frontend
9[...]
10We suggest that you begin by typing:
11 
12  cd frontend
13  npm start

OK, let's do that, then :)

1$ cd frontend
2$ npm start
3 
4[...] 
5 
6Compiled successfully!
7 
8You can now view frontend in the browser.
9 
10  Local:            http://localhost:3000/
11  On Your Network:  http://172.16.25.84:3000/
12 
13Note that the development build is not optimized.
14To create a production build, use npm run build.

This will open a web browser on your desktop, and it should display something like this:

Screenshot showing; welcome to react
Screenshot showing; welcome to react

Calling REST services in Spring from React

Now we have a backend server in Spring Boot running at http://localhost:8080 and a frontend frontend in React running at http://localhost:3000. We'd like to be able to call services in the backend and display the results in the frontend. In order to do this (and not get into trouble with any cross-origin requests (CORS)) we ask the frontend server to proxy any requests from :3000 to :8080.

According to the documentation you have to add a proxy entry to frontend/package.json. This will ensure that the web server at :3000 proxies any requests to http://localhost:3000/api/* to http://localhost:8080/api, which will enable us to call the backend without running into any CORS issues. Note that this is only useful during development. In a test or production environment, we will solve this in a different way. Read on :)

frontend/package.json

1{
2  "name": "frontend",
3  "version": "0.1.0",
4  "private": true,
5  "dependencies": {
6    "react": "^16.3.1",
7    "react-dom": "^16.3.1",
8    "react-scripts": "1.1.4"
9  },
10  "scripts": {
11    "start": "react-scripts start",
12    "build": "react-scripts build",
13    "test": "react-scripts test --env=jsdom",
14    "eject": "react-scripts eject"
15  },
16  "proxy": {
17    "/api": {
18      "target": "http://localhost:8080",
19      "ws": true
20    }
21  }
22}

Make sure you have the backend running, and restart the frontend. You should now be able to fetch the hello service through the frontend server at http://localhost:3000/api/hello

1$ curl http://localhost:3000/api/hello

Hello, the time at the server is now Wed Apr 11 10:04:47 CEST 2018

Next, let's add a rest call to the frontend:

Open frontend/src/App.js and replace its contents with this:

1import React, {Component} from 'react';
2import logo from './logo.svg';
3import './App.css';
4 
5class App extends Component {
6 
7    state = {};
8 
9    componentDidMount() {
10        setInterval(this.hello, 250);
11    }
12 
13    hello = () => {
14        fetch('/api/hello')
15            .then(response => response.text())
16            .then(message => {
17                this.setState({message: message});
18            });
19    };
20 
21    render() {
22        return (
23            <div className="App">
24                <header className="App-header">
25                    <img src={logo} className="App-logo" alt="logo"/>
26                    <h1 className="App-title">{this.state.message}</h1>
27                </header>
28                <p className="App-intro">
29                    To get started, edit <code>src/App.js</code> and save to reload.
30                </p>
31            </div>
32        );
33    }
34}
35 
36export default App;

The frontend should now display the current time at the server:

Screenshot showing the time at the server
Screenshot showing the time at the server

Success! We now have a React frontend that talks to our Spring Boot backend. But how do we deploy this to production?

Packaging the React app with Spring Boot

We'd like to be able to publish one jar file to production, and that jar file should contain both the backend and the frontend. Spring Boot applications can serve static content if you put it into the classes/public directory of the application jar file. Create React App can build a static bundle for production by running npm build in the frontend directory.

To accomplish this, we have to do the following:

  1. create a production build of the frontend
  2. copy the production build into ${target/classes/public}

We'll use frontend-maven-plugin in step 1, and maven-antrun-plugin in step 2. When we're done, we can just type $ mvn clean install and we'll end up with a single jar file containing both the frontend and the backend.

Run npm from maven

Add the following to pom.xml under /build/plugins:

1<plugin>
2    <groupId>com.github.eirslett</groupId>
3    <artifactId>frontend-maven-plugin</artifactId>
4    <version>1.6</version>
5    <configuration>
6        <workingDirectory>frontend</workingDirectory>
7        <installDirectory>target</installDirectory>
8    </configuration>
9    <executions>
10        <execution>
11            <id>install node and npm</id>
12            <goals>
13                <goal>install-node-and-npm</goal>
14            </goals>
15            <configuration>
16                <nodeVersion>v8.9.4</nodeVersion>
17                <npmVersion>5.6.0</npmVersion>
18            </configuration>
19        </execution>
20        <execution>
21            <id>npm install</id>
22            <goals>
23                <goal>npm</goal>
24            </goals>
25            <configuration>
26                <arguments>install</arguments>
27            </configuration>
28        </execution>
29        <execution>
30            <id>npm run build</id>
31            <goals>
32                <goal>npm</goal>
33            </goals>
34            <configuration>
35                <arguments>run build</arguments>
36            </configuration>
37        </execution>
38    </executions>
39</plugin>


When you run mvn clean install, maven will install npm and node locally and run npm build in the frontend directory.

1$ mvn clean install
2[...]
3 
4[INFO] Installed node locally.
5[INFO] Installing npm version 5.6.0
6[INFO] Unpacking /Users/oven/.m2/repository/com/github/eirslett/npm/5.6.0/npm-5.6.0.tar.gz into /Users/oven/git/react-and-spring/target/node/node_modules
7[INFO] Installed npm locally.
8[INFO]
9[INFO] --- frontend-maven-plugin:1.6:npm (npm install) @ spring-and-react ---
10[INFO] Running 'npm install' in /Users/oven/git/react-and-spring/frontend
11[WARNING] npm WARN ajv-keywords@3.1.0 requires a peer of ajv@^6.0.0 but none is installed. You must install peer dependencies yourself.
12[ERROR]
13[INFO] up to date in 7.23s
14[INFO]
15[INFO] --- frontend-maven-plugin:1.6:npm (npm run build) @ spring-and-react ---
16[INFO] Running 'npm run build' in /Users/oven/git/react-and-spring/frontend
17[INFO]
18[INFO] > frontend@0.1.0 build /Users/oven/git/react-and-spring/frontend
19[INFO] > react-scripts build
20[INFO]
21[INFO] Creating an optimized production build...
22[INFO] Compiled successfully.
23[...]

This results in a production build of the frontend in frontend/build:

1$ tree frontend/build
2frontend/build
3+-- asset-manifest.json
4+-- favicon.ico
5+-- index.html
6+-- manifest.json
7+-- service-worker.js
8+-- static
9    +-- css
10    ¦   +-- main.c17080f1.css
11    ¦   +-- main.c17080f1.css.map
12    +-- js
13    ¦   +-- main.9980f700.js
14    ¦   +-- main.9980f700.js.map
15    +-- media
16        +-- logo.5d5d9eef.svg
17 
184 directories, 10 files

Include frontend build files in spring boot jar

We now have to copy these files to target/classes/public in order to serve them as static resources from the Spring Boot application. We'll use the ant plugin for this.

Add the following to pom.xml under /build/plugins:

1<plugin>
2    <artifactId>maven-antrun-plugin</artifactId>
3    <executions>
4        <execution>
5            <phase>generate-resources</phase>
6            <configuration>
7                <target>
8                    <copy todir="${project.build.directory}/classes/public">
9                        <fileset dir="${project.basedir}/frontend/build"/>
10                    </copy>
11                </target>
12            </configuration>
13            <goals>
14                <goal>run</goal>
15            </goals>
16        </execution>
17    </executions>
18</plugin>

This will ensure that the frontend build files are copied after they have been generated by npm build.

Run maven again, and inspect the contents of the target/classes directory:

1$ mvn clean install
2[...]
3 
4[INFO] --- maven-antrun-plugin:1.8:run (default) @ spring-and-react ---
5[INFO] Executing tasks
6 
7main:
8     [copy] Copying 10 files to /Users/oven/git/react-and-spring/target/classes/public
9[INFO] Executed tasks
10 
11[...] 
12 
13$ tree target/classes
14  target/classes
15  +-- application.properties
16  +-- no
17  ¦   +-- kantega
18  ¦       +-- springandreact
19  ¦           +-- HelloController.class
20  ¦           +-- SpringAndReactApplication.class
21  +-- public
22      +-- asset-manifest.json
23      +-- favicon.ico
24      +-- index.html
25      +-- manifest.json
26      +-- service-worker.js
27      +-- static
28          +-- css
29          ¦   +-- main.c17080f1.css
30          ¦   +-- main.c17080f1.css.map
31          +-- js
32          ¦   +-- main.9980f700.js
33          ¦   +-- main.9980f700.js.map
34          +-- media
35              +-- logo.5d5d9eef.svg
36   
37  8 directories, 13 files


You should also check that the files are present in the resulting jar file:

1$ jar tvf target/spring-and-react-0.0.1-SNAPSHOT.jar | grep public
2     0 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/
3     0 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/static/
4     0 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/static/css/
5     0 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/static/js/
6     0 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/static/media/
7494612 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/static/js/main.9980f700.js.map
8  3235 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/service-worker.js
9123322 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/static/js/main.9980f700.js
10   650 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/static/css/main.c17080f1.css
11  2671 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/static/media/logo.5d5d9eef.svg
12  1288 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/static/css/main.c17080f1.css.map
13  3870 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/favicon.ico
14   257 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/asset-manifest.json
15   548 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/index.html
16   317 Wed Apr 11 11:50:14 CEST 2018 BOOT-INF/classes/public/manifest.json


Now, we're ready to start the application. Make sure you quit any running servers, and run the jar file

1$ java -jar target/spring-and-react-0.0.1-SNAPSHOT.jar
2 
3 
4  .   ____          _            __ _ _
5 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
6( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
7 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
8  '  |____| .__|_| |_|_| |_\__, | / / / /
9 =========|_|==============|___/=/_/_/_/
10 :: Spring Boot ::        (v2.0.1.RELEASE)
11 
122018-04-11 11:53:44.983  INFO 93434 --- [           main] n.k.s.SpringAndReactApplication          : Starting SpringAndReactApplication v0.0.1-SNAPSHOT on oven.local with PID 93434 (/Users/oven/git/react-and-spring/target/spring-and-react-0.0.1-SNAPSHOT.jar started by oven in /Users/oven/git/react-and-spring)
132018-04-11 11:53:44.986  INFO 93434 --- [           main] n.k.s.SpringAndReactApplication          : No active profile set, falling back to default profiles: default
142018-04-11 11:53:45.045  INFO 93434 --- [           main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@5af3afd9: startup date [Wed Apr 11 11:53:45 CEST 2018]; root of context hierarchy
152018-04-11 11:53:46.180  INFO 93434 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
162018-04-11 11:53:46.221  INFO 93434 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
172018-04-11 11:53:46.222  INFO 93434 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.29
18[...]
192018-04-11 11:53:47.039  INFO 93434 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
20[...]


Open your web browser, and navigate to http://localhost:8080. You should now see the following:

Screenshot showing the time at the server
Screenshot showing the time at the server



Congratulations!

You now have a spring boot application with a React frontend. During development, you can now run the application using React-scripts by running cd frontend; npm start, and you'll have all the benefits of rapid application development with hot reloads and everything you might wish for, while being able to deploy the application to test and production environments as a single artifact.

Happy hacking!