DEV Community

StackPuz
StackPuz

Posted on • Originally published at blog.stackpuz.com on

Building a Vue CRUD App with a Spring Boot API

Vue CRUD App with a Spring Boot API

Building a CRUD app is a vital exercise for web developers. In this article, we’ll explore how to create a CRUD application with Vue on the front end and a Spring Boot API on the back end, illustrating the power and flexibility of this full-stack approach.

Prerequisites

  • Node.js
  • JAVA 17
  • Maven
  • MySQL

Setup Vue project

npm create vite@4.4.0 view -- --template vue
cd view
npm install vue-router@4 axios
Enter fullscreen mode Exit fullscreen mode

Vue project structure

├─ index.html
├─ public
│  └─ css
│     └─ style.css
└─ src
   ├─ App.vue
   ├─ components
   │  └─ product
   │     ├─ Create.vue
   │     ├─ Delete.vue
   │     ├─ Detail.vue
   │     ├─ Edit.vue
   │     ├─ Index.vue
   │     └─ Service.js
   ├─ http.js
   ├─ main.js
   └─ router.js
Enter fullscreen mode Exit fullscreen mode

Vue project files

main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')
Enter fullscreen mode Exit fullscreen mode

This main.js file is the entry point for a Vue.js application. It sets up and mounts the app with routing by importing the root component and router, creating the app instance, and configuring it with the router before mounting it to the #app element.

App.vue

<template>
  <router-view />
</template>

<script>
export default {
  name: 'App'
}
</script>
Enter fullscreen mode Exit fullscreen mode

This App.vue file defines the root component of a Vue.js application. It uses a <router-view /> in the template to display routed components based on the current route.

router.js

import { createWebHistory, createRouter } from 'vue-router'

