Bootiful Development with Spring Boot and Vue

avatar-matt_raible.jpg Matt Raible

Vue is a web framework that’s gotten a lot of attention lately because it’s lean and mean. Its baseline framework cost is around 40K and is known as a minimalistic web framework. With all of the recent attention on web performance and mobile-first, mobile-fast, it’s no surprise that Vue has become more and more popular. If you spent the time to learn AngularJS back in the day, chances are you’ll find an old friend in Vue.js.

Spring Boot is one of my favorite frameworks in the Java ecosystem. Yes, I’m biased. I’ve been a fan of the Spring Framework since way back in 2004. It was neat to be able to write Java webapps with Spring MVC, but most people used XML to configure things. Even though Spring supported JavaConfig, it wasn’t until Spring Boot (in 2014) that it really took off. Nowadays, you never see a Spring tutorial that shows you how to configure things with XML. Nice work, Spring Boot team!

I’m writing this tutorial because I’m a big fan of Vue. If you know me, you’ll know I’m a web framework aficionado. That is, I’m a big fan of web frameworks. Much like an NBA fan has a few favorite players, I have a few favorite frameworks. Vue has recently become one of those, and I’d like to show you why.

In this post, I’ll show you how to build a Spring Boot API using Spring Data JPA and Hibernate. Then I’ll show you how to create a Vue PWA and customize it to display the data from your API. Then you’ll add in some animated gifs, a sprinkle of authentication, and have a jolly good time doing it!

Build a REST API with Spring Boot

To get started with Spring Boot, navigate to start.spring.io and choose version 2.1.1+. In the "Search for dependencies" field, select the following:

  • H2: An in-memory database

  • Lombok: Because no one likes generating (or even worse, writing!) getters and setters

  • JPA: Standard ORM for Java

  • Rest Repositories: Allows you to expose your JPA repositories as REST endpoints

  • Web: Spring MVC with Jackson (for JSON), Hibernate Validator, and embedded Tomcat

Spring Initializr

If you like the command-line better, install HTTPie and run the following command to download a demo.zip.

http https://start.spring.io/starter.zip dependencies==h2,lombok,data-jpa,data-rest,web \
  packageName==com.okta.developer.demo -d

Create a directory called spring-boot-vue-example. Expand the contents of demo.zip into its server directory.

mkdir spring-boot-vue-example
unzip demo.zip -d spring-boot-vue-example/server

Open the "server" project in your favorite IDE and run DemoApplication or start it from the command line using ./mvnw spring-boot:run.

Create a com.okta.developer.demo.beer package and a Beer.java file in it. This class will be the entity that holds your data.

package com.okta.developer.demo.beer;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Data
@NoArgsConstructor
@Entity
class Beer {

    public Beer(String name) {
        this.name = name;
    }

    @Id
    @GeneratedValue
    private Long id;

    @NonNull
    private String name;
}

Add a BeerRepository class that leverages Spring Data to do CRUD on this entity.

package com.okta.developer.demo.beer;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

@RepositoryRestResource
interface BeerRepository extends JpaRepository<Beer, Long> {
}
Adding the @RepositoryRestResource annotation to BeerRepository exposes all its CRUD operations as REST endpoints.

Add a BeerCommandLineRunner that uses this repository and creates a default set of data.

package com.okta.developer.demo.beer;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.stream.Stream;

@Component
public class BeerCommandLineRunner implements CommandLineRunner {

    private final BeerRepository repository;

    public BeerCommandLineRunner(BeerRepository repository) {
        this.repository = repository;
    }

    @Override
    public void run(String... strings) throws Exception {
        // Top beers from https://www.beeradvocate.com/lists/us, November 2018
        Stream.of("Kentucky Brunch Brand Stout", "Marshmallow Handjee", "Barrel-Aged Abraxas",
            "Hunahpu's Imperial Stout", "King Julius", "Heady Topper",
            "Budweiser", "Coors Light", "PBR").forEach(name ->
            repository.save(new Beer(name))
        );
        repository.findAll().forEach(System.out::println);
    }
}

Restart your app, and you should see a list of beers printed in your terminal.

Beers printed in terminal

Add a BeerController class to create an endpoint that filters out less-than-great beers.

package com.okta.developer.demo.beer;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;
import java.util.stream.Collectors;

@RestController
public class BeerController {
    private BeerRepository repository;

    public BeerController(BeerRepository repository) {
        this.repository = repository;
    }

    @GetMapping("/good-beers")
    public Collection<Beer> goodBeers() {
        return repository.findAll().stream()
                .filter(this::isGreat)
                .collect(Collectors.toList());
    }

    private boolean isGreat(Beer beer) {
        return !beer.getName().equals("Budweiser") &&
                !beer.getName().equals("Coors Light") &&
                !beer.getName().equals("PBR");
    }
}

Re-build your application and navigate to http://localhost:8080/good-beers. You should see the list of good beers in your browser.

Good Beers API

You should also see the same result in your terminal window when using HTTPie.

http :8080/good-beers

Create a Project with Vue CLI

Creating an API seems to be the easy part these days, thanks in large part to Spring Boot. In this section, I hope to show you that creating a UI with Vue is pretty simple too. I’ll also show you how to develop the Vue app with TypeScript. If you follow the steps below, you’ll create a new Vue app, fetch beer names and images from APIs, and create components to display their data.

To create a Vue project, make sure you have Node.js, and Vue CLI 3 installed. I used Node 11.3.0 when I created this tutorial.

npm install -g @vue/cli@3.2.1

From a terminal window, cd into the root of the spring-boot-vue-example directory and run the following command. This command will create a new Vue application and prompt you for options.

vue create client

When prompted to pick a present, choose Manually select features.

Vue CLI Features

Check the TypeScript, PWA, and Router features. Choose the defaults (by pressing Enter) for the rest of the questions.

In a terminal window, cd into the client directory and open package.json in your favorite editor. Add a start script that’s the same as the serve script.

"scripts": {
  "start": "vue-cli-service serve",
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "lint": "vue-cli-service lint"
},

Now you can start your Vue app using npm start. Your Spring Boot app should be still running on port 8080, which will cause your Vue app to use port 8081. I expect you to run your Vue app on 8081 throughout this tutorial. To ensure it always runs on this port, create a client/vue.config.js file and add the following JavaScript to it.

module.exports = {
  devServer: {
    port: 8081
  }
};

Open http://localhost:8081 in your browser, and you should see a page like the one below.

Vue Welcome

Create a Good Beers UI in Vue

So far, you’ve created a good beers API and a Vue client, but you haven’t created the UI to display the list of beers from your API. To do this, open client/src/views/Home.vue and add a created() method.

import axios from 'axios';
...

private async created() {
  const response = await axios.get('/good-beers');
  this.beers = await response.data;
}

Vue’s component lifecycle will call the created() method.

John Papa’s Vue.js with TypeScript was a big help in figuring out how to use TypeScript with Vue. Vue’s TypeScript docs were also helpful.

You’ll need to install axios for this code to compile.

npm i axios

You can see this puts the response data into a local beers variable. To properly define this variable, create a Beer interface and initialize the Home class’s beers variable to be an empty array.

export interface Beer {
  id: number;
  name: string;
  giphyUrl: string;
}

@Component({
  components: {
    HelloWorld,
  },
})
export default class Home extends Vue {
  public beers: Beer[] = [];

  private async created() {
    const response = await axios.get('/good-beers');
    this.beers = await response.data;
  }
}

A keen eye will notice this makes a request to /good-beers on the same port as the Vue application (since it’s a relative URL). For this to work, you’ll need to modify client/vue.config.js to have a proxy that sends this URL to your Spring Boot app.

module.exports = {
  devServer: {
    port: 8081,
    proxy: {
      "/good-beers": {
        target: "http://localhost:8080",
        secure: false
      }
    }
  }
};

Modify the template in client/src/views/Home.vue to display the list of good beers from your API.

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <h1>Beer List</h1>
    <div v-for="beer in beers">
      {{ beer.name }}
    </div>
  </div>
</template>

Restart your Vue app using npm start and refresh your app on http://localhost:8081. You should see a list of beers from your Spring Boot API.

Beer List in Vue

Create a BeerList Component

To make this application easier to maintain, move the beer list logic and rendering to its own BeerList component. Create client/src/components/BeerList.vue and populate it with the code from Home.vue. Remove the Vue logo, customize the template’s main class name, and remove the HelloWorld component. It should look as follows when you’re done.

<template>
  <div class="beer-list">
    <h1>Beer List</h1>
    <div v-for="beer in beers">
      {{ beer.name }}
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import axios from 'axios';

export interface Beer {
  id: number;
  name: string;
  giphyUrl: string;
}

@Component
export default class BeerList extends Vue {
  public beers: Beer[] = [];

  private async created() {
    const response = await axios.get('/good-beers');
    this.beers = await response.data;
  }
}
</script>

Then change client/src/views/Home.vue so it only contains the logo and a reference to <BeerList/>.

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <BeerList/>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import BeerList from '@/components/BeerList.vue';

@Component({
  components: {
    BeerList,
  },
})
export default class Home extends Vue {}
</script>

Create a GiphyImage Component

To make things look a little better, add a GIPHY component to fetch images based on the beer’s name. Create client/src/components/GiphyImage.vue and place the following code inside it.

<template>
  <img :src=giphyUrl v-bind:alt=name height="200"/>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import axios from 'axios';

@Component
export default class GiphyImage extends Vue {
  @Prop() private name!: string;
  private giphyUrl: string = '';

  private async created() {
    const giphyApi = '//api.giphy.com/v1/gifs/search?api_key=dc6zaTOxFJmzC&limit=1&q=';

    const response = await axios.get(giphyApi + this.name);
    const data = await response.data.data;
    if (data.length) {
      this.giphyUrl = data[0].images.original.url;
    } else {
      this.giphyUrl = '//media.giphy.com/media/YaOxRsmrv9IeA/giphy.gif';
    }
  }
}
</script>

<!-- The "scoped" attribute limits CSS to this component only -->
<style scoped>
img {
  margin: 10px 0 0;
}
</style>

Change BeerList.vue to use the <GiphyImage/> component in its template:

<div v-for="beer in beers">
  {{ beer.name }}<br/>
  <GiphyImage :name="beer.name"/>
</div>

And add it to the components list in the <script> block:

import GiphyImage from '@/components/GiphyImage.vue';

@Component({
  components: {GiphyImage},
})
export default class BeerList extends Vue { ... }

In this same file, add a <style> section at the bottom and use CSS Grid layout to organize the beers in rows.

<style scoped>
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-gap: 10px;
  grid-auto-rows: minmax(100px, auto);
}
</style>

You’ll need to wrap a div around the beer list template for this to have any effect.

<div class="grid">
  <div v-for="beer in beers">
    {{ beer.name }}<br/>
    <GiphyImage :name="beer.name"/>
  </div>
</div>

After making these changes, your UI should look something like the following list of beer names and matching images.

Beer List with Giphy images

You just created a Vue app that talks to a Spring Boot API. Congratulations! 🎉

Add PWA Support

Vue CLI has support for progressive web applications (PWAs) out-of-the-box. When you created your Vue app, you selected PWA as a feature.

PWA features are only enabled in production, because having assets cached in development can be a real pain. Run npm run build in the client directory to create a build ready for production. Then use serve to create a web server and show your app.

npm i -g serve
serve -s dist -p 8081

You should be able to open your browser and see your app at http://localhost:8081. When I first tried this, I found that loading the page didn’t render any beer names and all the images were the same. This is because the client attempts to make a request to /good-beers and there’s no proxy configured in production mode.

To fix this issue, you’ll need to change the URL in the client and configure Spring Boot to allow cross-domain access from http://localhost:8081.

Modify client/src/components/BeerList.vue to use the full URL to your Spring Boot API.

private async created() {
  const response = await axios.get('http://localhost:8080/good-beers');
  this.beers = await response.data;
}
If you don’t see any changes in your UI after making these changes, it’s because your browser has cached your app. Use an incognito window, or clear your cache (in Chrome: Developer Tools > Application > Clear storage > Clear site data) to fix this issue.

Configure CORS for Spring Boot

In the server project, open src/main/java/…​/demo/beer/BeerController.java and add a @CrossOrigin annotation to enable cross-origin resource sharing (CORS) from the client (http://localhost:8081).

import org.springframework.web.bind.annotation.CrossOrigin;
...
    @GetMapping("/good-beers")
    @CrossOrigin(origins = "http://localhost:8081")
    public Collection<Beer> goodBeers() {

After making these changes, rebuild your Vue app for production, refresh your browser, and everything should render as expected.

Use Lighthouse to See Your PWA Score

I ran a Lighthouse audit in Chrome and found that this app scores a 81/100 at this point. The most prominent complaint from this report was that I wasn’t using HTTPS. To see how the app would score when it used HTTPS, I deployed it to Pivotal Cloud Foundry and Heroku. I was pumped to discover it scored high on both platforms.

Lighthouse score on Heroku
Lighthouse score on Cloud Foundry

The reason it scores a 96 is because The viewport size is 939px, whereas the window size is 412px. I’m not sure what’s causing this issue, maybe it’s the CSS Grid layout?

To see the scripts I used to deploy everything, see heroku.sh and cloudfoundry.sh in this post’s companion GitHub repository.

You will need to initialize Git before running the deployment scripts. Run rm -rf client/.git, followed by git commit -a "Add project".

Add Authentication with Okta

You might be thinking, "this is pretty cool, it’s easy to see why people dig Vue." There’s another tool you might dig after you’ve tried it: Authentication with Okta! Why Okta? Because you can get 1,000 active monthly users for free! It’s worth a try, especially when you see how easy it is to add auth to Spring Boot and Vue with Okta.

Okta Spring Boot Starter

To secure your API, you can use Okta’s Spring Boot Starter. To integrate this starter, add the following dependencies to server/pom.xml:

<dependency>
    <groupId>com.okta.spring</groupId>
    <artifactId>okta-spring-boot-starter</artifactId>
    <version>0.6.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.1.RELEASE</version>
</dependency>

Now you need to configure the server to use Okta for authentication. You’ll need to create an OIDC app in Okta for that.

Create an OIDC App in Okta

Log in to your Okta Developer account (or sign up if you don’t have an account) and navigate to Applications > Add Application. Click Single-Page App, click Next, and give the app a name you’ll remember. Change all instances of localhost:8080 to localhost:8081 and click Done.

Copy the client ID into your server/src/main/resources/application.properties file. While you’re in there, add a okta.oauth2.issuer property that matches your Okta domain. For example:

okta.oauth2.issuer=https://{yourOktaDomain}/oauth2/default
okta.oauth2.client-id={yourClientId}
Replace {yourOktaDomain} with your org URL, which you can find on the Dashboard of the Developer Console. Make sure you don’t include -admin in the value!

Update server/src/main/java/…​/demo/DemoApplication.java to enable it as a resource server.

import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

@EnableResourceServer
@SpringBootApplication

After making these changes, you should be able to restart the server and see access denied when you try to navigate to http://localhost:8080.

Access Denied Error

Okta’s Vue Support

Okta’s Vue SDK allows you to integrate OIDC into a Vue application. You can learn more about Okta’s Vue SDK can be found on npmjs.com. To install, run the following commands in the client directory:

npm i @okta/okta-vue@1.0.7
npm i -D @types/okta__okta-vue
The types for Okta’s Vue SDK may be included in a future release. I created a pull request to add them.

Open client/src/router.ts and add your Okta configuration. The router.ts below also includes a path for the BeerList, a callback that’s required for authentication, and a navigation guard to require authentication for the /beer-list path. Replace yours with this one, then update yourClientDomain and yourClientId to match your settings. Make sure to remove the {} since those are just placeholders.

import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import OktaVuePlugin from '@okta/okta-vue';
import BeerList from '@/components/BeerList.vue';

Vue.use(Router);
Vue.use(OktaVuePlugin, {
  issuer: 'https://{yourOktaDomain}/oauth2/default',
  client_id: '{yourClientId}',
  redirect_uri: window.location.origin + '/implicit/callback',
  scope: 'openid profile email',
});

const router = new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import(/* webpackChunkName: "about" */ './views/About.vue'),
    },
    {
      path: '/beer-list',
      name: 'beer-list',
      component: BeerList,
      meta: {
        requiresAuth: true,
      },
    },
    { path: '/implicit/callback', component: OktaVuePlugin.handleCallback() },
  ],
});

