Building a Vue application with a Java backend and a custom vue-cli configuration


In this guide you will learn how to combine a VueJS front-end with a Java backend. We will achieve this using a custom vue-cli configuration and directory structure. You will also see how to use a maven plugin to help build the Vue app during the package phase of the maven build process, so that you do not have to run two commands to build the final application, which we will bundle as an uber jar.

What you will need

Introduction

We will create a basic Vue SPA (single page application) and connect it to a Java backend. In other setups this could involve having separate repositories, one for the front-end and another for the backend, but having everything in one place keeps things simple and makes finding and fixing things easier - especially for small projects or teams.

The code in this post is complete but is also available in the GitHub repository

The project layout

Java developers typically follow the maven convention for structuring files, we will try to adopt this conventional structure to our Vue code as well.

As a result, we will have the following layout at the end of the process:

.gitignore
.babelrc
package.json
yarn.lock
pom.xml
vue.config.js
src/
  main/
    java/         ( Java API code will be here )
    javascript/   ( Vue.JS components will be here )
      components/ ( Vue JS components )
      assets/     ( Assets including images and stylesheets )
    resources/
      assets/     ( Other assets required for our application )
      templates/  ( Html templates for our application )
      public/     ( The webpack build will place the JavaScript and other assets here)
test/             ( Test code )
  ...

Your Java code will go in src/main/java as usual, whereas the VueJS code will go into src/main/javascript. Other assets you need will be placed in src/main/resources/assets. We will configure our build so that Vue CLI places the built app code and assets in src/main/resources/public which will be picked up and served as static files by our Java Backend.

The Java API

First of all, let us create a simple Java webservice - we will use Spark Java to create our API.

Create the pom.xml file, the code listing below shows the complete pom file:

pom.xml

