Posted on • Originally published atblog.stackpuz.com on
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.0ng 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))
Thismain.ts
file initializes an Angular application by bootstrapping theAppModule
using theplatformBrowserDynamic
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]})exportclassAppModule{}
TheAppModule
is the main module of an Angular application. It imports core Angular modules and sets up routing withAppRoutingModule
. The module declares various product-related components. It also registersAppInterceptor
as an HTTP interceptor. TheAppComponent
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>`})exportclassAppComponent{}
Theapp.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 theapp-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'})exportclassAppInterceptorimplementsHttpInterceptor{baseURL='http://localhost:8000/api'intercept(request:HttpRequest<any>,next:HttpHandler):Observable<HttpEvent<any>>{returnnext.handle(request.clone({url:this.baseURL+request.url,}))}}
TheAppInterceptor
class is an Angular HTTP interceptor that appends a configurablebaseURL
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'constroutes: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]})exportclassAppRoutingModule{}
TheAppRoutingModule
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> <div> <div> <form ngNativeValidate method="post" (submit)="create()"> <div> <div> <label for="product_name">Name</label> <input name="name" [(ngModel)]="product.name" maxlength="50" /> <span *ngIf="errors.name">{{errors.name}}</span> </div> <div> <label for="product_price">Price</label> <input name="price" [(ngModel)]="product.price" type="number" /> <span *ngIf="errors.price">{{errors.price}}</span> </div> <div> <a routerLink="/product">Cancel</a> <button>Submit</button> </div> </div> </form> </div> </div> </div>`})exportclassProductCreate{product?:any={}errors?:any={}constructor(privaterouter:Router,privateroute:ActivatedRoute,privateProductService:ProductService){}create(){this.ProductService.create(this.product).subscribe(()=>{this.router.navigateByUrl('/product')},(e)=>{alert(e.error)})}}
TheProductCreate
component provides a form for creating a new product, binding input fields for name and price to aproduct
object. On submission, it callsProductService
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> <div> <div> <form ngNativeValidate method="post" (submit)="this.delete()"> <div> <div> <label for="product_id">Id</label> <input readonly name="id" value="{{product.id}}" type="number" required /> </div> <div> <label for="product_name">Name</label> <input readonly name="name" value="{{product.name}}" maxlength="50" /> </div> <div> <label for="product_price">Price</label> <input readonly name="price" value="{{product.price}}" type="number" /> </div> <div> <a routerLink="/product">Cancel</a> <button>Delete</button> </div> </div> </form> </div> </div> </div>`})exportclassProductDelete{product?:any={}constructor(privaterouter:Router,privateroute:ActivatedRoute,privateProductService:ProductService){}ngOnInit(){this.get()}get(){returnthis.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)})}}
TheProductDelete
component inDelete.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, thedelete()
method callsProductService
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> <div> <div> <form ngNativeValidate method="post"> <div> <div> <label for="product_id">Id</label> <input readonly name="id" value="{{product.id}}" type="number" required /> </div> <div> <label for="product_name">Name</label> <input readonly name="name" value="{{product.name}}" maxlength="50" /> </div> <div> <label for="product_price">Price</label> <input readonly name="price" value="{{product.price}}" type="number" /> </div> <div> <a routerLink="/product">Back</a> <a routerLink="/product/edit/{{product.id}}">Edit</a> </div> </div> </form> </div> </div> </div>`})exportclassProductDetail{product?:any={}constructor(privateroute:ActivatedRoute,privateProductService:ProductService){}ngOnInit(){this.get()}get(){returnthis.ProductService.get(this.route.snapshot.params['id']).subscribe(data=>{this.product=data},e=>{alert(e.error)})}}
TheProductDetail
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> <div> <div> <form ngNativeValidate method="post" (submit)="edit()"> <div> <div> <label for="product_id">Id</label> <input readonly name="id" [(ngModel)]="product.id" type="number" required /> <span *ngIf="errors.id">{{errors.id}}</span> </div> <div> <label for="product_name">Name</label> <input name="name" [(ngModel)]="product.name" maxlength="50" /> <span *ngIf="errors.name">{{errors.name}}</span> </div> <div> <label for="product_price">Price</label> <input name="price" [(ngModel)]="product.price" type="number" /> <span *ngIf="errors.price">{{errors.price}}</span> </div> <div> <a routerLink="/product">Cancel</a> <button>Submit</button> </div> </div> </form> </div> </div> </div>`})exportclassProductEdit{product?:any={}errors?:any={}constructor(privaterouter:Router,privateroute:ActivatedRoute,privateProductService:ProductService){}ngOnInit(){this.get()}get(){returnthis.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)})}}
TheProductEdit
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 viaProductService
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> <div> <div> <table> <thead> <tr> <th>Id</th> <th>Name</th> <th>Price</th> <th>Actions</th> </tr> </thead> <tbody> <tr *ngFor="let product of products"> <td>{{product.id}}</td> <td>{{product.name}}</td> <td>{{product.price}}</td> <td> <a routerLink="/product/{{product.id}}" title="View"><i></i></a> <a routerLink="/product/edit/{{product.id}}" title="Edit"><i></i></a> <a routerLink="/product/delete/{{product.id}}" title="Delete"><i></i></a> </td> </tr> </tbody> </table> <a routerLink="/product/create">Create</a> </div> </div> </div>`})exportclassProductIndex{products?:any[]constructor(publicrouter:Router,privateProductService:ProductService){}ngOnInit(){this.get()}get(){this.ProductService.get().subscribe(data=>{this.products=data},e=>{alert(e.error)})}}
TheProductIndex
component displays a list of products in a table format. It fetches the list of products fromProductService
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'})exportclassProductService{constructor(privatehttp:HttpClient){}get(id?:any):Observable<any>{if(id){returnthis.http.get(`/products/${id}`)}else{returnthis.http.get('/products'+location.search)}}create(data?:any):Observable<any>{if(data){returnthis.http.post('/products',data)}else{returnthis.http.get('/products/create')}}edit(id:any,data?:any):Observable<any>{if(data){returnthis.http.put(`/products/${id}`,data)}else{returnthis.http.get(`/products/${id}`)}}delete(id:any,data?:any):Observable<any>{if(data){returnthis.http.delete(`/products/${id}`)}else{returnthis.http.get(`/products/${id}`)}}}
TheProductService
uses Angular'sHttpClient
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><htmllang="en"><head><metacharset="utf-8"><metaname="viewport"content="width=device-width,initial-scale=1"><linkhref="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css"rel="stylesheet"><linkhref="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 thedatabase.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=mysqlDB_HOST=localhostDB_PORT=3306DB_DATABASE=exampleDB_USERNAME=rootDB_PASSWORD=
This file is used for Laravel configuration, specifically to store the database connection details.
app.php
<?phpuseIlluminate\Foundation\Application;useIlluminate\Foundation\Configuration\Exceptions;useIlluminate\Foundation\Configuration\Middleware;returnApplication::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
<?phpuseApp\Http\Controllers\ProductController;Route::resource('/products',ProductController::class);
Thisapi.php
file sets up resourceful API routes for a Laravel application. It usesRoute::resource
to automatically create routes for common CRUD operations (index, show, store, update, destroy
) for the/products
endpoint, all mapped to the corresponding methods inProductController
.
Product.php
<?phpnamespaceApp\Models;useIlluminate\Database\Eloquent\Model;classProductextendsModel{protected$table='Product';protected$primaryKey='id';protected$fillable=['name','price'];public$timestamps=false;}
Thisproduct.php
file defines aProduct
model in a Laravel application. It specifies that the model uses theProduct
table, setsid
as the primary key, and allows mass assignment for thename
andprice
fields.
ProductController.php
<?phpnamespaceApp\Http\Controllers;useApp\Models\Product;classProductController{publicfunctionindex(){returnProduct::get();}publicfunctionshow($id){returnProduct::find($id);}publicfunctionstore(){returnProduct::create(['name'=>request()->input('name'),'price'=>request()->input('price')]);}publicfunctionupdate($id){returntap(Product::find($id))->update(['name'=>request()->input('name'),'price'=>request()->input('price')]);}publicfunctiondestroy($id){Product::find($id)->delete();}}
TheProductController.php
file in a Laravel application defines methods for managingProduct
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 gotohttp://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 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
Top comments(1)

- LocationFrance
- EducationEngineering degree
- PronounsHe / Him
- WorkFull Stack Software Engineer
- Joined
Thanks for sharing! Note that Angular now focuses on using signals and standalone components, reducing the usage of modules and observables.
For further actions, you may consider blocking this person and/orreporting abuse