In the previous article, we have implemented user registration email verification using the Freemarker template engine. In this article, we are gonna implement CRUD operations in the same Spring Boot and Angular application.
What You’ll Build
Angular Frontend
Create Page
List Page
View Page
Edit Page
Spring Boot Backend
Implement CRUD REST APIs for performing CRUD operations on the product entity.
What You’ll Need
Run the following checklist before you begin the implementation:
- Spring Tool Suite 4 or any other IDE of your choice
- Lombok
- JDK 11
- Maven
- Spring Boot + Angular Application Source Code
Angular Client Implementation
Modify App Component
app.component.html
Add a new menu to the product management page.
<ul class="navbar-nav me-auto mb-2 mb-md-0" routerLinkActive="active">
<li class="nav-item"><a href="/home" class="nav-link" routerLink="home">Home</a></li>
<li class="nav-item" *ngIf="showAdminBoard"><a href="/admin" class="nav-link" routerLink="admin">Admin Board</a></li>
<li class="nav-item" *ngIf="showModeratorBoard"><a href="/mod" class="nav-link" routerLink="mod">Moderator Board</a></li>
<li class="nav-item"><a href="/user" class="nav-link" *ngIf="isLoggedIn" routerLink="user">User</a></li>
<li class="nav-item"><a href="/order" class="nav-link" *ngIf="isLoggedIn" routerLink="order">Order</a></li>
<li class="nav-item" *ngIf="showAdminBoard"><a href="/products/list" class="nav-link" *ngIf="isLoggedIn" routerLink="products">Products</a></li>
</ul>
Modify App Constants
app.constants.ts
Define a static variable called httpOptions
to hold the HTTP headers. So that it can be reused in the service classes.
import { environment } from '../../environments/environment';
import { HttpHeaders } from '@angular/common/http';
export class AppConstants {
private static API_BASE_URL = environment.apiBaseUrl;
private static OAUTH2_URL = AppConstants.API_BASE_URL + "oauth2/authorization/";
private static REDIRECT_URL = environment.clientUrl;
public static API_URL = AppConstants.API_BASE_URL + "api/";
public static AUTH_API = AppConstants.API_URL + "auth/";
public static GOOGLE_AUTH_URL = AppConstants.OAUTH2_URL + "google" + AppConstants.REDIRECT_URL;
public static FACEBOOK_AUTH_URL = AppConstants.OAUTH2_URL + "facebook" + AppConstants.REDIRECT_URL;
public static GITHUB_AUTH_URL = AppConstants.OAUTH2_URL + "github" + AppConstants.REDIRECT_URL;
public static LINKEDIN_AUTH_URL = AppConstants.OAUTH2_URL + "linkedin" + AppConstants.REDIRECT_URL;
public static httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
}
Create Product Model Class
product.ts
This is just a model class that maps to the Product entity in the backend.
export interface Product {
id: number;
name: string;
version: string;
edition: string;
description: string;
}
Create Product Service
product.service.ts
This is just a service class that calls backend services.
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AppConstants } from '../common/app.constants';
import { Product } from '../products/product';
@Injectable({providedIn: 'root'})
export class ProductService {
constructor(private httpClient: HttpClient) {
}
getAll(request): Observable<any> {
const params = request;
return this.httpClient.get(AppConstants.API_URL + 'products', {params});
}
create(product:any): Observable<any> {
return this.httpClient.post(AppConstants.API_URL + 'products', JSON.stringify(product), AppConstants.httpOptions)
}
find(id:number): Observable<any> {
return this.httpClient.get<Product>(AppConstants.API_URL + 'products/' + id)
}
update(id:number, product:Product): Observable<any> {
return this.httpClient.put(AppConstants.API_URL + 'products/' + id, JSON.stringify(product), AppConstants.httpOptions)
}
delete(id:number){
return this.httpClient.delete(AppConstants.API_URL + 'products/' + id)
}
}
Create Product List Component
list.component.ts
This component does the following:
- Declares a
products
field to hold the list of products. - The
ngOnInit()
method calls theproductService.getAll()
that returns anObservable
object. The response will hold theproducts
in thecontent
attribute along with the paging meta data returned by the REST API. So that we can implement pagination in the next article. - Binds the delete button
onclick
event to thedeleteProduct()
method. This method calls theproductService.delete()
method to delete the product in the backend. Once the product is deleted, then it removes the product from the list. In case of failure, the error message will be displayed in the same page.
import { Component, OnInit } from '@angular/core';
import { ProductService } from '../_services/product.service';
import { Product } from './product';
@Component({templateUrl: './list.component.html'})
export class ProductListComponent implements OnInit {
products: Product[] = [];
constructor(public productService: ProductService) {
}
ngOnInit(): void {
this.getProducts({ page: "0", size: "5" });
}
private getProducts(request) {
this.productService.getAll(request)
.subscribe(data => {
this.products = data['content'];
}
, error => {
console.log(error.error.message);
}
);
}
deleteProduct(id:number){
this.productService.delete(id)
.subscribe(data => {
this.products = this.products.filter(item => item.id !== id);
console.log('Product deleted successfully!');
}
, error => {
console.log(error.error.message);
}
);
}
}
list.component.html
The view and edit button is mapped with /products/{id}/view
and /products/{id}/edit
paths respectively.
<div class="container">
<h5 class="text-center mt-3">PRODUCTS</h5>
<div class="d-flex my-2">
<a href="#" routerLink="/products/add" class="btn btn-outline-success ms-auto">Create New Product</a>
</div>
<div class="table-responsive">
<table class="table table-bordered p-0 m-0">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Description</th>
<th scope="col">Version</th>
<th scope="col">Edition</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<tr scope="row" *ngFor="let product of products">
<td>{{ product.id }}</td>
<td>{{ product.name }}</td>
<td>{{ product.description}}</td>
<td>{{ product.version }}</td>
<td>{{ product.edition }}</td>
<td><a href="#" [routerLink]="['/products/', product.id, 'view']" class="btn btn-info m-1">View</a> <a href="#"
[routerLink]="['/products/', product.id, 'edit']" class="btn btn-primary m-1">Edit</a>
<button type="button" (click)="deleteProduct(product.id)" class="btn btn-danger m-1">Delete</button></td>
</tr>
<tr scope="row" *ngIf="products.length === 0">
<td colspan="6" class="text-center">No record found</td>
</tr>
</tbody>
</table>
</div>
</div>
Create Product Add/Edit Component
We are gonna create an Angular Reactive form instead of Template driven form. To learn more about the difference between Template Driven Form and Reactive Form, refer Template Driven vs Model Driven. Also, we are gonna use the same form for both add and edit operations. If path contains the product id
as a path variable, then the product will be fetched by the id
from the backend and the form will be populated for editing. Else it is an add operation.
add-edit.component.ts
This component does the following:
- Declares,
- An
id
field to hold theproduct id
in edit mode. - A
form
for adding/editing product details - An
errorMessage
field to hold the error message if any
- An
- The
ngOnInit()
method gets theid
from the URL if exists and calls theproductService.find()
that returns anObservable
object. Then, it calls thethis.form.patchValue(x)
method to populate the form with the values from the returned object. - The
onSubmit()
method calls theproductService.update()
method If it is in edit mode. Else it calls theproductService.create()
method. Once the product is created/updated, then it navigates to the product list page. In case of failure, the error message will be displayed in the same page.
import { Component, OnInit } from '@angular/core';
import { ProductService } from '../_services/product.service';
import { Router, ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';
@Component({templateUrl: './add-edit.component.html'})
export class ProductAddEditComponent implements OnInit {
id: any;
form: FormGroup;
errorMessage;
constructor(public productService: ProductService, private router: Router, private fb: FormBuilder, private route: ActivatedRoute) {
this.createForm();
}
ngOnInit(): void {
this.id = this.route.snapshot.paramMap.get('id');
console.log(this.id);
if(this.id){
this.productService.find(this.id).subscribe(x => this.form.patchValue(x));
}
}
createForm() {
this.form = this.fb.group({
name: ['', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(20),
] ],
description: ['', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(100),
] ],
version: ['', Validators.required],
edition: ['', Validators.maxLength(20)]
});
}
onSubmit(){
var response = this.id ? this.productService.update(this.id, this.form.value) : this.productService.create(this.form.value);
response.subscribe(
data => {
console.log('Product created / updated successfully!');
this.router.navigateByUrl('products/list');
}
,
err => {
this.errorMessage = err.error.message;
}
);
}
}
add-edit.component.html
<div class="card card-container form">
<h5 class="text-center">{{ id ? 'Edit' : 'Add'}} Product</h5>
<div class="row">
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="col s12">
<div class="row">
<div class="col s12">
<label for="name">Name</label> <input type="text" class="form-control" name="name" formControlName="name" />
<div class="alert-danger">
<div *ngIf="form.controls.name.touched && form.controls.name.errors?.required">* Required</div>
<div *ngIf="form.controls.name.touched && form.controls.name.errors?.minlength">Minimum length is {{form.controls.name.errors?.minlength.requiredLength}}
and actual is {{form.controls.name.errors?.minlength.actualLength}}</div>
<div *ngIf="form.controls.name.touched && form.controls.name.errors?.maxlength">Maximum length is {{form.controls.name.errors?.maxlength.requiredLength}}
and actual is {{form.controls.name.errors?.maxlength.actualLength}}</div>
</div>
</div>
</div>
<div class="row">
<div class="col s12">
<label for="description">Description</label> <input type="text" class="form-control" name="description" formControlName="description" />
<div class="alert-danger">
<div *ngIf="form.controls.description.touched && form.controls.description.errors?.required">* Required</div>
<div *ngIf="form.controls.description.touched && form.controls.description.errors?.minlength">Minimum length is
{{form.controls.description.errors?.minlength.requiredLength}} and actual is {{form.controls.description.errors?.minlength.actualLength}}</div>
<div *ngIf="form.controls.description.touched && form.controls.description.errors?.maxlength">Maximum length is
{{form.controls.description.errors?.maxlength.requiredLength}} and actual is {{form.controls.description.errors?.maxlength.actualLength}}</div>
</div>
</div>
</div>
<div class="row">
<div class="col s12">
<label for="version">Version</label> <input type="text" class="form-control" name="version" formControlName="version" />
<div class="alert-danger">
<div *ngIf="form.controls.version.touched && form.controls.version.errors?.required">* Required</div>
</div>
</div>
</div>
<div class="row">
<div class="col s12">
<label for="edition">Edition</label> <input type="text" class="form-control" name="edition" formControlName="edition" />
<div class="alert-danger">
<div *ngIf="form.controls.edition.touched && form.controls.edition.errors?.maxlength">Maximum length is
{{form.controls.edition.errors?.maxlength.requiredLength}} and actual is {{form.controls.edition.errors?.maxlength.actualLength}}</div>
</div>
</div>
</div>
<br />
<div class="d-flex gap-2">
<a href="#" routerLink="/products/list" class="btn btn-secondary w-100">Cancel</a>
<button class="btn btn-primary w-100" [disabled]="form.pristine || form.invalid">{{ id ? 'Update' : 'Create'}}</button>
</div>
<div class="alert alert-warning" *ngIf="errorMessage">Failed: {{ errorMessage }}</div>
</form>
</div>
</div>
Create Product View Component
view.component.ts
This component does the following:
- Declares,
- An
id
field to hold theproduct id
from the path. - A
product
field of typeProduct
to hold the product object fetched from the backend
- An
- The
ngOnInit()
method gets theid
from the URL if exists and calls theproductService.find()
that returns anObservable
object. Then, the response is assigned to theproduct
field.
import { Component, OnInit } from '@angular/core';
import { ProductService } from '../_services/product.service';
import { ActivatedRoute, Router } from '@angular/router';
import { Product } from './product';
@Component({templateUrl: './view.component.html'})
export class ProductViewComponent implements OnInit {
id: number;
product: Product;
constructor(
public productService: ProductService,
private route: ActivatedRoute,
private router: Router
) { }
ngOnInit(): void {
this.id = this.route.snapshot.params['id'];
this.productService.find(this.id).subscribe((data: Product)=>{
this.product = data;
});
}
}
view.component.html
<div class="container">
<h5 class="text-center">PRODUCT DETAILS</h5>
<div class="table-responsive">
<table class="table table-bordered p-0 m-0">
<tbody>
<tr scope="row">
<th>ID</th>
<td>{{ product?.id }}</td>
</tr>
<tr scope="row">
<th>Name</th>
<td>{{ product?.name }}</td>
</tr>
<tr scope="row">
<th>Description</th>
<td>{{ product?.description }}</td>
</tr>
<tr scope="row">
<th>Version</th>
<td>{{ product?.version }}</td>
</tr>
<tr scope="row">
<th>Edition</th>
<td>{{ product?.edition }}</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex">
<a href="#" routerLink="/products/list" class="btn btn-primary mt-2 w-100">Back</a>
</div>
</div>
Note: Safe navigation operator ( ?. ) is an Angular template expression operator used for avoiding exception for null and undefined values in property paths.
Define Module
app.module.ts
Import and add ReactiveFormsModule
and the newly created components in the module declarations
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { BoardAdminComponent } from './board-admin/board-admin.component';
import { BoardModeratorComponent } from './board-moderator/board-moderator.component';
import { BoardUserComponent } from './board-user/board-user.component';
import { TotpComponent } from './totp/totp.component';
import { OrderComponent } from './order/order.component';
import { TokenComponent } from './register/token.component';
import { ProductListComponent } from './products/list.component';
import { ProductViewComponent } from './products/view.component';
import { ProductAddEditComponent } from './products/add-edit.component';
import { authInterceptorProviders } from './_helpers/auth.interceptor';
@NgModule({
declarations: [
AppComponent,
LoginComponent,
RegisterComponent,
HomeComponent,
ProfileComponent,
BoardAdminComponent,
BoardModeratorComponent,
BoardUserComponent,
TotpComponent,
OrderComponent,
TokenComponent,
ProductListComponent,
ProductViewComponent,
ProductAddEditComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule,
HttpClientModule
],
providers: [authInterceptorProviders],
bootstrap: [AppComponent]
})
export class AppModule { }
Define Module Routing
app-routing.module.ts
Import and add the newly created components in the route declarations
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { RegisterComponent } from './register/register.component';
import { LoginComponent } from './login/login.component';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { BoardUserComponent } from './board-user/board-user.component';
import { BoardModeratorComponent } from './board-moderator/board-moderator.component';
import { BoardAdminComponent } from './board-admin/board-admin.component';
import { TotpComponent } from './totp/totp.component';
import { OrderComponent } from './order/order.component';
import { TokenComponent } from './register/token.component';
import { ProductListComponent } from './products/list.component';
import { ProductViewComponent } from './products/view.component';
import { ProductAddEditComponent } from './products/add-edit.component';
const routes: Routes = [
{ path: 'home', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
{ path: 'profile', component: ProfileComponent },
{ path: 'user', component: BoardUserComponent },
{ path: 'mod', component: BoardModeratorComponent },
{ path: 'admin', component: BoardAdminComponent },
{ path: 'totp', component: TotpComponent },
{ path: 'order', component: OrderComponent },
{ path: 'verify', component: TokenComponent },
{ path: 'products', redirectTo: 'products/list', pathMatch: 'full'},
{ path: 'products/list', component: ProductListComponent },
{ path: 'products/:id/view', component: ProductViewComponent },
{ path: 'products/add', component: ProductAddEditComponent },
{ path: 'products/:id/edit', component: ProductAddEditComponent },
{ path: '', redirectTo: 'home', pathMatch: 'full' }
];
@NgModule({
imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })],
exports: [RouterModule]
})
export class AppRoutingModule { }
Spring Boot Backend Implementation
Add Constants
AppConstants.java
Modify this class to define a new constant.
public static final String ID = "ID";
Create Model Class
ProductRequest.java
This is a model class (DTO) that maps to the product form
in the front end.
package com.javachinna.dto;
import javax.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class ProductRequest {
@NotBlank
private String name;
@NotBlank
private String version;
private String edition;
@NotBlank
private String description;
}
Create JPA Entity
BaseEntity.java
This is a base class that contains the id
field and overrides the hashcode
and equals
methods. So that all the entities can extend from it instead of defining these in each entity class.
package com.javachinna.model;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@MappedSuperclass
public abstract class BaseEntity implements Serializable {
/**
*
*/
private static final long serialVersionUID = -7363399724812884337L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false, nullable = false)
protected Long id;
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!this.getClass().isInstance(o))
return false;
BaseEntity other = (BaseEntity) o;
return id != null && id.equals(other.getId());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
Product.java
This Product
entity maps to the Product
table in the database
package com.javachinna.model;
import javax.persistence.Entity;
import javax.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Entity
@Table(name = "product")
@Getter
@Setter
@ToString
@RequiredArgsConstructor
@NoArgsConstructor
public class Product extends BaseEntity {
/**
*
*/
private static final long serialVersionUID = 1493867573442690205L;
@NonNull
private String name;
@NonNull
private String version;
@NonNull
private String edition;
@NonNull
private String description;
}
Create Spring Data Repository
ProductRepository.java
This repository is responsible for querying the product
table.
package com.javachinna.repo;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;
import com.javachinna.model.Product;
@Repository
public interface ProductRepository extends PagingAndSortingRepository<Product, Long> {
}
Create Service Class
Create Generic Service Interface
Service.java
This is just an interface used to define generic methods required for any service class.
package com.javachinna.service;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public interface Service<T, R> {
T save(R r);
void update(Long id, R r);
void deleteById(Long id);
Optional<T> findById(Long id);
Page<T> findAll(Pageable pageable);
}
Create Product Service
ProductService.java
This is just an interface used to define the methods required for product-specific operations.
package com.javachinna.service;
import com.javachinna.dto.ProductRequest;
import com.javachinna.model.Product;
public interface ProductService extends Service<Product, ProductRequest> {
}
ProductServiceImpl.java
This service class is responsible for calling the repository methods.
package com.javachinna.service.impl;
import static com.javachinna.config.AppConstants.ID;
import java.util.Optional;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import com.javachinna.dto.ProductRequest;
import com.javachinna.exception.ResourceNotFoundException;
import com.javachinna.model.Product;
import com.javachinna.repo.ProductRepository;
import com.javachinna.service.ProductService;;
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductRepository productRepository;
@Override
public Product save(ProductRequest productRequest) {
Product product = new Product();
BeanUtils.copyProperties(productRequest, product);
return productRepository.save(product);
}
@Override
public void deleteById(Long id) {
productRepository.deleteById(id);
}
@Override
public Optional<Product> findById(Long id) {
return productRepository.findById(id);
}
@Override
public Page<Product> findAll(Pageable pageable) {
return productRepository.findAll(pageable);
}
@Override
public void update(Long id, ProductRequest productRequest) {
findById(id).map(p -> {
BeanUtils.copyProperties(productRequest, p);
return productRepository.save(p);
}).orElseThrow(() -> new ResourceNotFoundException("Product", ID, id));
}
}
Create Product Controller
ProductController.java
This controller exposes the REST API endpoints for CRUD operations.
package com.javachinna.controller;
import static com.javachinna.config.AppConstants.ID;
import javax.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.javachinna.config.AppConstants;
import com.javachinna.dto.ApiResponse;
import com.javachinna.dto.ProductRequest;
import com.javachinna.exception.ResourceNotFoundException;
import com.javachinna.model.Product;
import com.javachinna.service.ProductService;
import lombok.RequiredArgsConstructor;
@RestController
@RequestMapping(path = "/api/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping
public Page<Product> getProductList(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "3") int size) {
Pageable paging = PageRequest.of(page, size);
return productService.findAll(paging);
}
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productService.findById(id).orElseThrow(() -> new ResourceNotFoundException("Product", ID, id));
}
@PostMapping
public ResponseEntity<?> createProduct(@Valid @RequestBody ProductRequest product) {
productService.save(product);
return ResponseEntity.ok().body(new ApiResponse(true, AppConstants.SUCCESS));
}
@PutMapping("/{id}")
public ResponseEntity<?> updateProduct(@PathVariable Long id, @Valid @RequestBody ProductRequest productRequest) {
productService.update(id, productRequest);
return ResponseEntity.ok().body(new ApiResponse(true, AppConstants.SUCCESS));
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteProduct(@PathVariable Long id) {
return productService.findById(id).map(p -> {
productService.deleteById(id);
return ResponseEntity.ok().body(new ApiResponse(true, AppConstants.SUCCESS));
}).orElseThrow(() -> new ResourceNotFoundException("Product", ID, id));
}
}
Run Spring Boot App with Maven
You can run the application with mvn clean spring-boot:run
and the REST API services can be accessed via http://localhost:8080
Run the Angular App
You can run this App with the below command and hit the URL http://localhost:8081/ in browser
ng serve --port 8081
Source Code
https://github.com/JavaChinna/angular-spring-boot-crud
Conclusion
That’s all folks. In this article, we have implemented CRUD operations with our Spring Boot Angular application.
Thank you for reading.