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
- About 20 minutes
- A favorite text editor or IDE
- JDK 14 or later (This tutorial uses Java Record classes)
- Maven 3.2+
- Node.js 12+, with npm, yarn, vue-cli installed
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> {{person.age}}</p>
<p><strong>Where:</strong> {{person.location}}</p>
<p><strong>Who they are to me:</strong> {{person.relationship}}</p>
<p><strong>What they do:</strong> {{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