Spring Boot 3 + Vue JS 3 + PostgreSQL CRUD Application Example

In this section, we will learn how to develop a full-stack web application that is a basic User Management Application using Spring Boot 3, PostgreSQL, and Vue 3. You could download the source code from our Github repository, the download link is provided at the end of this tutorial.



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 - Restful API Development with Spring Boot 3 & PostgreSQL.
PART 2 - UI development using Vue JS 3.


PART 1 - Restful API Development with Spring Boot 3 & PostgreSQL

These are APIs that Spring backend App will export:



Backend project directory:




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 utilized by Maven to build the project.
<?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

First, you need to create a database in the PostgreSQL server. You can use the following command to create a database in the PostgresSQL server:
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;
}
}
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.


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:


Update User by ID:


Fetch all Users:


Get User by ID:


Delete User by ID:



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

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 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

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

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