router.beforeEach(Vue.prototype.$auth.authRedirectGuard());

export default router;

Since you have a route for BeerList remove it from client/src/views/Home.vue.

<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class Home extends Vue {}
</script>

Add a link to the BeerList in client/src/App.vue. You’ll also need to add code that detects if the user is logged in or not. Replace the <template> section and add the <script> below to your App.vue.

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
      <template v-if="authenticated"> |
        <router-link to="/beer-list">Good Beers</router-link>
      </template>
    </div>
    <button v-if="authenticated" v-on:click="logout">Logout</button>
    <button v-else v-on:click="$auth.loginRedirect()">Login</button>
    <router-view/>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';

@Component
export default class App extends Vue {
  public authenticated: boolean = false;

  private created() {
    this.isAuthenticated();
  }

  @Watch('$route')
  private async isAuthenticated() {
    this.authenticated = await this.$auth.isAuthenticated();
  }

  private async logout() {
    await this.$auth.logout();
    await this.isAuthenticated();

    // Navigate back to home
    this.$router.push({path: '/'});
  }
}
</script>

Restart your Vue app and you should see a button to log in.

Login Button

Click on it and you’ll be redirected to Okta. Enter the credentials you used to sign up for Okta and you’ll be redirected back to the app. You should see a Logout button and a link to see some good beers.

Vue app after authenticating

If you click to on the Good Beers link, you’ll see the component’s header, but no data. If you look at your JavaScript console, you’ll see there’s a CORS error.

This error happens because Spring’s @CrossOrigin doesn’t play well with Spring Security. To solve this problem, add a simpleCorsFilter bean to the body of DemoApplication.java.

package com.okta.developer.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.Collections;

@EnableResourceServer
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Bean
    public FilterRegistrationBean<CorsFilter> simpleCorsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.setAllowedOrigins(Collections.singletonList("http://localhost:8081"));
        config.setAllowedMethods(Collections.singletonList("*"));
        config.setAllowedHeaders(Collections.singletonList("*"));
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

Restart your server after making this change. To make it all work on the client, modify the created() method in client/src/components/BeerList.vue to set an authorization header.

private async created() {
  const response = await axios.get('http://localhost:8080/good-beers',
    {
      headers: {
        Authorization: `Bearer ${await this.$auth.getAccessToken()}`,
      },
    },
  );
  this.beers = await response.data;
}

Now you should be able to see the good beer list as an authenticated user.

Success at last!

If it works, excellent! 👍

Learn More About Spring Boot and Vue

This tutorial showed you how to build an app that uses modern frameworks like Spring Boot and Vue. You learned how to add authentication with OIDC and protect routes using Okta’s Vue SDK. If you’d like to watch a video of this tutorial, I published it as a screencast to YouTube.

If you want to learn more about the Vue phenomenon, I have a couple of recommended articles. First of all, I think it’s awesome it’s not sponsored by a company (like Angular + Google and React + Facebook), and that’s it’s mostly community driven. The Solo JavaScript Developer Challenging Google and Facebook is an article in Wired that explains why this is so amazing.

Regarding JavaScript framework performance, The Baseline Costs of JavaScript Frameworks is an interesting blog post from Anku Sethi. I like his motivation for writing it:

Last week I was curious about how much of a performance impact just including React on a page can have. So I ran some numbers on a cheap Android phone and wrote about it.

To learn more about Vue, Spring Boot, or Okta, check out the following resources:

You can find the source code associated with this article on GitHub. The primary example (without authentication) is in the master branch, while the Okta integration is in the okta branch. To check out the Okta branch on your local machine, run the following command.

git clone -b okta https://github.com/oktadeveloper/spring-boot-vue-example.git

If you find any issues, please add a comment below, and I’ll do my best to help. If you liked this tutorial, you should follow my team on Twitter. We also have a YouTube channel where we publish screencasts.

There are Angular and React versions of this same tutorial.