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 constrained environment like a Java JEE Servlet Engine (Tomcat, Jetty, Resin) or a full-fledged Application Server (Weblogic, JBoss / WildFly, Websphere).

Forget having your app deployed on a separate "lightweight" 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, 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, that 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 and install it!

create-react-app

React boilerplates are, in a nutshell, opinionated stacks that "attempt to figure out a good way to start developing React apps". create-react-app (maintained by Facebook) is one of the most popular, complete and easy to use.

A new application can be created in a few easy steps:

  • install NPM or (better) 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

The application will be ready and available at:
http://localhost:3000/

WAR Files

A Java Web Application must packed in a WAR (Web Application Archive) file to be deployed. WAR files include everything the application needs:

  • configuration files: WEB-INF/web.xml and others
  • compiled Java classes
  • JSP / JSF pages, pre-compiled or source
  • static assets

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.

For our purpose we will need just a web.xml file in the root directory of our project. Our Maven build will take care of placing it under the correct place in the final WAR archive.

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.

web.xml lets you define how to handle common HTTP error codes like 404, 500, etc. using the <error-page> element. We will use this feature to redirect any 404 - not found to /index.html:

    <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

More often than not Java Web Application must co-exist with other web apps in the same container. This usually means that they will be served under a relative context path, e.g. the /my-app part in:
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.

IMPORTANT NOTE: this example uses npm as build command for the react application. If you are using Yarn instead of NPM you should modify the relevant <executable> sections!

<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. Drop that file in your preferred servlet engine / application server, then acces
http://localhost:8080/my-app

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

Links and Sample Project

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

Other Links: