Building a Single-Page-App (SPA) with Micronaut

Learn how to build and package a SPA together with a Micronaut backend

Authors: Zachary Klein

Micronaut Version: 1.1.0

1 Getting Started

In this guide, you will learn how to create a Single Page App (using React) together with a Micronaut backend, and (optionally) how to package both projects together as an executable JAR. We will be using the Gradle build tool due to its powerful multi-project support and (via plugins) Node.js integration.

1.1 What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

1.2 Solution

We recommend you to follow the instructions in the next sections and create the app step by step. However, you can go right to the completed example.

or

Then, cd into the complete folder which you will find in the root project of the downloaded/cloned project.

2 Writing the Application

In the initial directory you should see a directory named server, containing a simple Micronaut application written in Java and using a Gradle build. The application includes a single controller, shown below:

server/src/main/java/example/micronaut/HelloController.java
package example.micronaut;

import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;

@Controller("/")
public class HelloController {

    @Produces(MediaType.TEXT_PLAIN)
    @Get("/{name}")
    HttpResponse<String> hello(String name) {
        return HttpResponse.ok("Hello, " + name + "!");
    }
}

You can run the Micronaut application with the Gradle wrapper:

~ cd server
~ ./gradlew run

Exercise the controller with a simple GET request:

~ curl http://localhost:8080/Bob
  Hello, Bob!

Additionally, there is a directory named client which contains a React single-page-app. The project was generated using create-react-app, and has no modifications. If you have yarn installed and a Node.js installation of 10.x or above, you can run the app as shown below:

~ cd client
~ yarn start

2.1 Creating a Single Page App

Within the client directory, edit the file src/App.js as shown below:

client/src/App.js
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import {SERVER_URL} from "./config";

class App extends Component {

  (1)
  state = {
    name: 'World',
    message: null
  };

  (2)
  setName = e => this.setState({name: e.target.value});

  (3)
  getMessage = e => {
    e.preventDefault();
    fetch(`${SERVER_URL}/${this.state.name}`)
        .then(r => r.text())
        .then(message => this.setState({message}))
        .catch(e => console.error(e))
  };

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          {/* <4> */}
          <form onSubmit={this.getMessage}>
            <label>Enter your name: </label>
            <input type="text" value={this.state.name} onChange={this.setName} />
            <input type="submit" value="Submit" />
          </form>

          {/* <5> */}
          <p>
            { this.state.message ?
                <strong>{this.state.message}</strong> :
                <span>Edit <code>src/App.js</code> and save to reload.</span>
            }
          </p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>
    );
  }
}

export default App;
1 Define some initial state
2 Method to update the name property
3 Method to make a request to the server to retrieve the "Hello" message
4 Form to accept the user’s name
5 Conditionally display message from the server

The SERVER_URL variable used in the above component needs to be defined in a configuration file. Create the file config.js within the client/src directory, and add the following content:

const prod = process.env.NODE_ENV === 'production';  (1)

console.log(`Loading ${process.env.NODE_ENV} config...`);

export const SERVER_URL = prod ? '' : 'http://localhost:8080';
1 Use the current Node environment to determine which URL to use for server requests

2.2 Configuring Gradle

At this point we will turn our two separate projects server and client into subprojects within a Gradle multi-project build.

At the same level as the server and client directories, create a new file named settings.gradle and add the following content:

settings.gradle
include "client"
include "server"

This file will tell Gradle about the two subprojects.

Next, within the client directory, create a build.gradle file and add the following content:

client/build.gradle
plugins {
    id "com.github.node-gradle.node" version "1.3.0"
}

node {
    version = '10.15.0' // https://nodejs.org/en/
    yarnVersion = '1.13.0' // https://yarnpkg.com/en/
    download = true
}

task start(type: YarnTask, dependsOn: 'yarn') {
    group = 'application'
    description = 'Run the client app'
    args = ['run', 'start']
}

task build(type: YarnTask, dependsOn: 'yarn') {
    group = 'build'
    description = 'Build the client bundle'
    args = ['run', 'build']
}

task test(type: YarnTask, dependsOn: 'yarn') {
    group = 'verification'
    description = 'Run the client tests'
    args = ['run', 'test']
}

task eject(type: YarnTask, dependsOn: 'yarn') {
    group = 'other'
    description = 'Eject from the create-react-app scripts'
    args = ['run', 'eject']
}

This Gradle file will install the Gradle Node plugin and configure Gradle tasks that correspond to the Yarn scripts specified in package.json.

We now need to add the Gradle wrapper script to our multi-project build. If you have Gradle 4.10 installed, you can generate the script using the wrapper command:

~ gradle wrapper
Make sure to run the wrapper command at the same directory level as the client and server directories

Another option is to copy the wrapper scripts from the server directory to the directory above.

~ cp server/gradlew .
~ cp server/gradlew.bat .
~ cp -r server/gradle/ .
Make sure to run the cp commands at the same directory level as the client and server directories

Finally, create a new build.gradle file (at the same level as the settings.gradle file created previously), and add the following content:

build.gradle
task copyClientResources(dependsOn: ':client:build') { (1)
    group = 'build'
    description = 'Copy client resources into server'
}
copyClientResources.doFirst { (2)
    copy {
        from project(':client').buildDir.absolutePath
        into "${project(':server').buildDir}/resources/main/public"
    }
}

task assembleServerAndClient(dependsOn: ['copyClientResources', ':server:shadowJar']) { (3)
    group = 'build'
    description = 'Build combined server & client JAR'
}

task test {

}

task check {

}
1 The copyClientResources task will copy the static files generated by yarn build into the resources directory of the server project.
2 copyClientResources must be executed before :server:shadowJar. This ensures client static files will be available when the Micronaut app is assembled.
3 The assembleServerAndClient task ties the copy step to the existing Gradle shadowJar task for the server project. This means that we will get our unified archive only when running this custom task (running server:shadowJar alone will generate a JAR file without the client resources).

2.3 Updating Configuration

In order for the React app to make requests to the Micronaut server, we need to enable CORS support in the Micronaut configuration. Add the following snippet to `application.yml:

server/src/main/resources/application.yml
---
micronaut:
    server:
        cors:
            enabled: true

2.4 Static Resources

In order to serve the React single-page-app from the running Micronaut server, we need to configure the server to handle static resources. When the React app is built using yarn build, a set of HTML and JavaScript files are created that contain all of the resources needed to run the single-page-app.

server/src/main/resources/application.yml
---
micronaut:
    router:
        static-resources:
            default:
                enabled: true   (1)
                mapping: "/**"  (2)
                paths: "classpath:public" (3)
1 Enable serving of static resources
2 Specify the URI pattern that resources should served from
3 Specify the location from which static resources should be served

3 Running the Application

The server application can be started from the top-level directory with the server:run task.

Running the Micronaut app
~ ./gradlew server:run

The client React app can be started either with yarn start or with the client:start task.

Running the React app
~ ./gradlew client:start

Browse to http://localhost:3000, and you should see the React app. Enter a name into the form and click "Submit" - you should see the message returned from the server.

Building an Executable JAR

To generate the unified executable JAR, file, run the assembleServerAndClient task.

~ ./gradlew assembleServerAndClient

To run the application, use the java -jar command:

~ java -jar server/build/libs/server-0.1-all.jar

...
INFO  io.micronaut.runtime.Micronaut - Startup completed in 1365ms. Server Running: http://localhost:8080

Browse to http://localhost:8080, and you should see the React app. Enter a name into the form and click "Submit" - you should see the message returned from the server.

reactmicronaut