How to Build a CRUD application with Angular and Spring Boot in 15 Steps

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

Product Create Page

List Page

Product List Page

View Page

View Page

Edit Page

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

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 the productService.getAll() that returns an Observable object. The response will hold the products in the content 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 the deleteProduct() method. This method calls the productService.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 the product id in edit mode.
    • A form for adding/editing product details
    • An errorMessage field to hold the error message if any
  • The ngOnInit() method gets the id from the URL if exists and calls the productService.find() that returns an Observable object. Then, it calls the this.form.patchValue(x) method to populate the form with the values from the returned object.
  • The onSubmit() method calls the productService.update() method If it is in edit mode. Else it calls the productService.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 the product id from the path.
    • A product field of type Product to hold the product object fetched from the backend
  • The ngOnInit() method gets the id from the URL if exists and calls the productService.find() that returns an Observable object. Then, the response is assigned to the product 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.

Leave a Reply