Spring Boot 3 + Vue JS 3 + PostgreSQL CRUD Application Example
After completing this tutorial what we will build?
We divided this tutorial into two parts.
PART 1 - Restful API Development with Spring Boot 3 & PostgreSQL
These are APIs that Spring backend App will export:
Backend project directory:
pom.xml
<?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>3.0.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>spring-boot3-crud-application</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot3-crud-application</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</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>
Configuring PostgreSQL Database
CREATE DATABASE users;
Now, let’s configure the Spring Boot application to use PostgreSQL as our data source. You can do that simply by adding PostgreSQL database URL, username, and password in the src/main/resources/application.properties file.
Open src/main/resources/application.properties file and add the following content to it:
spring.datasource.url=jdbc:postgresql://localhost:5432/users
spring.datasource.username=postgres
spring.datasource.password=root
spring.jpa.show-sql=true
# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update
Creating User Entity
package com.knf.dev.demo.crudapplication.entity;
import jakarta.persistence.*;
@Entity
@Table(name = "userTable")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@Column(name = "email_id")
private String emailId;
public User() {
}
public User(String firstName, String lastName, String emailId) {
super();
this.firstName = firstName;
this.lastName = lastName;
this.emailId = emailId;
}
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 getEmailId() {
return emailId;
}
public void setEmailId(String emailId) {
this.emailId = emailId;
}
}
Creating UserRepository
package com.knf.dev.demo.crudapplication.repository;
import com.knf.dev.demo.crudapplication.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
Create CustomErrorResponse
package com.knf.dev.demo.crudapplication.exception;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
public class CustomErrorResponse {
@JsonFormat(shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd hh:mm:ss")
private LocalDateTime timestamp;
private int status;
private String error;
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
Create ResourceNotFoundException
package com.knf.dev.demo.crudapplication.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException{
private static final long serialVersionUID = 1L;
public ResourceNotFoundException(String message) {
super(message);
}
}
Create GlobalExceptionHandler
package com.knf.dev.demo.crudapplication.exception;
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;
import java.time.LocalDateTime;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<CustomErrorResponse> globalExceptionHandler
(Exception ex, WebRequest request) {
CustomErrorResponse errors = new CustomErrorResponse();
errors.setTimestamp(LocalDateTime.now());
errors.setError(ex.getMessage());
errors.setStatus(HttpStatus.NOT_FOUND.value());
return new ResponseEntity<>(errors, HttpStatus.NOT_FOUND);
}
}
Create UserController
package com.knf.dev.demo.crudapplication.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.knf.dev.demo.crudapplication.entity.User;
import com.knf.dev.demo.crudapplication.exception.ResourceNotFoundException;
import com.knf.dev.demo.crudapplication.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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;
@CrossOrigin(origins = "*")
@RestController
@RequestMapping("/api/v1")
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping("/users")
public List<User> getAllUsers() {
return userRepository.findAll();
}
@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable(value = "id")
Long id) throws ResourceNotFoundException {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException
("User not found for this id :: " + id));
return ResponseEntity.ok().body(user);
}
@PostMapping("/users")
public User createUser(@RequestBody User user) {
return userRepository.save(user);
}
@PutMapping("/users/{id}")
public ResponseEntity<User> updateUser(@PathVariable(value = "id")
Long id, @RequestBody User userDto)
throws ResourceNotFoundException {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException
("User not found for this id :: " + id));
user.setEmailId(userDto.getEmailId());
user.setLastName(userDto.getLastName());
user.setFirstName(userDto.getFirstName());
user.setId(id);
final User updateUser = userRepository.save(user);
return ResponseEntity.ok(updateUser);
}
@DeleteMapping("/users/{id}")
public Map<String, Boolean> deleteUser(@PathVariable(value = "id")
Long id) throws ResourceNotFoundException {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException
("User not found for this id :: " + id));
userRepository.delete(user);
Map<String, Boolean> response = new HashMap<>();
response.put("deleted", Boolean.TRUE);
return response;
}
}
Main Driver
package com.knf.dev.demo.crudapplication;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CrudApplication {
public static void main(String[] args) {
SpringApplication.run(CrudApplication.class, args);
}
}
Run the application and verify REST APIs
Step 1: mvn clean install
Step 2: Run the Spring Boot application - mvn spring-boot:run
Add User:
PART 2 - UI development using Vue JS 3
Frontend Project Directory
package.json
{ "name": "vue-3-crud-application", "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.21.1", "bootstrap": "^4.6.0", "core-js": "^3.6.5", "jquery": "^3.6.0", "popper.js": "^1.16.1", "vue": "^3.0.0", "vue-router": "^4.0.6" }, "devDependencies": { "@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-plugin-eslint": "^5.0.8", "@vue/cli-service": "^5.0.8", "@vue/compiler-sfc": "^3.0.0", "babel-eslint": "^10.1.0", "eslint": "^7.32.0", "eslint-plugin-vue": "^7.0.0" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/vue3-essential", "eslint:recommended" ], "parserOptions": { "parser": "babel-eslint" }, "rules": {} }, "browserslist": [ "> 1%", "last 2 versions", "not dead" ]}
Components
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 Id</label> <input type="text" class="form-control" v-model="emailId" /> </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: "", emailId: "", 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.emailId = res.data.emailId; }); }, validateAndSubmit(e) { e.preventDefault(); this.errors = []; if (!this.firstName) { this.errors.push("Enter valid values"); } else if (this.firstName.length < 5) { this.errors.push("Enter atleast 5 characters in First Name"); } if (!this.lastName) { this.errors.push("Enter valid values"); } else if (this.lastName.length < 5) { this.errors.push("Enter atleast 5 characters in Last Name"); } if (this.errors.length === 0) { if (this.id == -1) { UserDataService.createUser({ firstName: this.firstName, lastName: this.lastName, emailId: this.emailId, }).then(() => { this.$router.push("/"); }); } else { UserDataService.updateUser(this.id, { id: this.id, firstName: this.firstName, lastName: this.lastName, emailId: this.emailId, }).then(() => { this.$router.push("/"); }); } } }, }, 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 Id</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.emailId }}</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:8080/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 3+ PostgreSQL + Vue 3 CRUD Application</a ><br /><br /> </div> <router-view /> </div></template>
<script>export default { name: "app",};</script>
router.js
import { createWebHistory, createRouter } from "vue-router";
const routes = [ { path: "/", name: "Users", component: () => import("./components/Users"), }, { path: "/user/:id", name: "User", component: () => import("./components/User"), },];
const router = createRouter({ history: createWebHistory(), routes,});
export default router;
main.js
import { createApp } from 'vue'import App from './App.vue'import 'bootstrap'import 'bootstrap/dist/css/bootstrap.min.css'import router from './router'
createApp(App).use(router).mount('#app')
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 -- --port 8081
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 -- --port 8081