Deploy React Applications in a Servlet Environment

September 19, 2017

Modern web applications using React and other frameworks are often distributed as static websites. It is undoubtely the simplest, cache-friendly and dead-cheap solution. However, some enterprisey projects (think about data-entry and legacy-business applications) need to be deployed in a standard, more constrained environment like a Java JEE Servlet engine, if not an actual application server.

Forget having your app deployed on a separate web server (like Apache or Nginx), let alone a dedicated 3rd-level domain or IP address like frontend.mylegacyapp.com! These new requirements can easily interfere with the standard toolchain of the so-called React boilerplates, slowing down development and discouraging developers.

In this article we will see a solution that enables frontend developers to:

  • bootstrap applications using create-react-app
  • pack and deploy them in a Servlet Engine like Tomcat or Jetty, even under a relative context path
  • enable the use of HTML5 History Push API with React Router
  • keep the modification to code base substantially intact

All this while preserving development speed!

Maven Build Tool

We are going to use Maven, a very popular build tool used in Java environments. It can build, pack and even deploy projects. Its XML-heavy syntax is quite verbose (and sometimes awkward) but it has everything we need for our purposes. Besides, it is often the de-facto build tool for Java EE projects, especially legacy ones.

So, if you didn’t already, go on an install it!

create-react-app

create-react-app is a so-called react boilerplate, i.e. an opinionated stack that represents “attempt to figure out a good way to start developing React apps” (source: project docs). Many types of applications can be created using this stack, which is a very popular way to bootstrap frontend projects.

Creating a new application is very easy:

  • install NPM or Yarn
  • install create-react-app:
    npm install -g create-react-app
  • create your application:
    create-react-app my-app cd my-app/
  • install React Router and add it as a dependency in package.json:
    npm install --save react-router-dom
  • run the application using an embedded Node server:
    npm start

Th application is ready and available at:
http://localhost:3000/

WAR Files

Java Web Applications need to be packed in a WAR file, a standard format that includes everything the application need: configuration files, compiled Java classes, JSP or JSF pages, static assets, and so on. They are basically ordinary zip-compressed archives with some special files and directories. See Oracle Java EE official documentation if you’re curious about the details.

As the documentation of create-react-app points out in the “Serving Apps with Client-Side Routing” paragraph, to use HTML5 History Push API we need to configure the server so that it redirects 404 - not found errors to /index.html.

Java web applications can be configured by placing a web.xml in the /WEB-INF folder. For simplicity, we will just create a web.xml file in the root (/) of our project:

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
	      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	      xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
	      http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
	      version="2.4">

  <display-name>create-react-app-servlet</display-name>

  <error-page>
    <error-code>404</error-code>
    <location>/index.html</location>
  </error-page>

</web-app>

Relative vs. Absolute Paths

The most annoying part of deploying Java web applications to application servers is that, as they must co-exist with other web apps, they will almost always be served under a relative path, e.g.
http://myserver/my-app

This is totally different from the usual http://localhost:3000/ address used while developing! To overcome this limitation, and to preserve our quick-and-easy development environment (and mental sanity), we need to enable our application to handle both absolute and relative paths.

To achieve this, we are going to use the basename property of BrowserRouter as suggested in “Building for Relative Paths” paragraph of create-react-app.

basename: string
The base URL for all locations. If your app is served from a sub-directory on your server, you’ll want to set this to the sub-directory. A properly formatted basename should have a leading slash, but no trailing slash.

The trick here is to create a Custom Environment Variable REACT_APP_ROUTER_BASE that makes the application aware of the context path under which it is deployed.

If REACT_APP_ROUTER_BASE is defined, such as REACT_APP_ROUTER_BASE=/my-app, then React Router will assume all links start from /my-app. Simple as that!

Following is an example that uses this technique. Just copy and paste it into your App.js:

import React, { Component } from 'react';
import {
    BrowserRouter,
    Link,
    Route,
    Switch
} from 'react-router-dom';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <BrowserRouter basename={process.env.REACT_APP_ROUTER_BASE || ''}>
          <div>
            <ul className="nav">
              <li><Link to="/">Homepage</Link></li>
              <li><Link to="/blog">Blog Posts</Link></li>
              <li><Link to="/about">About Us</Link></li>
            </ul>
            <Switch>
              <Route path="/blog" component={BlogScreen}/>
              <Route path="/about" component={AboutScreen}/>
              <Route path="/" component={HomeScreen}/>
            </Switch>
          </div>
        </BrowserRouter>
      </div>
    );
  }
}

function HomeScreen() {
  return (
    <div>
      <h1>Home</h1>
      <p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam,
      eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.</p>
    </div>
  );
}

function BlogScreen() {
  return (
    <div>
      <h1>Blog</h1>
      <h4>Some blog post</h4>
      <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
      <h4>Another blog post</h4>
      <p>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
      <p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
      <h4>Even more blog post</h4>
      <p>Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
    </div>
  );
}

