Building an Angular CRUD App with a Spring Boot API
Creating a CRUD (Create, Read, Update, Delete) application is a fundamental aspect of modern web development. In this article, we'll walk you through building a CRUD app with Angular as the front end and a Spring Boot API as the back end, ensuring a smooth integration between the two technologies.
Prerequisites
- Node.js
- JAVA 17
- Maven
- MySQL
Setup Angular project
Install Angular 18 and create a new project with the following command.
npm install -g @angular/cli@18.0.0
ng new view --minimal --routing --style css --no-standalone --ssr=false
Angular project structure
└─ src
├─ app
│ ├─ app-routing.module.ts
│ ├─ app.component.ts
│ ├─ app.interceptor.ts
│ ├─ app.module.ts
│ └─ components
│ └─ product
│ ├─ Create.component.ts
│ ├─ Delete.component.ts
│ ├─ Detail.component.ts
│ ├─ Edit.component.ts
│ ├─ Index.component.ts
│ └─ Product.service.ts
├─ index.html
├─ main.ts
└─ styles.css
*This project structure will display only the files and folders that we plan to create or modify.
Angular Project files
main.ts
import { enableProdMode } from '@angular/core'
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
import { AppModule } from './app/app.module'
platformBrowserDynamic().bootstrapModule(AppModule).catch(e => console.error(e))
This main.ts
file initializes an Angular application by bootstrapping the AppModule
using the platformBrowserDynamic
function. It sets up the application to run in the browser and handles any errors that occur during the bootstrapping process.
app.module.ts
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { FormsModule } from '@angular/forms'
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'
import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { AppInterceptor } from './app.interceptor'
import { ProductIndex } from './components/product/Index.component'
import { ProductCreate } from './components/product/Create.component'
import { ProductDetail } from './components/product/Detail.component'
import { ProductEdit } from './components/product/Edit.component'
import { ProductDelete } from './components/product/Delete.component'
@NgModule({
declarations: [
AppComponent,
ProductIndex,
ProductCreate,
ProductDetail,
ProductEdit,
ProductDelete,
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
HttpClientModule
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: AppInterceptor, multi: true }
],
bootstrap: [AppComponent]
})
export class AppModule { }
The AppModule
is the main module of an Angular application. It imports core Angular modules and sets up routing with AppRoutingModule
. The module declares various product-related components. It also registers AppInterceptor
as an HTTP interceptor. The AppComponent
is set as the bootstrap component, making it the entry point of the application.
app.component.ts
import { Component } from '@angular/core'
@Component({
selector: 'app-root',
template: `<router-outlet></router-outlet>`
})
export class AppComponent { }
The app.component.ts
file defines the root component, AppComponent
, which uses the <router-outlet>
app-root
selector and serves as the entry point for the Angular application.
app.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor } from '@angular/common/http';
import { HttpRequest, HttpErrorResponse } from '@angular/common/http'
import { Observable, throwError } from 'rxjs'
import { HttpHandler } from '@angular/common/http'
import { HttpEvent } from '@angular/common/http'
@Injectable({
providedIn: 'root'
})
export class AppInterceptor implements HttpInterceptor {
baseURL = 'http://localhost:8080/api'
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request.clone({
url: this.baseURL + request.url,
}))
}
}
The AppInterceptor
class is an Angular HTTP interceptor that appends a configurable baseURL
to all outgoing HTTP request URLs before they are sent to the server. This allows the application to centralize and easily manage the base API endpoint.
app-routing.module.ts
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { ProductIndex } from './components/product/Index.component'
import { ProductCreate } from './components/product/Create.component'
import { ProductDetail } from './components/product/Detail.component'
import { ProductEdit } from './components/product/Edit.component'
import { ProductDelete } from './components/product/Delete.component'
const routes: Routes = [
{ path: '', redirectTo: 'product', pathMatch: 'full' },
{ path: 'product', component: ProductIndex },
{ path: 'product/create', component: ProductCreate },
{ path: 'product/:id', component: ProductDetail },
{ path: 'product/edit/:id', component: ProductEdit },
{ path: 'product/delete/:id', component: ProductDelete }
]
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
The AppRoutingModule
sets up routing for an Angular application, including product-related paths for listing, creating, viewing, editing, and deleting products. It also includes a route that redirects from the root path "/" to the product listing page "/product".
Create.component.ts
import { Component } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { ProductService } from './Product.service'
@Component({
selector: 'product-create',
template: `
<div class="container">
<div class="row">
<div class="col">
<form ngNativeValidate method="post" (submit)="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" [(ngModel)]="product.name" maxlength="50" />
<span *ngIf="errors.name" class="text-danger">{{errors.name}}</span>
</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" [(ngModel)]="product.price" type="number" />
<span *ngIf="errors.price" class="text-danger">{{errors.price}}</span>
</div>
<div class="col-12">
<a class="btn btn-secondary" routerLink="/product">Cancel</a>
<button class="btn btn-primary">Submit</button>
</div>
</div>
</form>
</div>
</div>
</div>`
})
export class ProductCreate {
product?: any = {}
errors?: any = {}
constructor(private router: Router, private route: ActivatedRoute, private ProductService: ProductService) { }
create() {
this.ProductService.create(this.product).subscribe(() => {
this.router.navigateByUrl('/product')
}, (e) => {
alert(e.error)
})
}
}
The ProductCreate
component provides a form for creating a new product, binding input fields for name and price to a product
object. On submission, it calls ProductService
to create the product and navigates back to the product list. Validation errors are displayed next to the corresponding fields, and any creation errors trigger an alert.
Delete.component.ts
import { Component } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { ProductService } from './Product.service'
@Component({
selector: 'product-delete',
template: `
<div class="container">
<div class="row">
<div class="col">
<form ngNativeValidate method="post" (submit)="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">
<a class="btn btn-secondary" routerLink="/product">Cancel</a>
<button class="btn btn-danger">Delete</button>
</div>
</div>
</form>
</div>
</div>
</div>`
})
export class ProductDelete {
product?: any = {}
constructor(private router: Router, private route: ActivatedRoute, private ProductService: ProductService) { }
ngOnInit() {
this.get()
}
get() {
return this.ProductService.delete(this.route.snapshot.params['id']).subscribe(data => {
this.product = data
}, e => {
alert(e.error)
})
}
delete() {
this.ProductService.delete(this.route.snapshot.params['id'], this.product).subscribe(() => {
this.router.navigateByUrl('/product')
}, (e) => {
alert(e.error)
})
}
}
The ProductDelete
component in Delete.component.ts
is an Angular component that handles the deletion of a product. It displays a form with read-only fields showing the product's details (ID, name, and price). When the component initializes, it fetches the product details using the product ID from the route. On form submission, the delete()
method calls ProductService
to delete the product and then redirects to the product list. If there's an error during deletion, an alert is shown.
Detail.component.ts
import { Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { ProductService } from './Product.service'
@Component({
selector: 'product-detail',
template: `
<div class="container">
<div class="row">
<div class="col">
<form ngNativeValidate 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">
<a class="btn btn-secondary" routerLink="/product">Back</a>
<a class="btn btn-primary" routerLink="/product/edit/{{product.id}}">Edit</a>
</div>
</div>
</form>
</div>
</div>
</div>`
})
export class ProductDetail {
product?: any = {}
constructor(private route: ActivatedRoute, private ProductService: ProductService) { }
ngOnInit() {
this.get()
}
get() {
return this.ProductService.get(this.route.snapshot.params['id']).subscribe(data => {
this.product = data
}, e => {
alert(e.error)
})
}
}
The ProductDetail
component displays the details of a specific product. It retrieves the product information based on the ID from the route and shows the product's ID, name, and price in read-only fields. The component provides "Back" and "Edit" buttons for navigation. The product details are fetched and displayed when the component initializes.
Edit.component.ts
import { Component } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { ProductService } from './Product.service'
@Component({
selector: 'product-edit',
template: `
<div class="container">
<div class="row">
<div class="col">
<form ngNativeValidate method="post" (submit)="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" [(ngModel)]="product.id" type="number" required />
<span *ngIf="errors.id" class="text-danger">{{errors.id}}</span>
</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" [(ngModel)]="product.name" maxlength="50" />
<span *ngIf="errors.name" class="text-danger">{{errors.name}}</span>
</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" [(ngModel)]="product.price" type="number" />
<span *ngIf="errors.price" class="text-danger">{{errors.price}}</span>
</div>
<div class="col-12">
<a class="btn btn-secondary" routerLink="/product">Cancel</a>
<button class="btn btn-primary">Submit</button>
</div>
</div>
</form>
</div>
</div>
</div>`
})
export class ProductEdit {
product?: any = {}
errors?: any = {}
constructor(private router: Router, private route: ActivatedRoute, private ProductService: ProductService) { }
ngOnInit() {
this.get()
}
get() {
return this.ProductService.edit(this.route.snapshot.params['id']).subscribe(data => {
this.product = data
}, e => {
alert(e.error)
})
}
edit() {
this.ProductService.edit(this.route.snapshot.params['id'], this.product).subscribe(() => {
this.router.navigateByUrl('/product')
}, (e) => {
alert(e.error)
})
}
}
The ProductEdit
component allows users to edit an existing product. It retrieves the product details using the product ID from the route and displays them in a form with editable fields for name and price. On form submission, it updates the product via ProductService
and navigates back to the product list. Any errors during fetching or updating are shown as alerts, and validation errors are displayed next to the relevant fields.
Index.component.ts
import { Component } from '@angular/core'
import { Router, NavigationEnd } from '@angular/router'
import { ProductService } from './Product.service'
@Component({
selector: 'product-index',
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 *ngFor="let product of products">
<td class="text-center">{{product.id}}</td>
<td>{{product.name}}</td>
<td class="text-center">{{product.price}}</td>
<td class="text-center">
<a class="btn btn-secondary" routerLink="/product/{{product.id}}" title="View"><i class="fa fa-eye"></i></a>
<a class="btn btn-primary" routerLink="/product/edit/{{product.id}}" title="Edit"><i class="fa fa-pencil"></i></a>
<a class="btn btn-danger" routerLink="/product/delete/{{product.id}}" title="Delete"><i class="fa fa-times"></i></a>
</td>
</tr>
</tbody>
</table>
<a class="btn btn-primary" routerLink="/product/create">Create</a>
</div>
</div>
</div>`
})
export class ProductIndex {
products?: any[]
constructor(public router: Router, private ProductService: ProductService) { }
ngOnInit() {
this.get()
}
get() {
this.ProductService.get().subscribe(data => {
this.products = data
}, e => {
alert(e.error)
})
}
}
The ProductIndex
component displays a list of products in a table format. It fetches the list of products from ProductService
on initialization and shows each product's ID, name, and price, with action buttons for viewing, editing, and deleting each product. It also includes a button to navigate to the product creation page.
Product.service.ts
import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs'
@Injectable({
providedIn: 'root'
})
export class ProductService {
constructor(private http: HttpClient) { }
get(id?: any): Observable<any> {
if (id) {
return this.http.get(`/products/${id}`)
}
else {
return this.http.get('/products' + location.search)
}
}
create(data?: any): Observable<any> {
if (data) {
return this.http.post('/products', data)
}
else {
return this.http.get('/products/create')
}
}
edit(id: any, data?: any): Observable<any> {
if (data) {
return this.http.put(`/products/${id}`, data)
}
else {
return this.http.get(`/products/${id}`)
}
}
delete(id: any, data?: any): Observable<any> {
if (data) {
return this.http.delete(`/products/${id}`)
}
else {
return this.http.get(`/products/${id}`)
}
}
}
The ProductService
uses Angular's HttpClient
to perform the relevant HTTP requests for product management. It provides methods to:
get(id?)
: Fetch product details by ID or a list of products if no ID is provided.create(data?)
: Create a new product ifdata
is provided, otherwise fetch the create product page.edit(id, data?)
: Update a product by ID ifdata
is provided, otherwise fetch product details for editing.delete(id, data?)
: Delete a product by ID ifdata
is provided, otherwise fetch product details for confirmation.
styles.css
.container {
margin-top: 2em;
}
.btn {
margin-right: 0.25em;
}
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">
</head>
<body>
<app-root></app-root>
</body>
</html>
The HTML serves as the main entry point for an Angular application, including Bootstrap for styling, Font Awesome for icons, and <app-root>
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
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>
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
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);
}
}
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> {
}
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;
}
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);
}
}
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);
}
}
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 Angular project
npm start
Run Spring Boot API project
mvn spring-boot:run
Open the web browser and goto http://localhost:4200
You will find this product list page.
Testing
Click the "View" button to see the product details page.
Click the "Edit" button to modify the product and update its details.
Click the "Submit" button to save the updated product details.
Click the "Create" button to add a new product and input its details.
Click the "Submit" button to save the new product.
Click the "Delete" button to remove the previously created product.
Click the "Delete" button to confirm the removal of this product.
Conclusion
In conclusion, we have learned how to create a basic Angular project with components, views, and routing, while setting up a Spring Boot API as the backend. By leveraging Spring Data JPA for database operations, we've successfully built a dynamic front-end that integrates seamlessly with a powerful backend. This combination offers a solid foundation for developing modern, full-stack web applications.
Source code: https://github.com/stackpuz/Example-CRUD-Angular-18-Spring-Boot-3
Create an Angular CRUD App in Minutes: https://stackpuz.com