Create a single page app with Spring Boot and Vue.js

tunaranch

Haikal Saadh

Posted on March 31, 2019

Create a single page app with Spring Boot and Vue.js

Objectives

Create a single page app with Spring Boot and Vue.js with:

  • One maven build for both frontend and backend.
  • Frontend bundled into the Boot app.
  • Use Vue router in router history mode, so that we don't have the # in the url bar.

Prerequisites

You'll need to install:

  • npm (on macOS, you can simply brew install npm)
  • vue-cli (npm install -g @vue/cli
  • JDK (This example uses java 11, but any will do, just change the java version when creating the spring project)
  • httpie (optional. You can use https://start.spring.io to bootstrap your Spring project).

Step by step

Create a Spring Boot Project

From a terminal

$ http https://start.spring.io/starter.tgz \
 artifactId==cafe \
 javaVersion==11 \
 language==kotlin \
 name==Cafe \
 dependencies==webflux,devtools,actuator \
 baseDir==cafe | tar -xzvf -

This will give you a basic spring boot project under cafe/.

Test the build to make sure it works:

$ ./mvnw test
[INFO] Scanning for projects...
[INFO] 
[INFO] --------------------------< com.example:cafe >--------------------------
[INFO] Building Cafe 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
...
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  13.718 s
[INFO] Finished at: 2019-03-30T12:19:24+10:00
[INFO] ------------------------------------------------------------------------

Create a Vue project

Use vue-cli to generate a Hello World Vue CLI project.

$ cd src/main
$ vue create frontend \
    --no-git -i '{
  "useConfigFiles": false,
  "plugins": {
    "@vue/cli-plugin-babel": {},
    "@vue/cli-plugin-typescript": {
      "classComponent": true,
      "useTsWithBabel": true
    },
    "@vue/cli-plugin-eslint": {
      "config": "standard",
      "lintOn": [
        "save"
      ]
    }
  },
  "router": true,
  "routerHistoryMode": true,
  "cssPreprocessor": "node-sass"
}'

Configure javascript build output directory

Configure webpack so that compiled static content is under target, in keeping with maven conventions. Spring Boot serves static resources from public at the classpath root, so we'll take that into consideration as well.

Edit src/main/frontend/vue.config.js:

module.exports = {
    outputDir: '../../../target/frontend/public'
}

Configure the maven build to compile the Vue project

We need to make sure the built static resources end up in the correct place so the maven build and spring know about it

Configure the npm build

Add this plugin to your pom.xml's plugins section:

<project>
  ...
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>com.github.eirslett</groupId>
        <artifactId>frontend-maven-plugin</artifactId>
        <version>1.7.5</version>
        <executions>
          <execution>
            <id>install node and npm</id>
            <goals>
              <goal>install-node-and-npm</goal>
            </goals>
            <configuration>
              <nodeVersion>v11.12.0</nodeVersion>
            </configuration>
          </execution>
          <execution>
            <id>npm install</id>
            <goals>
              <goal>npm</goal>
            </goals>
            <configuration>
              <arguments>install</arguments>
            </configuration>
          </execution>
          <execution>
            <id>npm build</id>
            <goals>
              <goal>npm</goal>
            </goals>
            <phase>generate-resources</phase>
            <configuration>
              <arguments>run build</arguments>
            </configuration>
          </execution>
        </executions>
        <configuration>
          <workingDirectory>${project.basedir}/src/main/frontend</workingDirectory>
          <installDirectory>${project.build.directory}/node</installDirectory>
        </configuration>
      </plugin>
      ...
    <plugins>
  </build>
</project>

Test it out by running ./mvnw process-resources. You should see the output of the npm build in target/frontend/.

Add compiled static resources to the maven build

Add the generated static component as resource to your build, by adding a resources section to your pom.xml.


<project>
  ...
  <build>
    ...
    <resources>
      <resource>
        <directory>${project.build.directory}/frontend</directory>
      </resource>
    </resources>
    ...
  </build>
</project>

Configure spring boot plugin to include static resources

Add this extra configuration element to the spring-boot-maven-plugin's config so it will be treated as part of the Spring Boot app.

<project>
  ...
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <folders>
            <folder>${project.build.directory}/frontend</folder>
          </folders>
        </configuration>
      </plugin>
      ...
    <plugins>
  </build>
</project>

Almost there! If you run ./mvnw spring-boot:run and point your browser to http://localhost:8080/index.html, you should see half of the vue hello world page. We need to do some more work on the backend to things lined up properly.

Rewrite URLs to make router history mode work

Create a filter that routes everything that's not a bunch of pre-set paths to the static index page.

We will let boot handle the following paths:

  • /actuator: Spring Boot's actuator has endpoints for health checks, metrics etc
  • /api: this app's backend API can go under this path
  • /js, /css, /img: static resources
package com.example.cafe.web

import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono

@Component
class VueRoutePathFilter : WebFilter {

    companion object {
        val BOOT_PATHS = listOf(
            "/actuator/",
            "/api/",
            "/js/",
            "/css/",
            "/img/"
        )

        const val SPA_PATH = "/index.html"
    }

    override fun filter(exchange: ServerWebExchange,
                        chain: WebFilterChain): Mono<Void> {
        if (isApiPath(exchange.request.uri.path)) {
            return chain.filter(exchange)
        }

        return chain
            .filter(exchange
                .mutate()
                .request(exchange.request
                    .mutate().path(SPA_PATH)
                    .build())
                .build())
    }

    private fun isApiPath(path: String): Boolean {
        return BOOT_PATHS.any { path.startsWith(it) }
    }
}

You should now be able to hit http://localhost:8080 to get the vue Hello World page.

The sample code for this project is on GitHub. Enjoy!

💖 💪 🙅 🚩
tunaranch
Haikal Saadh

Posted on March 31, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related