Build a CRUD App with Vue.js, Spring Boot, and Kotlin
Much like React or Angular, Vue.js is a JavaScript view library. When coupled with a state management library like MobX, Vue.js becomes a full-featured application framework. Vue.js is designed to be incrementally adoptable, so you can use as much or as little of it as you like. Like React, Vue.js utilizes a virtual DOM to streamline processing so that it renders as little as possible on each state update.
In my experience, Vue is far simpler to use and learn than React and Angular and is particularly great on small projects where you don’t need a full web application framework (or don’t want to write a bunch of sagas, actions, and action creators just to manage a few state variables). That said, Vue.js is also increasingly being used for larger projects.
Spring Boot is Spring’s streamlined Java application framework. It makes constructing server-side code with Java or Kotlin super easy. It’s an opinionated library where a lot of magic happens behind the scenes with relatively few lines of code. In the past with Spring, I found that this tended to obfuscate what was happening, making it hard to customize or understand, but with Spring Boot they seem to have found a real balance between customizability and ease of use. As you’ll see, you can use Spring Boot to create a fully functioning resource server from a plain Java object (or Kotlin, in our case) from shockingly few lines of code.
Today I’ll show you how to build a fully-functioning client-server CRUD application using Vue.js for the client and Spring Boot for the resource server. The Spring Boot server application will be written using Kotlin. You’ll also use Okta and OAuth 2.0 to secure the application.
CRUD—if you don’t know—stands for Create, Read, Update, and Delete. Once you can do these operations, you’ve got all the basics for a server application.
The example application you’re going to build here is a todo app. The client-side of the app is based on a project written by Evan You and the original Vue Todo App project. It’s all been modified a fair amount, but I wanted to give credit where credit is due.
Prerequisites:
Table of Contents
- Create a Vue + Spring Boot Application
- Build a Spring Boot Resource Server with Kotlin
- Test the Spring Boot Resource Server
- Test the Vue.js Client App
- Create an OIDC Application for Vue Authentication
- Add Authentication to Vue
- Test Your Vue App’s OAuth Flow
- Configure Spring Boot Server for JWT Auth
- Moving Forward with Okta, Vue, and Spring Boot
Create a Vue + Spring Boot Application
Rather than creating an app from scratch, go ahead and download the example application from this tutorial’s GitHub repository.
git clone https://github.com/oktadeveloper/okta-kotlin-spring-boot-vue-example.git
The example project contains two main sub-directories:
client
: contains the Vue.js clientserver
: contains the Spring Boot Kotlin resource server
First, you’re going to take a look at the resource server and make sure that it’s all working.
Build a Spring Boot Resource Server with Kotlin
The Kotlin resource server is pretty simple. Spring has done a great job reducing boilerplate code. There are four Kotlin source files in the project, all in the com.okta.springbootvue
package:
SpringBootVueApplication
: the main entry point into the applicationRestRepositoryConfigurator
: used to configure the auto-generated REST resource to return resource IDsTodo
: defines the Todo class data modelTodoRepository
: configures the auto-generated Spring Boot JPA repository
The first file, SpringBootVueApplication
, is the place where the application starts. It contains the @SpringBootApplication
annotation that loads the Spring Boot framework. It also contains a function named init()
that loads some initial test data into the repository. It contains a bean that configures a CORS filter so that you can make calls from the Vue.js application without getting cross-origin errors. Finally, it contains the good, old-fashioned main()
function where the magic begins.
com/okta/springbootvue/SpringBootVueApplication.kt
@SpringBootApplication
class SpringBootVueApplication {
// Bootstrap some test data into the in-memory database
@Bean
fun init(repository: TodoRepository): ApplicationRunner {
return ApplicationRunner { _: ApplicationArguments? ->
arrayOf("Buy milk", "Eat pizza", "Write tutorial", "Study Vue.js", "Go kayaking").forEach {
val todo = Todo(it, false)
repository.save(todo)
}
repository.findAll().forEach(Consumer { x: Todo? -> println(x) })
}
}
// Fix the CORS errors
@Bean
fun simpleCorsFilter(): FilterRegistrationBean<*> {
val source = UrlBasedCorsConfigurationSource()
val config = CorsConfiguration()
config.allowCredentials = true
// *** URL below needs to match the Vue client URL and port ***
config.allowedOrigins = listOf("http://localhost:8080")
config.allowedMethods = listOf("*")
config.allowedHeaders = listOf("*")
source.registerCorsConfiguration("/**", config)
val bean: FilterRegistrationBean<*> = FilterRegistrationBean(CorsFilter(source))
bean.order = Ordered.HIGHEST_PRECEDENCE
return bean
}
companion object {
@JvmStatic
fun main(args: Array<String>) {
SpringApplication.run(SpringBootVueApplication::class.java, *args)
}
}
}
The Todo
class is what defines the data model. Specifically, it defines three properties: 1) a string title
, 2) a boolean completed
, and 3) an auto-generated id
integer value. If you’re not familiar with Kotlin you might find this strange, but the title
and the completed
properties are declared on the first line of the class definition in the default constructor.
Other than that, the @Entity
annotation is what tells Spring that this class is a data model entity. There’s also a helper toString()
override.
@Entity
class Todo(var title: String, var completed: Boolean) {
@Id
@GeneratedValue
var id: Long? = null
override fun toString() : String {
return "Title='$title', Completed=$completed";
}
}
Spring is doing a lot of work behind the scenes. Pretty much all of the resource server’s infrastructure is auto-generated. You’re simply defining the data model and pointing Spring Boot to it. This is achingly clear in the next class, TodoRepository
, which is the class that defines and creates the REST interface for your resource server as well as your persistence store.
The persistence store in this case (the class that is responsible for saving and loading your data resources from a database) uses the default store, which is an in-memory database. It is great for examples and testing but needs to be overridden in production so that you can actually persist your data.
The coolest part is the @RepositoryRestResource
annotation. All you have to do is add it to the JpaRepository
class and Spring will turn your repository into a REST interface. This annotation requires the spring-boot-starter-data-rest
dependency. The details of the resource server can be configured extensively. Take a look at the Spring Data REST Reference documentation for more info.
Now for the code:
com/okta/springbootvue/TodoRepository.kt
@RepositoryRestResource
interface TodoRepository:JpaRepository<Todo, Long>
Yep, that’s it!
The last file is RestRepositoryConfigurator
. This file overrides RepositoryRestConfigurer
and is used to configure the REST repository. Specifically, it configures it to expose IDs for the Todo
class. This tells Spring that when it returns Todo objects, it should return the object ID with it as well. Doing this makes it a lot easier to implement the CRUD methods on the client-side.
@Component
class RestRepositoryConfigurator : RepositoryRestConfigurer {
override fun configureRepositoryRestConfiguration(config: RepositoryRestConfiguration) {
config.exposeIdsFor(Todo::class.java)
}
}
Test the Spring Boot Resource Server
That’s all you need to create a working REST API.
Now you’re going to test it using HTTPie. But first, open a shell, navigate to the /server
subdirectory, and start the server using ./gradlew bootRun
.
You should see some output that ends like this:
2020-06-24 19:31:34.033 INFO 25910 --- [ restartedMain] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
2020-06-24 19:31:34.039 INFO 25910 --- [ restartedMain] c.o.s.SpringBootVueApplication$Companion : Started SpringBootVueApplication.Companion in 2.287 seconds (JVM running for 2.532)
Title='Buy milk', Completed=false
Title='Eat pizza', Completed=false
Title='Write tutorial', Completed=false
Title='Study Vue.js', Completed=false
Title='Go kayaking', Completed=false
<==========---> 80% EXECUTING [13s]
> :bootRun
Now, open a separate terminal window and perform a basic GET request on the server endpoint using the following command: http :9000
, which is short for http GET http://localhost:9000
.
HTTP/1.1 200
Connection: keep-alive
...
{
"_links": {
"profile": {
"href": "http://localhost:9000/profile"
},
"todos": {
"href": "http://localhost:9000/todos{?page,size,sort}",
"templated": true
}
}
}
The profile
link refers to the ALPS (Application-Level Profile Semantics). Take a look at the Spring docs on it. It’s a way to describe the available resources exposed by the REST API.
The todos
link is the endpoint generated from the Todo class.
Perform a GET on the /todos
endpoint:
http :9000/todos
You’ll see an output like below (for brevity I’ve omitted all but the first of the todo objects).
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/hal+json
Date: Wed, 24 Jun 2020 02:35:59 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
{
"_embedded": {
"todos": [
{
"_links": {
"self": {
"href": "http://localhost:9000/todos/1"
},
"todo": {
"href": "http://localhost:9000/todos/1"
}
},
"completed": false,
"id": 1,
"title": "Buy milk"
},
...
]
},
"_links": {
"profile": {
"href": "http://localhost:9000/profile/todos"
},
"self": {
"href": "http://localhost:9000/todos"
}
},
"page": {
"number": 0,
"size": 20,
"totalElements": 5,
"totalPages": 1
}
}
You can try adding a todo using a POST.
http POST :9000/todos title="Drink more coffee"
HTTP/1.1 201
Connection: keep-alive
Content-Type: application/json
...
{
"_links": {
"self": {
"href": "http://localhost:9000/todos/6"
},
"todo": {
"href": "http://localhost:9000/todos/6"
}
},
"completed": false,
"id": 6,
"title": "Drink more coffee"
}
If you perform another GET on the /todos
endpoint, you’ll see that there are now six todo items and the last item is your newly added todo.
http :9000/todos
Again, a lot of the output below has been omitted for brevity, but notice the page.totalElements
value now equals six and that there is a new todo item.
HTTP/1.1 200
Connection: keep-alive
Content-Type: application/hal+json
...
{
"_embedded": {
"todos": [
...
{
"_links": {
"self": {
"href": "http://localhost:9000/todos/6"
},
"todo": {
"href": "http://localhost:9000/todos/6"
}
},
"completed": false,
"id": 6,
"title": "Drink more coffee"
}
]
},
...
"page": {
"number": 0,
"size": 20,
"totalElements": 6,
"totalPages": 1
}
}
Test the Vue.js Client App
I’m not going to go into a ton of detail on the Vue.js client app. A lot of this was covered in the previous tutorial and is largely the same. I will explicitly show you how to modify the unsecured client app (and server) to use Okta OAuth.
The Vue module that is the heart of the app is src/components/Todos.vue
. This is what you see and what controls the application flow.
Another important file is src/Api.js
. This module encapsulates the functionality for interacting with the resource server using the axios
HTTP client module. If you look at the code below you’ll see that this module contains clear methods for Create, Read, Update, and Delete. You’ll also notice how I’m able to create a base axios
instance that is configured with some global settings, such as the server URL and timeout. Later, this comes in handy when we need to configure the token authentication.
import axios from 'axios'
const SERVER_URL = 'http://localhost:9000';
const instance = axios.create({
baseURL: SERVER_URL,
timeout: 1000
});
export default {
// (C)reate
createNew: (text, completed) => instance.post('todos', {title: text, completed: completed}),
// (R)ead
getAll: () => instance.get('todos', {
transformResponse: [function (data) {
return data? JSON.parse(data)._embedded.todos : data;
}]
}),
// (U)pdate
updateForId: (id, text, completed) => instance.put('todos/'+id, {title: text, completed: completed}),
// (D)elete
removeForId: (id) => instance.delete('todos/'+id)
}
Open a shell in the /client
sub-directory.
Before running the client app, install the dependencies: yarn install
.
Go ahead and run the client using yarn serve
. Make sure your resource server is still running, as well. If it’s not, run it using ./gradlew bootRun
.
Open a browser and navigate to http://localhost:8080
.
Try it out! You can edit existing todos, delete them, and create new ones.
Create an OIDC Application for Vue Authentication
Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register
to sign up for a new account. If you already have an account, run okta login
.
Then, run okta apps create
. Select the default app name, or change it as you see fit.
Choose Single-Page App and press Enter.
Use http://localhost:8080/callback
for the Redirect URI and accept the default Logout Redirect URI of http://localhost:8080
.
What does the Okta CLI do?
The Okta CLI will create an OIDC Single-Page App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. It will also add a trusted origin for http://localhost:8080
. You will see output like the following when it’s finished:
Okta application configuration:
Issuer: https://dev-133337.okta.com/oauth2/default
Client ID: 0oab8eb55Kb9jdMIr5d6
NOTE: You can also use the Okta Admin Console to create your app. See Create a Vue App for more information.
Add Authentication to Vue
To configure Vue.js to use Okta as an OAuth 2.0 and OIDC provider, you’re going to use the okta-vue
module. This greatly simplifies integrating Okta authentication into your client application. You can take a look at the project GitHub page for more info.
Stop your client Vue.js application. Open a shell and, from the client
sub-directory of the example project, use Yarn to install okta-vue
.
yarn add @okta/okta-vue@2.0.0
Now create a src/router.js
file in the client app project.
import Auth from '@okta/okta-vue';
import Vue from 'vue'
import Router from 'vue-router'
import Todos from './components/Todos'
Vue.use(Auth, {
issuer: 'https://{yourOktaDomain}/oauth2/default',
clientId: '{yourClientId}',
redirectUri: window.location.origin + '/callback'
});
Vue.use(Router);
let router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'Todos',
component: Todos,
meta: {
requiresAuth: true
}
},
{
path: '/callback',
component: Auth.handleCallback(),
},
]
});
router.beforeEach(Vue.prototype.$auth.authRedirectGuard());
export default router;
Next, replace {yourClientId}
with the Client ID from the OIDC app you just created. You’ll also need to change {yourOktaDomain}
to your Okta developer domain—something like dev-123456.okta.com
. Make sure to remove the {...}
placeholders and just use the raw values.
The Okta Vue authentication plugin injects an authClient
object into your Vue instance which can be accessed by calling this.$auth
anywhere inside this instance.
There are only two routes. The home route is the todo app itself. The meta: { requiresAuth: true }
property turns on authentication for that route.
The other route, /callback
, is the OAuth 2.0 callback route that handles a successful authentication from the Okta servers.
Now you need to update the src/main.js
to use the router.
Add the following import statement near the top of the file:
import router from './router'
And update the Vue app instance to use the imported router, replacing the old Vue instance declaration:
new Vue({
el: '#app',
router, // <-- add this line
template: '<App/>',
components: { App }
})
Next, update the src/App.vue
module to match the following:
<template>
<div id="app">
<router-view :activeUser="activeUser"/>
<footer class="info">
<p v-if="activeUser" class="logout-link"><a @click="handleLogout" href="#">Logout</a></p>
<p>Based on a project written by <a href="http://evanyou.me">Evan You</a></p>
<p>Original Vue TodoApp project is <a href="https://vuejs.org/v2/examples/todomvc.html">here</a></p>
<p>Modified for this tutorial by Andrew Hughes</p>
</footer>
</div>
</template>
<script>
// app Vue instance
const app = {
name: 'app',
// app initial state
data: () => {
return {
activeUser: null
}
},
async created() {
await this.refreshActiveUser()
},
watch: {
'$route': 'refreshActiveUser'
},
methods: {
async refreshActiveUser() {
this.activeUser = await this.$auth.getUser()
this.$log.debug('activeUser', this.activeUser)
},
async handleLogout() {
await this.$auth.logout()
await this.refreshActiveUser()
this.$router.push({ path: '/' })
}
},
}
export default app
</script>
<style>
[v-cloak] {
display: none;
}
</style>
These changes demonstrate a couple of things. First, the code creates and updates a property, activeUser
, that passes information to the Todos module about the currently active user (if there is one, or null if there isn’t). It also adds a logout link to the footer.
The last thing you need to do is update the src/Api.js
file to add the access token to each request.
import axios from 'axios'
import Vue from 'vue'
const SERVER_URL = 'http://localhost:9000';
const instance = axios.create({
baseURL: SERVER_URL,
timeout: 1000
});
export default {
async execute(method, resource, data, config) {
let accessToken = await Vue.prototype.$auth.getAccessToken()
return instance({
method: method,
url: resource,
data,
headers: {
Authorization: `Bearer ${accessToken}`
},
...config
})
},
// (C)reate
createNew(text, completed) {
return this.execute('POST', 'todos', {title: text, completed: completed})
},
// (R)ead
getAll() {
return this.execute('GET', 'todos', null, {
transformResponse: [function (data) {
return data ? JSON.parse(data)._embedded.todos : data;
}]
})
},
// (U)pdate
updateForId(id, text, completed) {
return this.execute('PUT', 'todos/' + id, {title: text, completed: completed})
},
// (D)elete
removeForId(id) {
return this.execute('DELETE', 'todos/' + id)
}
}
These changes take the access token from the Okta Vue Auth module and inject it into the API request methods.
Test Your Vue App’s OAuth Flow
At this point, you can run the client application and it will force you to log in.
Run: yarn serve
and navigate to: http://localhost:8080
.
You may need to use a private or incognito browser window to see the login screen.
Once you log in, you’ll see the authenticated todo app with your email.
You’re not done yet, however. The Spring Boot resource server is still unsecured and isn’t requiring a valid JSON Web Token yet.
Configure Spring Boot Server for JWT Auth
To add OAuth 2.0 JSON Web Token (JWT) authentication to your Spring Boot project, Okta provides a helper project, the Okta Spring Boot Starter (check out the GitHub project).
You need to add this as a dependency in the build.gradle
file. Add the following dependency to the dependency
block.
dependencies {
implementation("com.okta.spring:okta-spring-boot-starter:1.4.0")
...
}
Next, create a new Kotlin file com/okta/springbootvue/SecurityConfiguration.kt
. This file configures the Spring Boot project to authorize all requests and to use JWT authentication.
package com.okta.springbootvue
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
@EnableWebSecurity
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity?) {
http!!.authorizeRequests()
.anyRequest().authenticated()
.and().oauth2ResourceServer().jwt()
}
}
Finally, add some properties to your src/main/resources/application.properties
file. Don’t forget to substitute in the correct values for your Okta domain and your OIDC Client ID (these are the same values you used above).
server.port=9000
okta.oauth2.issuer=https://{yourOktaDomain}/oauth2/default
okta.oauth2.clientId={yourClientId}
Start (or re-start) your Spring Boot resource server.
./gradlew bootRun
If you want to verify that this endpoint is now secure, use HTTPie to run a simple GET from another shell.
http :9000/todos
You’ll see that the previously public endpoint is now protected.
HTTP/1.1 401
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
...
That’s pretty much it. If you go back to the todo app, you’ll see that it’s still working with the protected resource server.
You can check out the auth
branch of the example project to see the finished, fully authenticated code.
git clone -b auth https://github.com/oktadeveloper/okta-kotlin-spring-boot-vue-example.git
Moving Forward with Okta, Vue, and Spring Boot
This tutorial covered quite a lot. You built a Vue.js client application and a Spring Boot REST service, using them to demonstrate a fully functioning CRUD application. You also added authentication using Okta and the Okta Vue SDK.
If you’d like to dig a little deeper, take a look at the Okta Vue SDK project.
The Spring Boot REST service used Spring Data’s JPA implementation to persist data based on a Java class. Spring Data and JPA is a very complex area, and the Spring docs on it are a great place to learn more.
Okta also has a number of other great related tutorials.
- Build a CRUD App with Angular 9 and Spring Boot 2.2
- Build a Basic CRUD App with Vue.js and Node
- Build a CRUD Application with Kotlin and React
- Build a Web App with Spring Boot and Spring Security in 15 Minutes
- 10 Excellent Ways to Secure Your Spring Boot Application
If you have any questions about this post, please add a comment below. For more awesome content, follow @oktadev on Twitter, like us on Facebook, or subscribe to our YouTube channel.
Changelog:
- Aug 31, 2020: Updated GitHub repo to have proper starter files and fixed logout in Vue. You can see the changes in the example app on GitHub. Changes to this article can be viewed in oktadeveloper/okta-blog#392.
Okta Developer Blog Comment Policy
We welcome relevant and respectful comments. Off-topic comments may be removed.