const routes = [
  {
    path: '/',
    redirect: '/product'
  },
  {
    path: '/product',
    name: 'product',
    component: () => import('./components/product/Index.vue')
  },
  {
    path: '/product/create',
    component: () => import('./components/product/Create.vue')
  },
  {
    path: '/product/:id/',
    component: () => import('./components/product/Detail.vue')
  },
  {
    path: '/product/edit/:id/',
    component: () => import('./components/product/Edit.vue')
  },
  {
    path: '/product/delete/:id/',
    component: () => import('./components/product/Delete.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes,
})

export default router
Enter fullscreen mode Exit fullscreen mode

This router.js file configures routing for a Vue.js application. It sets up routes for various product-related views, including listing, creating, editing, and deleting products, as well as a default redirect to the product list. The router uses createWebHistory for HTML5 history mode and exports the configured router instance.

http.js

import axios from 'axios'

let http = axios.create({
  baseURL: 'http://localhost:5122/api',
  headers: {
    'Content-type': 'application/json'
  }
})

export default http
Enter fullscreen mode Exit fullscreen mode

The http.js file configures and exports an Axios instance with a centralized base URL, which is a standard practice for managing API endpoints and default headers set to application/json.

Create.vue

<template>
  <div class="container">
    <div class="row">
      <div class="col">
        <form method="post" @submit.prevent="create()">
          <div class="row">
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_name">Name</label>
              <input id="product_name" name="Name" class="form-control" v-model="product.name" maxlength="50" />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_price">Price</label>
              <input id="product_price" name="Price" class="form-control" v-model="product.price" type="number" />
            </div>
            <div class="col-12">
              <router-link class="btn btn-secondary" to="/product">Cancel</router-link>
              <button class="btn btn-primary">Submit</button>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
</template>
<script>
import Service from './Service'

export default {
  name: 'ProductCreate',
  data() {
    return {
      product: {}
    }
  },
  methods: {
    create() {
      Service.create(this.product).then(() => {
        this.$router.push('/product')
      }).catch((e) => {
        alert(e.response.data)
      })
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

This Create.vue component provides a form for adding a new product with fields for name and price. On submission, it calls a create method to save the product and redirects to the product list upon success. It also includes a cancel button to navigate back to the list and handles errors with an alert.

Delete.vue

<template>
  <div class="container">
    <div class="row">
      <div class="col">
        <form method="post" @submit.prevent="this.delete()">
          <div class="row">
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_id">Id</label>
              <input readonly id="product_id" name="Id" class="form-control" :value="product.id" type="number" required />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_name">Name</label>
              <input readonly id="product_name" name="Name" class="form-control" :value="product.name" maxlength="50" />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_price">Price</label>
              <input readonly id="product_price" name="Price" class="form-control" :value="product.price" type="number" />
            </div>
            <div class="col-12">
              <router-link class="btn btn-secondary" to="/product">Cancel</router-link>
              <button class="btn btn-danger">Delete</button>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
</template>
<script>
import Service from './Service'

export default {
  name: 'ProductDelete',
  data() {
    return {
      product: {}
    }
  },
  mounted() {
    this.get()
  },
  methods: {
    get() {
      return Service.delete(this.$route.params.id).then(response => {
        this.product = response.data
      })
    },
    delete() {
      Service.delete(this.$route.params.id, this.product).then(() => {
        this.$router.push('/product')
      }).catch((e) => {
        alert(e.response.data)
      })
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

The Delete.vue component provides a form for deleting a product, with read-only fields for the product's. The component fetches the product details when mounted. The form calls a delete method to remove the product and redirects to the product list upon success.

Detail.vue

<template>
  <div class="container">
    <div class="row">
      <div class="col">
        <form method="post">
          <div class="row">
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_id">Id</label>
              <input readonly id="product_id" name="Id" class="form-control" :value="product.id" type="number" required />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_name">Name</label>
              <input readonly id="product_name" name="Name" class="form-control" :value="product.name" maxlength="50" />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_price">Price</label>
              <input readonly id="product_price" name="Price" class="form-control" :value="product.price" type="number" />
            </div>
            <div class="col-12">
              <router-link class="btn btn-secondary" to="/product">Back</router-link>
              <router-link class="btn btn-primary" :to="`/product/edit/${product.id}`">Edit</router-link>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
</template>
<script>
import Service from './Service'

export default {
  name: 'ProductDetail',
  data() {
    return {
      product: {}
    }
  },
  mounted() {
    this.get()
  },
  methods: {
    get() {
      return Service.get(this.$route.params.id).then(response => {
        this.product = response.data
      })
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

The Detail.vue component displays detailed information about a product. It features read-only fields for the product's. The component fetches product details when mounted. It includes a "Back" button to navigate to the product list and an "Edit" button to navigate to the product's edit page.

Edit.vue

<template>
  <div class="container">
    <div class="row">
      <div class="col">
        <form method="post" @submit.prevent="edit()">
          <div class="row">
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_id">Id</label>
              <input readonly id="product_id" name="Id" class="form-control" v-model="product.id" type="number" required />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_name">Name</label>
              <input id="product_name" name="Name" class="form-control" v-model="product.name" maxlength="50" />
            </div>
            <div class="mb-3 col-md-6 col-lg-4">
              <label class="form-label" for="product_price">Price</label>
              <input id="product_price" name="Price" class="form-control" v-model="product.price" type="number" />
            </div>
            <div class="col-12">
              <router-link class="btn btn-secondary" to="/product">Cancel</router-link>
              <button class="btn btn-primary">Submit</button>
            </div>
          </div>
        </form>
      </div>
    </div>
  </div>
</template>
<script>
import Service from './Service'

export default {
  name: 'ProductEdit',
  data() {
    return {
      product: {}
    }
  },
  mounted() {
    this.get()
  },
  methods: {
    get() {
      return Service.edit(this.$route.params.id).then(response => {
        this.product = response.data
      })
    },
    edit() {
      Service.edit(this.$route.params.id, this.product).then(() => {
        this.$router.push('/product')
      }).catch((e) => {
        alert(e.response.data)
      })
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

The Edit.vue component provides a form for editing an existing product. It includes fields for the product's. The component fetches the product details when mounted and updates the product on form submission. It also features a "Cancel" button to navigate back to the product list and a "Submit" button to save the changes.

Index.vue

<template>
  <div class="container">
    <div class="row">
      <div class="col">
        <table class="table table-striped table-hover">
          <thead>
            <tr>
              <th>Id</th>
              <th>Name</th>
              <th>Price</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="product in products" :key="product">
              <td class="text-center">{{product.id}}</td>
              <td>{{product.name}}</td>
              <td class="text-center">{{product.price}}</td>
              <td class="text-center">
                <router-link class="btn btn-secondary" :to="`/product/${product.id}`" title="View"><i class="fa fa-eye"></i></router-link>
                <router-link class="btn btn-primary" :to="`/product/edit/${product.id}`" title="Edit"><i class="fa fa-pencil"></i></router-link>
                <router-link class="btn btn-danger" :to="`/product/delete/${product.id}`" title="Delete"><i class="fa fa-times"></i></router-link>
              </td>
            </tr>
          </tbody>
        </table>
        <router-link class="btn btn-primary" to="/product/create">Create</router-link>
      </div>
    </div>
  </div>
</template>
<script>
import Service from './Service'

export default {
  name: 'ProductIndex',
  data() {
    return {
      products: []
    }
  },
  mounted() {
    this.get()
  },
  methods: {
    get() {
      Service.get().then(response => {
        this.products = response.data
      }).catch(e => {
        alert(e.response.data)
      })
    }
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

The Index.vue component displays a table of products with columns for ID, name, and price. It fetches the list of products when mounted and populates the table. Each product row includes action buttons for viewing, editing, and deleting the product. There is also a "Create" button for adding new products.

Service.js

import http from '../../http'

export default {
  get(id) {
    if (id) {
      return http.get(`/products/${id}`)
    }
    else {
      return http.get('/products' + location.search)
    }
  },
  create(data) {
    if (data) {
      return http.post('/products', data)
    }
    else {
      return http.get('/products/create')
    }
  },
  edit(id, data) {
    if (data) {
      return http.put(`/products/${id}`, data)
    }
    else {
      return http.get(`/products/${id}`)
    }
  },
  delete(id, data) {
    if (data) {
      return http.delete(`/products/${id}`)
    }
    else {
      return http.get(`/products/${id}`)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The Service.js file defines API methods for handling product operations. It uses an http instance for making requests:

  • get(id) Retrieves a single product by ID or all products if no ID is provided.
  • create(data) Creates a new product with the provided data or fetches the creation form if no data is provided.
  • edit(id, data) Updates a product by ID with the provided data or fetches the product details if no data is provided.
  • delete(id, data) Deletes a product by ID or fetches the product details if no data is provided.

style.css

.container {
    margin-top: 2em;
}

.btn {
    margin-right: 0.25em;
}
Enter fullscreen mode Exit fullscreen mode

The CSS adjusts the layout by adding space above the container and spacing out buttons horizontally.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
  <link href="/css/style.css" rel="stylesheet">
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The HTML serves as the main entry point for a Vue application. It includes Bootstrap for styling and Font Awesome for icons. The application will render within a div with the ID app.

Setup Spring Boot API project

Create a testing database named "example" and execute the database.sql file to import the table and data.

Spring Boot API Project structure

├─ pom.xml
└─ src
   └─ main
      ├─ java
      │  └─ com
      │     └─ stackpuz
      │        └─ example
      │           ├─ App.java
      │           ├─ controller
      │           │  └─ ProductController.java
      │           ├─ entity
      │           │  └─ Product.java
      │           ├─ repository
      │           │  └─ ProductRepository.java
      │           └─ service
      │              └─ ProductService.java
      └─ resources
         └─ application.properties
Enter fullscreen mode Exit fullscreen mode

Spring Boot API Project files

pom.xml

This pom.xml file defines the configuration and dependencies for the Maven 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>
    <groupId>com.stackpuz</groupId>
    <artifactId>example-crud</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>example-crud</name>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.10</version>
    </parent>
    <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.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>
Enter fullscreen mode Exit fullscreen mode

application.properties

spring.datasource.url= jdbc:mysql://localhost/example
spring.datasource.username = root
spring.datasource.password = 
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQLDialect
Enter fullscreen mode Exit fullscreen mode

This is the Spring Boot application configuration file, which includes the database connection details.

App.java

package com.stackpuz.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }

}
Enter fullscreen mode Exit fullscreen mode

This App.java file serves as the main entry point for the Spring Boot application.

ProductRepository.java

package com.stackpuz.example.repository;

import com.stackpuz.example.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Integer> {

}
Enter fullscreen mode Exit fullscreen mode

The ProductRepository.java file defines a Spring Data JPA repository interface for the Product entity. It extends JpaRepository, providing CRUD operations and additional query methods for Product entities.

Product.java

package com.stackpuz.example.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.math.BigDecimal;

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Product {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private int id;
    private String name;
    private BigDecimal price;
}
Enter fullscreen mode Exit fullscreen mode

The Product.java file defines a JPA entity for the Product class in a Spring Boot application. Lombok annotations @Getter, @Setter, and @NoArgsConstructor are used to automatically generate getter and setter methods, and a no-argument constructor.

ProductService.java

package com.stackpuz.example.service;

import com.stackpuz.example.entity.Product;
import com.stackpuz.example.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ProductService {

    @Autowired
    private ProductRepository repository;

    public Product saveProduct(Product product) {
        return repository.save(product);
    }

    public List<Product> getProducts() {
        return repository.findAll();
    }

    public Product getProductById(int id) {
        return repository.findById(id).get();
    }

    public Product updateProduct(int id, Product product) {
        Product existing = repository.findById(id).get();
        existing.setName(product.getName());
        existing.setPrice(product.getPrice());
        return repository.save(existing);
    }

    public void deleteProduct(int id) {
        repository.deleteById(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

The ProductService.java file provides a service class for managing Product entities in a Spring Boot application. It uses the ProductRepository to perform CRUD operations. The @Service annotation marks the class as a Spring service component, and @Autowired injects the ProductRepository dependency.

ProductController.java

package com.stackpuz.example.controller;

import com.stackpuz.example.entity.Product;
import com.stackpuz.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@CrossOrigin(origins = "*", allowedHeaders = "*")
public class ProductController {

    @Autowired
    private ProductService service;

    @GetMapping("/api/products")
    public List<Product> getProducts() {
        return service.getProducts();
    }

    @GetMapping("/api/products/{id}")
    public Product getProduct(@PathVariable int id) {
        return service.getProductById(id);
    }

    @PostMapping("/api/products")
    public Product createProduct(@RequestBody Product product) {
        return service.saveProduct(product);
    }

    @PutMapping("/api/products/{id}")
    public Product updateProduct(@PathVariable int id, @RequestBody Product product) {
        return service.updateProduct(id, product);
    }

    @DeleteMapping("/api/products/{id}")
    public void deleteProduct(@PathVariable int id) {
        service.deleteProduct(id);
    }
}
Enter fullscreen mode Exit fullscreen mode

The ProductController.java file defines a Spring Boot REST controller for managing Product entities. It handles HTTP requests related to products with the following methods:

  • getProducts (GET) retrieves all products.
  • getProduct (GET) fetches a specific product by ID.
  • createProduct (POST) adds a new product.
  • updateProduct (PUT) modifies an existing product by ID.
  • deleteProduct (DELETE) removes a product by ID.

Run projects

Run Vue project

npm run dev
Enter fullscreen mode Exit fullscreen mode

Run Spring Boot API project

mvn spring-boot:run
Enter fullscreen mode Exit fullscreen mode

Open the web browser and goto http://localhost:5173

You will find this product list page.

list page

Testing

Click the "View" button to see the product details page.

details page

Click the "Edit" button to modify the product and update its details.

edit page

Click the "Submit" button to save the updated product details.

updated data

Click the "Create" button to add a new product and input its details.

create page

Click the "Submit" button to save the new product.

created product

Click the "Delete" button to remove the previously created product.

delete page

Click the "Delete" button to confirm the removal of this product.

deleted product

Conclusion

In conclusion, we have learned how to create a basic Vue project using Single-File Components (SFC) to build views and define application routing. By setting up a Spring Boot API as the backend and using Spring Data JPA for database operations, we've developed a dynamic front-end that integrates effectively with a powerful backend, laying a strong foundation for modern, full-stack web applications.

Source code: https://github.com/stackpuz/Example-CRUD-Vue-3-Spring-Boot-3

Create a Vue CRUD App in Minutes: https://stackpuz.com

Top comments (0)