This project demonstrates a full-stack CRUD application with:
Backend (Spring Boot 3)
- REST API with endpoints for CRUD operations
- Spring Data JPA for database interaction
- CORS configured for frontend access
- Layered architecture (Controller → Service → Repository)
Frontend (Vue 3)
- Vue Router for navigation
- Axios for API calls
- Reactive components for listing/editing data
- Modular structure (services, components, router)
Final Project Directory

Key Files Explanation:
Backend:
CorsConfig.java
: CORS configuration for allowing frontend requestsProductController.java
: REST API endpointsProduct.java
: JPA EntityProductRepository.java
: Spring Data JPA repositoryProductService.java
: Business logic layerapplication.properties
: Database configuration, server port, etc.
Frontend:
ProductList.vue
: Displays all products with edit/delete optionsProductForm.vue
: Form for adding/editing productsapi.js
: Axios HTTP client for API callsrouter/index.js
: Vue Router configurationApp.vue
: Main application component
This guide will walk you through creating a full-stack CRUD (Create, Read, Update, Delete) application using Spring Boot 3 for the backend and Vue 3 for the frontend.
Backend: Spring Boot 3
1. Setup Spring Boot Project
Create a new Spring Boot project using Spring Initializr with these dependencies:
- Spring Web
- Spring Data JPA
- Lombok (optional but recommended)
- Your preferred database driver (H2 for development, PostgreSQL/MySQL for production)
2. Entity Class
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String description;
private double price;
}
3. Repository Interface
public interface ProductRepository extends JpaRepository<Product, Long> {
}
4. Service Layer
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepository;
public List<Product> findAll() {
return productRepository.findAll();
}
public Optional<Product> findById(Long id) {
return productRepository.findById(id);
}
public Product save(Product product) {
return productRepository.save(product);
}
public void deleteById(Long id) {
productRepository.deleteById(id);
}
}
5. REST Controller
@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping
public List<Product> findAll() {
return productService.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<Product> findById(@PathVariable Long id) {
return productService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public Product create(@RequestBody Product product) {
return productService.save(product);
}
@PutMapping("/{id}")
public ResponseEntity<Product> update(@PathVariable Long id, @RequestBody Product product) {
if (!productService.findById(id).isPresent()) {
return ResponseEntity.notFound().build();
}
product.setId(id);
return ResponseEntity.ok(productService.save(product));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
if (!productService.findById(id).isPresent()) {
return ResponseEntity.notFound().build();
}
productService.deleteById(id);
return ResponseEntity.noContent().build();
}
}
6. CORS Configuration
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080") // Vue dev server
.allowedMethods("GET", "POST", "PUT", "DELETE");
}
};
}
}
Frontend: Vue 3
1. Setup Vue Project
npm init vue@latest vue-spring-crud
cd vue-spring-crud
npm install
npm install axios vue-router
2. API Service (src/services/api.js)
import axios from 'axios';
const apiClient = axios.create({
baseURL: 'http://localhost:8080/api',
withCredentials: false,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
});
export default {
getProducts() {
return apiClient.get('/products');
},
getProduct(id) {
return apiClient.get('/products/' + id);
},
createProduct(product) {
return apiClient.post('/products', product);
},
updateProduct(id, product) {
return apiClient.put('/products/' + id, product);
},
deleteProduct(id) {
return apiClient.delete('/products/' + id);
}
}
3. Product List Component (src/components/ProductList.vue)
<template>
<div>
<h1>Products</h1>
<router-link to="/products/add" class="btn btn-primary">Add Product</router-link>
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Price</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="product in products" :key="product.id">
<td>{{ product.name }}</td>
<td>{{ product.description }}</td>
<td>{{ product.price }}</td>
<td>
<router-link :to="'/products/edit/' + product.id" class="btn btn-info">Edit</router-link>
<button @click="deleteProduct(product.id)" class="btn btn-danger">Delete</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import api from '@/services/api';
export default {
data() {
return {
products: []
};
},
created() {
this.fetchProducts();
},
methods: {
async fetchProducts() {
try {
const response = await api.getProducts();
this.products = response.data;
} catch (error) {
console.error('Error fetching products:', error);
}
},
async deleteProduct(id) {
if (confirm('Are you sure you want to delete this product?')) {
try {
await api.deleteProduct(id);
this.fetchProducts();
} catch (error) {
console.error('Error deleting product:', error);
}
}
}
}
};
</script>
4. Product Form Component (src/components/ProductForm.vue)
<template>
<div>
<h1>{{ id ? 'Edit' : 'Add' }} Product</h1>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label>Name</label>
<input v-model="product.name" class="form-control" required>
</div>
<div class="form-group">
<label>Description</label>
<textarea v-model="product.description" class="form-control"></textarea>
</div>
<div class="form-group">
<label>Price</label>
<input v-model="product.price" type="number" step="0.01" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<router-link to="/" class="btn btn-secondary">Cancel</router-link>
</form>
</div>
</template>
<script>
import api from '@/services/api';
export default {
props: {
id: {
type: String,
default: null
}
},
data() {
return {
product: {
name: '',
description: '',
price: 0
}
};
},
created() {
if (this.id) {
this.fetchProduct();
}
},
methods: {
async fetchProduct() {
try {
const response = await api.getProduct(this.id);
this.product = response.data;
} catch (error) {
console.error('Error fetching product:', error);
}
},
async handleSubmit() {
try {
if (this.id) {
await api.updateProduct(this.id, this.product);
} else {
await api.createProduct(this.product);
}
this.$router.push('/');
} catch (error) {
console.error('Error saving product:', error);
}
}
}
};
</script>
5. Router Configuration (src/router/index.js)
import { createRouter, createWebHistory } from 'vue-router';
import ProductList from '@/components/ProductList.vue';
import ProductForm from '@/components/ProductForm.vue';
const routes = [
{
path: '/',
name: 'ProductList',
component: ProductList
},
{
path: '/products/add',
name: 'ProductAdd',
component: ProductForm
},
{
path: '/products/edit/:id',
name: 'ProductEdit',
component: ProductForm,
props: true
}
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
});
export default router;
6. App Component (src/App.vue)
<template>
<div class="container mt-5">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
/* Add your styles here */
</style>
Running the Application
1. Start the Spring Boot backend:
./mvnw spring-boot:run
2. Start the Vue frontend:
npm run dev
By default, it binds to:
- URL: http://localhost:5173
- Port: 5173 (avoids conflicts with Spring Boot's 8080)