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: 

  1. GET all User's        :     /api/v1/users             : Get 
  2. GET User by ID      :     /api/v1/users/{id}       : Get
  3. CREATE User        :     /api/v1/users             : Post
  4. UPDATE User        :     /api/v1/users/{id}       : Put
  5. 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:



package.json 

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

From the browser call the endpoint http://localhost:8080

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete