Spring Boot + PostgreSQL + Vue.js CRUD: Full stack development example
Hello everyone, In this article, we will learn how to build a full-stack application that is a basic User Management Application using Spring Boot, PostgreSQL, and Vue.js.The GitHub repository link is provided at the end of this tutorial. You can download the source code.
After completing this tutorial what we will build?
We will build a full-stack web application that is a basic User Management Application with CRUD features:
• Create User
• List User
• Update User
• Delete User
We divided this tutorial into two parts
PART 1 - Rest APIs Development using Spring Boot and PostgreSQL
PART 2 - UI development using Vue.js, Axios & Bootstrap
PART 1 - Rest APIs Development using Spring Boot & PostgreSQL
These are APIs that Spring backend App will export:
- GET all User's : /api/v1/users : Get
- GET User by ID : /api/v1/users/{id} : Get
- CREATE User : /api/v1/users : Post
- UPDATE User : /api/v1/users/{id} : Put
- DELETE User : /api/v1/users/{id} : Delete
Backend Project Directory:
Maven[pom.xml]
A Project Object Model or POM is the fundamental unit of work in Maven. It is an XML file that contains information about the project and configuration details utilised by Maven to build the project. It contains default values for most projects. Some of the configurations that can be designated in the POM is the project dependencies, the plugins or goals that can be executed, the build profiles, and so on. Other information such as the project version, description, developers, mailing lists and such can withal be designated.
<?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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/>
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>backend</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
We need to override the H2 database properties being set by default in Spring Boot. The following properties are needed to configure PostgreSQL with Spring Boot. We can see these are pretty standard Java data source properties. Since in my example project, I’m using JPA too, we need to configure Hibernate for PostgreSQL too.
**create-drop option tells Hibernate to recreate the database on startup
spring.datasource.url= jdbc:postgresql://localhost:5432/postgres
spring.datasource.username= postgres
spring.datasource.password= password
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation= true
spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=create-drop
server.port=8081
Creating the Entity User
The @Entity annotation specifies that the class is an entity and is mapped to a database table. The @Id annotation specifies the primary key of an entity and the @GeneratedValue provides for the specification of generation strategies for the values of primary keys. The @Column annotation is used to specify the mapped column for a persistent property or field. If no Column annotation is specified, the default value will be applied.
package com.knf.dev.demo.backend.entity;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@Column(name = "email", nullable = false, length = 200)
private String email;
public User() {
super();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public User(String firstName, String lastName,
String email) {
super();
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
}
Creating the User Repository
CrudRepository is a Spring Data interface for generic CRUD operations on a repository of a specific type. It provides several methods out of the box for interacting with a database.
package com.knf.dev.demo.backend.repository;
import org.springframework.data.repository.CrudRepository;
import com.knf.dev.demo.backend.entity.User;
public interface UserRepository
extends CrudRepository<User, Long> {
}
Exception Handler with Controller Advice
Spring supports exception handling by a global Exception Handler (@ExceptionHandler) with Controller Advice (@ControllerAdvice). This enables a mechanism that makes ResponseEntity work with the type safety and flexibility of @ExceptionHandler.
package com.knf.dev.demo.backend.exception;
import java.util.Date;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(InternalServerError.class)
public ResponseEntity<ErrorMessage>
internalServerError(Exception ex, WebRequest request) {
ErrorMessage errors =
new ErrorMessage(500, new Date(),
ex.getMessage(), "Internal Server Error");
return new ResponseEntity<>
(errors, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(UserNotFound.class)
public ResponseEntity<ErrorMessage>
userNotFound(Exception ex, WebRequest request) {
ErrorMessage errors =
new ErrorMessage(404, new Date(),
ex.getMessage(), "User Not Found");
return new ResponseEntity<>
(errors, HttpStatus.NOT_FOUND);
}
}
Custom Exception - Internal Server Error
package com.knf.dev.demo.backend.exception;
public class InternalServerError extends RuntimeException {
private static final long serialVersionUID = 1L;
public InternalServerError(String msg) {
super(msg);
}
}
Custom Exception - UserNotFound
package com.knf.dev.demo.backend.exception;
public class UserNotFound extends RuntimeException {
private static final long serialVersionUID = 1L;
public UserNotFound(String msg) {
super(msg);
}
}
Error Message
package com.knf.dev.demo.backend.exception;
import java.util.Date;
public class ErrorMessage {
private Integer statusCode;
private Date timestamp;
private String message;
private String description;
public Integer getStatusCode() {
return statusCode;
}
public void setStatusCode(Integer statusCode) {
this.statusCode = statusCode;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public ErrorMessage(Integer statusCode,
Date timestamp, String message,
String description) {
super();
this.statusCode = statusCode;
this.timestamp = timestamp;
this.message = message;
this.description = description;
}
}
User Rest Controller
The @RestController annotation was introduced in Spring 4.0 to simplify the engendering of RESTful web services. It's a convenience annotation that combines @Controller and @ResponseBody. @RequestMapping annotation maps HTTP requests to handler methods of MVC and REST controllers.
package com.knf.dev.demo.backend.controller;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.knf.dev.demo.backend.entity.User;
import com.knf.dev.demo.backend.exception.InternalServerError;
import com.knf.dev.demo.backend.exception.UserNotFound;
import com.knf.dev.demo.backend.repository.UserRepository;
@RestController
@RequestMapping("/api/v1/users")
@CrossOrigin(origins = "http://localhost:8080")
public class UserController {
@Autowired
UserRepository userRepository;
// Create user
@PostMapping
public ResponseEntity<User>
createUser(@RequestBody User user) {
try {
User newuser = new User(user.getFirstName(),
user.getLastName(), user.getEmail());
userRepository.save(newuser);
return new ResponseEntity<>
(newuser, HttpStatus.CREATED);
} catch (Exception e) {
throw new InternalServerError(e.getMessage());
}
}
// Update user
@PutMapping("/{id}")
public ResponseEntity<User>
updateUser(@PathVariable("id") Long id,
@RequestBody User user) {
Optional<User> userdata = userRepository.findById(id);
if (userdata.isPresent()) {
User _user = userdata.get();
_user.setEmail(user.getEmail());
_user.setFirstName(user.getFirstName());
_user.setLastName(user.getLastName());
return new ResponseEntity<>
(userRepository.save(_user), HttpStatus.OK);
} else {
throw new UserNotFound("Invalid User Id");
}
}
// Get all Users
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
try {
List<User> users = new ArrayList<User>();
userRepository.findAll().forEach(users::add);
return new ResponseEntity<>(users, HttpStatus.OK);
} catch (Exception e) {
throw new InternalServerError(e.getMessage());
}
}
// Get user by ID
@GetMapping("/{id}")
public ResponseEntity<User>
getUserByID(@PathVariable("id") Long id) {
Optional<User> userdata = userRepository.findById(id);
if (userdata.isPresent()) {
return new ResponseEntity<>
(userdata.get(), HttpStatus.OK);
} else {
throw new UserNotFound("Invalid User Id");
}
}
// Delete user
@DeleteMapping("/{id}")
public ResponseEntity<User>
deleteUser(@PathVariable("id") Long id) {
Optional<User> userdata = userRepository.findById(id);
if (userdata.isPresent()) {
userRepository.deleteById(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} else {
throw new UserNotFound("Invalid User Id");
}
}
}
Spring Boot main driver
package com.knf.dev.demo.backend;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BackendApplication {
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}
}
PART 2 - UI development using Vue.js
Frontend project directory:
A package.json is a JSON file that subsists at the root of a Javascript/Node project. It holds metadata pertinent to the project and is utilized for managing the project's dependencies, scripts, version, and a whole lot more.
{ "name": "frontend", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "axios": "^0.18.0", "vue": "^2.6.6", "vue-router": "^3.0.2" }, "devDependencies": { "@vue/cli-plugin-babel": "^3.5.0", "@vue/cli-plugin-eslint": "^3.5.0", "@vue/cli-service": "^3.5.0", "babel-eslint": "^10.0.1", "eslint": "^5.8.0", "eslint-plugin-vue": "^5.0.0", "vue-template-compiler": "^2.5.21" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/essential", "eslint:recommended" ], "rules": {}, "parserOptions": { "parser": "babel-eslint" } }, "postcss": { "plugins": { "autoprefixer": {} } }, "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ]}
Components
Vue Components are one of the important features of VueJS that creates custom elements, which can be reused in HTML
User.vue
<template> <div> <h3>User</h3> <div class="container"> <form @submit="validateAndSubmit"> <div v-if="errors.length"> <div class="alert alert-danger" v-bind:key="index" v-for="(error, index) in errors" > {{ error }} </div> </div> <fieldset class="form-group"> <label>First Name</label> <input type="text" class="form-control" v-model="firstName" /> </fieldset> <fieldset class="form-group"> <label>Last Name</label> <input type="text" class="form-control" v-model="lastName" /> </fieldset> <fieldset class="form-group"> <label>Email</label> <input type="text" class="form-control" v-model="email" /> </fieldset> <button class="btn btn-success" type="submit">Save</button> </form> </div> </div></template><script>import UserDataService from "../service/UserDataService";
export default { name: "User", data() { return { firstName: "", lastName: "", email: "", errors: [], }; }, computed: { id() { return this.$route.params.id; }, }, methods: { refreshUserDetails() { UserDataService.retrieveUser(this.id).then((res) => { this.firstName = res.data.firstName; this.lastName = res.data.lastName; this.email = res.data.email; }); }, validateAndSubmit(e) { e.preventDefault(); this.errors = []; if (!this.firstName) { this.errors.push("Enter valid values"); } else if (this.firstName.length < 4) { this.errors.push ("Enter atleast 4 characters in First Name"); } if (!this.lastName) { this.errors.push("Enter valid values"); } else if (this.lastName.length < 4) { this.errors.push ("Enter atleast 4 characters in Last Name"); } if (this.errors.length === 0) { if (this.id == -1) { UserDataService.createUser({ firstName: this.firstName, lastName: this.lastName, email: this.email, }).then(() => { this.$router.push("/users"); }); } else { UserDataService.updateUser(this.id, { id: this.id, firstName: this.firstName, lastName: this.lastName, email: this.email, }).then(() => { this.$router.push("/users"); }); } } }, }, created() { this.refreshUserDetails(); },};</script>
Users.vue
<template> <div class="container"> <h3>All Users</h3> <div v-if="message" class="alert alert-success"> {{ this.message }}</div> <div class="container"> <table class="table"> <thead> <tr> <th>First Name</th> <th>Last Name</th> <th>Email</th> <th>Update</th> <th>Delete</th> </tr> </thead> <tbody> <tr v-for="user in users" v-bind:key="user.id"> <td>{{ user.firstName }}</td> <td>{{ user.lastName }}</td> <td>{{ user.email }}</td> <td> <button class="btn btn-warning" v-on:click="updateUser(user.id)"> Update </button> </td> <td> <button class="btn btn-danger" v-on:click="deleteUser(user.id)"> Delete </button> </td> </tr> </tbody> </table> <div class="row"> <button class="btn btn-success" v-on:click="addUser()">Add</button> </div> </div> </div></template><script>import UserDataService from "../service/UserDataService";
export default { name: "Users", data() { return { users: [], message: "", }; }, methods: { refreshUsers() { UserDataService.retrieveAllUsers().then((res) => { this.users = res.data; }); }, addUser() { this.$router.push(`/user/-1`); }, updateUser(id) { this.$router.push(`/user/${id}`); }, deleteUser(id) { UserDataService.deleteUser(id).then(() => { this.refreshUsers(); }); }, }, created() { this.refreshUsers(); },};</script>
UserDataService.js
import axios from 'axios'
const USER_API_URL = 'http://localhost:8081/api/v1'
class UserDataService {
retrieveAllUsers() {
return axios.get(`${USER_API_URL}/users`); }
retrieveUser(id) {
return axios.get(`${USER_API_URL}/users/${id}`); }
deleteUser(id) {
return axios.delete(`${USER_API_URL}/users/${id}`); }
updateUser(id, user) {
return axios.put(`${USER_API_URL}/users/${id}`, user); }
createUser(user) {
return axios.post(`${USER_API_URL}/users`, user); } }
export default new UserDataService()
App.vue
<template> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="#"> Spring Boot + PostgreSQL + Vue CRUD Application </a><br/><br/> </div> <router-view/> </div></template>
<script>export default { name: "app"};</script>
<style>@import url(https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css);
</style>
routes.js
import Vue from "vue";import Router from "vue-router";
Vue.use(Router);
const router = new Router({ mode: 'history', routes: [ { path: "/", name: "Users", component: () => import("./components/Users"), }, { path: "/users", name: "Users", component: () => import("./components/Users"), }, { path: "/user/:id", name: "User", component: () => import("./components/User"), }, ]});
export default router;
main.js
import Vue from 'vue'import App from './App.vue'import router from './routes';
Vue.config.productionTip = false
new Vue({ router, render: h => h(App),}).$mount('#app')
index.html
<!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>spring-boot-vue-postgresql</title></head>
<body> <noscript> <strong>We're sorry but spring-boot-vue-postgresql crud 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>
Download the complete source code - click here
Local Setup and Run the application
Step1: Download or clone the source code from GitHub to the local machine - Click here
Backend
Step 2: mvn clean install
Step 3: Run the Spring Boot application - mvn spring-boot:run
Frontend
Step 4: npm install
Step 5: npm run serve