function AboutScreen() {
  return (
    <div>
      <h1>About</h1>
      <p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam,
      eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.</p>
    </div>
  );
}

export default App;

We have three different screens, each triggered by its own path:

  • /: Homepage
  • /blog: Blog Posts
  • /about: About Us

It’s interesting to note that React lets you use actual Javascript expressions inside attributes values, like
basename={process.env.REACT_APP_ROUTER_BASE || ''}

This way we can provide a fallback in case REACT_APP_ROUTER_BASE is not defined, for example when we run the application with npm start.

Maven Build File

To activate Maven we must create a pom.xml file in the root of the project. The following POM is an adaptation (and simplification) from an example written by Philip Green II. See Running NPM Scripts through maven for a much more comprehensive example.

<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">
    <!--
    Adapted from:
    https://gist.github.com/phillipgreenii/7c954e3c3911e5c32bd0
    -->
    <modelVersion>4.0.0</modelVersion>
    <groupId>it.megadix</groupId>
    <artifactId>my-app</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <npm.output.directory>build</npm.output.directory>
    </properties>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- Standard plugin to generate WAR -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.1.1</version>
                <configuration>
                    <webResources>
                        <resource>
                            <directory>${npm.output.directory}</directory>
                        </resource>
                    </webResources>
                    <webXml>${basedir}/web.xml</webXml>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.3.2</version>
                <executions>
                    <!-- Required: The following will ensure `npm install` is called
                         before anything else during the 'Default Lifecycle' -->
                    <execution>
                        <id>npm install (initialize)</id>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <phase>initialize</phase>
                        <configuration>
                            <executable>npm</executable>
                            <arguments>
                                <argument>install</argument>
                            </arguments>
                        </configuration>
                    </execution>
                    <!-- Required: The following will ensure `npm install` is called
                         before anything else during the 'Clean Lifecycle' -->
                    <execution>
                        <id>npm install (clean)</id>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <phase>pre-clean</phase>
                        <configuration>
                            <executable>npm</executable>
                            <arguments>
                                <argument>install</argument>
                            </arguments>
                        </configuration>
                    </execution>

                    <!-- Required: This following calls `npm run build` where 'build' is
                         the script name I used in my project, change this if yours is
                             different -->
                    <execution>
                        <id>npm run build (compile)</id>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <phase>compile</phase>
                        <configuration>
                            <executable>npm</executable>
                            <arguments>
                                <argument>run</argument>
                                <argument>build</argument>
                            </arguments>
                        </configuration>
                    </execution>

                </executions>

                <configuration>
                    <environmentVariables>
                        <CI>true</CI>
                        <!-- The following parameters create an NPM sandbox for CI -->
                        <NPM_CONFIG_PREFIX>${basedir}/npm</NPM_CONFIG_PREFIX>
                        <NPM_CONFIG_CACHE>${NPM_CONFIG_PREFIX}/cache</NPM_CONFIG_CACHE>
                        <NPM_CONFIG_TMP>${project.build.directory}/npmtmp</NPM_CONFIG_TMP>
                    </environmentVariables>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <profiles>
        <profile>
            <id>local</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.codehaus.mojo</groupId>
                        <artifactId>exec-maven-plugin</artifactId>

                        <configuration>
                            <environmentVariables>
                                <PUBLIC_URL>http://localhost:7001/${project.artifactId}</PUBLIC_URL>
                                <REACT_APP_ROUTER_BASE>/${project.artifactId}</REACT_APP_ROUTER_BASE>
                            </environmentVariables>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>

        <profile>
            <id>prod</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.codehaus.mojo</groupId>
                        <artifactId>exec-maven-plugin</artifactId>

                        <configuration>
                            <environmentVariables>
                                <PUBLIC_URL>http://my-awesome-production-host/${project.artifactId}</PUBLIC_URL>
                                <REACT_APP_ROUTER_BASE>/${project.artifactId}</REACT_APP_ROUTER_BASE>
                            </environmentVariables>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>
</project>

Don’t be intimidated by the size of this build file! The only really important section is <profiles>.

Each profile defines two environment variables:

  • PUBLIC_URL: a pre-defined variable of create-react-app. It tells the template system which is the root URL of our application;
  • REACT_APP_ROUTER_BASE: our custom variable used by React Router.

Depending on your environment, you would need to change PUBLIC_URL changing the default port or host. For example Weblogic uses port 7001 instead of 8080.

Build!

To create a WAR file you need to trigger a Maven build:

mvn package

The first build will take a lot, because Maven has to download all dependencies. However, subsequent build will be much faster as Maven caches dependencies on local file system.

The result of the build will be a my-app.war in the target folder. Now just drop that file in your preferred servelt engine / application server, then acces
http://localhost:8080/my-app

Now all links will have a /my-app prefix, as configured in pom.xml.

You can find a complete example on GitHub create-react-app-servlet repository

Other Links:

comments powered by Disqus