Angular CRUD App with a Laravel API

Building an Angular CRUD App with a Laravel API

Mastering CRUD operations is essential for developing robust web applications. This article will guide you step by step in building a CRUD app using Angular for the client side and a Laravel API for the server side, demonstrating the powerful synergy between these technologies.

Prerequisites

  • Node.js
  • Composer
  • PHP 8.2
  • 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> directive in its template to display routed views. The component is identified by the 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:8000/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 if data is provided, otherwise fetch the create product page.
  • edit(id, data?): Update a product by ID if data is provided, otherwise fetch product details for editing.
  • delete(id, data?): Delete a product by ID if data 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> as the placeholder for the Angular app.

Setup Laravel API project

composer create-project laravel/laravel api 11.0.3

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

Laravel API Project structure

├─ .env
├─ app
│  ├─ Http
│  │  └─ Controllers
│  │     └─ ProductController.php
│  └─ Models
│     └─ Product.php
├─ bootstrap
│  └─ app.php
└─ routes
   └─ api.php

*This project structure will display only the files and folders that we plan to create or modify.

Laravel API Project files

.env

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=example
DB_USERNAME=root
DB_PASSWORD=

This file is used for Laravel configuration, specifically to store the database connection details.

app.php

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

This file is a Laravel application configuration file where we have included the API routing setup.

api.php

<?php

use App\Http\Controllers\ProductController;

Route::resource('/products', ProductController::class);

This api.php file sets up resourceful API routes for a Laravel application. It uses Route::resource to automatically create routes for common CRUD operations (index, show, store, update, destroy) for the /products endpoint, all mapped to the corresponding methods in ProductController.

Product.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $table = 'Product';
    protected $primaryKey = 'id';
    protected $fillable = [ 'name', 'price' ];
    public $timestamps = false;
}

This product.php file defines a Product model in a Laravel application. It specifies that the model uses the Product table, sets `id` as the primary key, and allows mass assignment for the `name` and `price` fields.

ProductController.php

<?php

namespace App\Http\Controllers;

use App\Models\Product;

class ProductController {

    public function index()
    {
        return Product::get();
    }

    public function show($id)
    {
        return Product::find($id);
    }

    public function store()
    {
        return Product::create([
            'name' => request()->input('name'),
            'price' => request()->input('price')
        ]);
    }

    public function update($id)
    {
        return tap(Product::find($id))->update([
            'name' => request()->input('name'),
            'price' => request()->input('price')
        ]);
    }

    public function destroy($id)
    {
        Product::find($id)->delete();
    }
}

The ProductController.php file in a Laravel application defines methods for managing Product resources:

  • index Retrieves and returns all products.
  • show Fetches and returns a single product by its ID.
  • store Creates a new product using the data from the request.
  • update Updates an existing product with the data from the request.
  • destroy Deletes a product by its ID.

Run projects

Run Angular project

npm start

Run Laravel API project

php artisan serve

Open the web browser and goto http://localhost:4200
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 Angular project with components, views, and routing, while integrating it with a Laravel API as the backend. By utilizing Eloquent as the ORM for database operations, we've built a responsive front-end that effectively communicates with a robust backend. This approach provides a solid foundation for developing modern, full-stack web applications.

Source code: https://github.com/stackpuz/Example-CRUD-Angular-18-Laravel-11

Create an Angular CRUD App in Minutes: https://stackpuz.com

Related post