<?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.yourdomain.sparkvue</groupId>
    <artifactId>spark-vue</artifactId>
    <version>0.1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Vue with Java Backend API</name>
    <description>Backend API for VueJS with Java.</description>

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

    <dependencies>
        <dependency>
            <groupId>com.sparkjava</groupId>
            <artifactId>spark-core</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.6</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.1</version>
                <configuration>
                    <filters>
                        <filter>
                            <artifact>*:*</artifact>
                            <excludes>
                                <exclude>META-INF/*.SF</exclude>
                                <exclude>META-INF/*.DSA</exclude>
                                <exclude>META-INF/*.RSA</exclude>
                            </excludes>
                        </filter>
                    </filters>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.yourdomain.sparkvue.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <compilerArgs>
                        <compilerArg>--enable-preview</compilerArg>
                    </compilerArgs>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

NOTE: Notice we have <compilerArgs> in our pom file to enable Java Records feature

Create a file src/main/java/com/yourdomain/sparkvue/Main.java containing the following:

package com.yourdomain.sparkvue;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gson.Gson;
import spark.Spark;
import org.slf4j.LoggerFactory;

public class Main {

    // We use Java's new Record class feature to create an immutable class
    public static record Person(
        String firstname, String lastname, 
        int age, String occupation, 
        String location, String relationship) {}

    public static void main(String... args) {
        final Gson gson = new Gson();

        final List<Person> people = fetchPeople();

        Spark.port(4567);
        // root is 'src/main/resources', so put files in 'src/main/resources/public'
        Spark.staticFiles.location("/public");

        Spark.get("/people", (request, response) -> {
            response.type("application/json;charset=utf-8");
            return gson.toJson(people);
        });

        LoggerFactory.getLogger(Main.class).info("========== API RUNNING =================");
    }


    public static List<Person> fetchPeople() {
        List<Person> m = new ArrayList<>(); 

        m.add(new Person("Bob", "Banda", 13, "Future Accountant", "Blantyre", "Self"));
        m.add(new Person("John", "Banda", 68, "Accountant", "Blantyre", "Father"));
        m.add(new Person("Mary", "Banda", 8, "Accountant", "Blantyre", "Mother"));
        m.add(new Person("James", "Banda", 18, "Accountant", "Blantyre", "Brother"));
        m.add(new Person("Jane", "Banda", 8, "Student", "Blantyre", "Sister"));
        m.add(new Person("John", "Doe", 22, "Developer", "Lilongwe", "Cousin"));
        m.add(new Person("Brian", "Banda", 32, "Student", "Blantyre", "Best Friend"));
        m.add(new Person("Hannibal", "Kaya", 12, "Jerk", "Blantyre", "Arch Enemy"));

        return m;
    }
}

With that done, we have a working HTTP endpoint /people that we can fetch data on people from.

Logging with logback

Let’s just add some configuration for logback. Create a file at src/main/resources/logback.xml and place the following there:

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] %level - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

The VueJS application

So first of all, make sure you have Vue CLI installed. Vue CLI is a project from the Vue project that helps you to get started developing Vue apps quickly. It has out-of-the-box settings for hot-reloading and ofcourse bundling javascript with webpack, among many other features.

1. Create an application with vue-cli

Run the following in the command-line to create a Vue app

$ vue create frontend

Once this command executes successfully, you will have a new directory named frontend with a structure that looks like this:

$ ls frontend
babel.config.js  node_modules/  package.json  public/  README.md  src/  yarn.lock

2. Moving the vue-cli files

First Move all the files and directories in from the frontend directory to to the parent directory of the project, and then remove the frontend directory

$ mv frontend/*  .
$ rmdir frontend

Create a directory named src/main/javascript and move the following files:

$ mkdir -p src/main/javascript
$ mv src/assets src/main/javascript/
$ mv src/components src/main/javascript/
$ mv src/main.js src/main/javascript/
$ mv src/App.vue src/main/javascript/

Create a directory named src/main/resources/templates and move the index.html from public to there:

# Move the index template to templates
$ mkdir -p src/main/resources/templates
$ mv public/index.html src/main/resources/templates/index.html

# Move everything else to assets
$ mkdir -p src/main/resources/assets
$ mv public/* src/main/resources/assets/

3. Create the vue-cli config

Since we are using an unconventional setup for the frontend, we need to create a custom Vue CLI configuration. Create a file named vue.config.js with the following content, this will configure our vue-cli build with custom paths pointing to the files and directories moved in the previous step:

const path = require('path');
const outputDirectory = path.resolve(__dirname, 'src', 'main', 'resources', 'public');
const contentBaseDir = path.resolve(__dirname, 'src', 'main', 'resources', 'assets');
const entryFile = path.resolve(__dirname, 'src', 'main','javascript', 'main.js');
const indexHtmlTemplate = path.resolve(__dirname, 'src', 'main','resources', 'templates', 'index.html');

module.exports = {
  publicPath: '/',
  outputDir: outputDirectory,
  assetsDir: '',
  devServer: {
      contentBase: contentBaseDir,
      compress: true,
      port: 9000
  },
  pages: {
    index: {
      // entry for the page
      entry: entryFile,
      // the source template
      template: indexHtmlTemplate,
      // output as dist/index.html
      filename: 'index.html',
      // when using title option,
      // template title tag needs to be <title><%= htmlWebpackPlugin.options.title %></title>
      title: 'Vue with Java',
      // chunks to include on this page, by default includes
      // extracted common chunks and vendor chunks.
      chunks: ['chunk-vendors', 'chunk-common', 'index']
    }
  }
}

4. Vue components

We will use axios to make HTTP requests, we need to add it first:

$ yarn add axios

src/main/javascript/components/Person.vue

<template>
<div>
  <h3>{{person.firstname}} {{person.lastname}}</h3>
  <div class="person-bio">
    <p><strong>Age:</strong>&nbsp;{{person.age}}</p>
    <p><strong>Where:</strong>&nbsp;{{person.location}}</p>
    <p><strong>Who they are to me:</strong>&nbsp;{{person.relationship}}</p>
    <p><strong>What they do:</strong>&nbsp;{{person.occupation}}</p>
  </div>
  <hr>
</div>
</template>
<script>
export default {
    props: [ "person" ]
}
</script>

src/main/javascript/components/People.vue

<template>
<div>
  <h1>The people I know</h1>
  <div class="people-list">
    
    <person :person="p" v-for="p in people" :key="p.firstname + p.lastname"/>

  </div>
</div>
</template>
<script>
import axios from 'axios';
import Person from './Person.vue';

export default {
    components: {
        'person': Person
    },

    mounted() {
        axios.get("/people")
          .then(response => {
              this.people = response.data;
          })
          .catch(err => {
              alert("Failed to fetch data from /people", err);
          })
    },
    data() {
        return {
            people: []
        }
    }
}
</script>

5. Modify App.vue

Open up App.vue, which is created by vue-cli and edit it so that it looks like this:

<template>
  <div id="app">
    <img src="./assets/logo.png" />

    <h1>Using Vue with Java</h1>

    <People />
  </div>
</template>

<script>
import People from './components/People.vue'

export default {
  name: 'App',
  components: {
    People
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  margin: 0 auto;
  width: 80%;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Building the application

Congratulations! At this point you have done enough to build a Vue application with a Java backend that we can bundle as an executable .jar file. Let’s build the Vue side first.

$ yarn build

This will result in the public directory being created in the src/main/resources/ directory, which will be served by our Spark Java application.

Next we have to build the Java backend itself. We will build an uber jar or fat jar, as others call them. This is basically a file with all your application dependencies bundled into one .jar and with the jar’s main-class configured in the MANIFEST file.

Run the following command:

$ mvn clean package

If this executes successfully, it will result in jar files in the target directory named something like spark-vue-0.1.0-SNAPSHOT.jar and spark-vue-0.1.0-SNAPSHOT-original.jar.

Our uber jar is the one without the -original suffix. Next run this in your command line to start the server:

$ java --enable-preview -jar target/spark-vue-0.1.0-SNAPSHOT.jar 

NOTE: We use the --enable-preview because our code is compile with the Java Records feature

This should produce output similar to what’s shown below and indicates Spark is running our Java API code:

[2020-07-05 04:14:18] INFO - StaticResourceHandler configured with folder = /public
[2020-07-05 04:14:18] INFO - ========== API RUNNING =================
[2020-07-05 04:14:18] INFO - Logging initialized @1766ms to org.eclipse.jetty.util.log.Slf4jLog
[2020-07-05 04:14:18] INFO - == Spark has ignited ...
[2020-07-05 04:14:18] INFO - >> Listening on 0.0.0.0:4567
...

Congratulations, it’s running!! Open your browser and visit http://localhost:4567/.

Bonus 1: Adding maven exec plugin to run Vue build together with Java build

As you can see, we had to first build our Vue app with yarn build and then our Java code with mvn package, while it’s just two commands it is easy to forget to build the front-end before the backend. We can solve this using the exec-maven-plugin which allows us to run arbitrary commands during a maven build.

Add the following plugin configuration to your pom.xml file.

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.3.2</version>
    <executions>
        <execution>
            <phase>prepare-package</phase>
            <goals>
                <goal>exec</goal>
            </goals>
            <configuration>
                <executable>yarn</executable>
                <commandlineArgs>build</commandlineArgs>
                <environmentVariables>
                    <environment>production</environment>
                </environmentVariables>
            </configuration>
        </execution>
    </executions>
</plugin>

Now all you have to do is run mvn clean package to build both the Vue and Java code together.

Bonus 2: Working with Vue CLI Hot-reloading

Alright, so far you have seen how to build a Vue JS application and Java application - but so far we assumed the code for the Vue side was all ready for a final build. What if you are still developing the front-end and want to use vue-cli’s hot-reloading features?

This section will show you how to get that up and running for a better development experience.

Add some CORS to Java

Firstly, we need to deal with CORS (Cross-Origin Resource Sharing). Since the Vue app will run on another port (set to 9000 in our vue.config.js) using webpack’s dev-server when hot-reloading, we need to enable CORS for the Java backend so it can serve requests to JavaScript in the browser coming from a different port.

We will use a simple Spark Java Filter to add CORS support. The code in src/main/java/com/yourdomain/sparkvue/Main.java should be modified to look like this:

// ... imports

public class Main {

    // We use Java's new Record class feature to create an immutable class
    public static record Person(
        String firstname, String lastname, 
        int age, String occupation, 
        String location, String relationship) {}

    public static void main(String... args) {
        final Gson gson = new Gson();

        final List<Person> people = fetchPeople();

        Spark.port(4567);
        // root is 'src/main/resources', so put files in 'src/main/resources/public'
        Spark.staticFiles.location("/public");
        
        addCORS();

        redirect.get("/", "/public/index.html");

        Spark.get("/people", (request, response) -> {
            response.type("application/json;charset=utf-8");
            return gson.toJson(people);
        });

        LoggerFactory.getLogger(Main.class).info("========== API RUNNING =================");
    }


    public static List<Person> fetchPeople() {
        List<Person> m = new ArrayList<>(); 

        m.add(new Person("Bob", "Banda", 13, "Future Accountant", "Blantyre", "Self"));
        m.add(new Person("John", "Banda", 68, "Accountant", "Blantyre", "Father"));
        m.add(new Person("Mary", "Banda", 8, "Accountant", "Blantyre", "Mother"));
        m.add(new Person("James", "Banda", 18, "Accountant", "Blantyre", "Brother"));
        m.add(new Person("Jane", "Banda", 8, "Student", "Blantyre", "Sister"));
        m.add(new Person("John", "Doe", 22, "Developer", "Lilongwe", "Cousin"));
        m.add(new Person("Brian", "Banda", 32, "Student", "Blantyre", "Best Friend"));
        m.add(new Person("Hannibal", "Kaya", 12, "Jerk", "Blantyre", "Arch Enemy"));

        return m;
    }

    private static final HashMap<String, String> corsHeaders = new HashMap<String, String>();

    static {
        corsHeaders.put("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS");
        corsHeaders.put("Access-Control-Allow-Origin", "*");
        corsHeaders.put("Access-Control-Allow-Headers", "Content-Type,Authorization,X-Requested-With,Content-Length,Accept,Origin,");
        corsHeaders.put("Access-Control-Allow-Credentials", "true");
    }
    public final static void addCORS() {
        Spark.after((request, response) -> {
            corsHeaders.forEach((key, value) -> {
                response.header(key, value);
            });
        });
    }
}

With this done, you can now (re-)build the Java backend and run it

$ mvn clean package
$ java --enable-preview -jar target/spark-vue-0.1.0-SNAPSHOT.jar 

And then in another terminal run the following to run the Vue CLI with hot reloading so you can keep working on the front-end. Once it starts, you can visit http://localhost:9000/ in your browser.

$ yarn serve

Broken API calls :(

You may have noticed that when you navigate to the app in the web browser, the API calls fail. Oops.

The reason is that since the front-end is running with hot-reloading on a different port to your Java API (running on port 4567) you will have to modify your API requests to make calls to that port i.e. //localhost:4567/people instead of just /people during development and then revert to /people when ready build for production. Yuck! We can do better than that.

We can work around this by adding a custom option, apiBaseUri, to our configuration file. Add the following on a new line right after title: 'Vue with Java' in vue.config.js;

// This is a custom option to inject the base url in a script tag
// <script type="text/javascript">
//   window._apiBaseUri="<%= htmlWebpackPlugin.options.apiBaseUri %>";
// </script>
apiBaseUri: 'http://localhost:4567/',

Change the axios api call in People.vue to look like this:

// NOTE the window._apiBaseUri  
axios.get(window._apiBaseUri + "/people") 

And then, finally, update the content in src/main/resources/templates/index.html to the following (take note of the script tag in the head):

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>

    <script type="text/javascript">
      window._apiBaseUri = "<%= htmlWebpackPlugin.options.apiBaseUri %>";
    </script>

  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

You can now run yarn serve and will see the results loaded correctly once again. I consider this approach a bit of a hack, so welcome to hearing of better approaches.

Two Vue apps?

The nice side effect of this setup, with hot-reloading, is that you now have “two” frontends accessible, a previous build running behind the server at http://localhost:4567 and the current, hot-reloaded, one at http://localhost:9000. This could allow you to compare your frontend work side by side as you go.

Conclusion

In this guide you have seen how to combine VueJS and Java in one project to enable them to interact. There is a lot more you can do from here, including but not limited to adding database interaction on the backend to fetch data and create/update it from the Vue frontend.

Thank you for reading this far. I hope this tutorial helped you learn a thing or two.

Zikani. Finnd me on Twitter

See also