HOW TO BUILD A SERVERLESS WEBSHOP
How To Create CRUD For Managing Webshop Products
In this part of the series “How To Build A Serverless Webshop” we are going to explore how to build CRUD endpoints for our products with FaunaDB and Serverless functions.
Series
- Connect Angular and FaunaDB with Netlify Serverless functions
- Product list and detail view with Angular
- Create crud for products and show products on an Angular page
We will make it possible to create new products, update existing products, and even delete them.
In the next part of the series, we will make sure that not everyone can add, update, or delete our products with authentication.
If you are ready to get your hands dirty with the FaunaDB API, please follow along.
Happy coding! 🚀
1. Product admin
To manage our products we need to have a product admin page.
ng generate component products/components/product-admin
To access this page we need to create a route to access all the product data.
import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
import { ProductListComponent } from './products/components/product-list/product-list.component'
import { ProductItemComponent } from './products/components/product-item/product-item.component'
import { ProductAdminComponent } from './products/components/product-admin/product-admin.component'const routes: Routes = [
{
path: '',
component: ProductListComponent,
},
{
path: 'product/:id',
component: ProductItemComponent,
},
{
path: 'admin',
component: ProductAdminComponent,
},
]@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
In the app.component.html
we add a button to navigate to the admin page.
<div class="toolbar" role="banner">
<h1 class="name">FaunaDB Webshop</h1>
<nav>
<button [routerLink]="['/']" mat-flat-button>Home</button>
<button [routerLink]="['/admin']" mat-flat-button>Admin</button>
</nav>
</div><div class="content" role="main">
<router-outlet></router-outlet>
</div>
2. Making Forms Easier
Making forms in Angular or any other web app is a time consuming job. To make it much easier to create and maintain the forms I will use NGX-Formly.
If you want to check out a more detailed tutorial about all the details in NGX-Formly, check my first and second part here.
Adding Formly can be done via the Angular CLI. In this case I’ve added the Material plugin for Formly in the command below. You can replace material with bootstrap or anything that they offer.
ng add @ngx-formly/schematics --ui-theme=material
Now the Angular CLI has added the Formly module to the app.module.ts. But we also need to add the Material modules for using the Material input components in our forms.
import { BrowserModule } from '@angular/platform-browser'
import { NgModule } from '@angular/core'import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'import { MatButtonModule } from '@angular/material/button'import { HttpClientModule } from '@angular/common/http'
import { ProductListComponent } from './products/components/product-list/product-list.component'
import { ProductItemComponent } from './products/components/product-item/product-item.component'
import { ReactiveFormsModule } from '@angular/forms'
import { FormlyModule } from '@ngx-formly/core'
import { FormlyMaterialModule } from '@ngx-formly/material'import { FormlyMatDatepickerModule } from '@ngx-formly/material/datepicker'
import { FormlyMatToggleModule } from '@ngx-formly/material/toggle'
import { MatDatepickerModule } from '@angular/material/datepicker'
import { MatDialogModule } from '@angular/material/dialog'
import { MatFormFieldModule } from '@angular/material/form-field'
import { MatInputModule } from '@angular/material/input'
import { MatRadioModule } from '@angular/material/radio'
import { MatSelectModule } from '@angular/material/select'
import { MatCheckboxModule } from '@angular/material/checkbox'
import { MatNativeDateModule } from '@angular/material/core'
import { ProductAdminComponent } from './products/components/product-admin/product-admin.component'@NgModule({
declarations: [
AppComponent,
ProductListComponent,
ProductItemComponent,
ProductItemComponent,
ProductAdminComponent,
],
imports: [
BrowserModule,
HttpClientModule,
AppRoutingModule,
BrowserAnimationsModule,
MatButtonModule,
ReactiveFormsModule,
FormlyModule.forRoot(),
FormlyMaterialModule,
ReactiveFormsModule,
MatCheckboxModule,
MatDatepickerModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule, MatRadioModule,
MatSelectModule, MatNativeDateModule,
FormlyMatDatepickerModule,
FormlyMatToggleModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Let’s build our first form.
3. Product overview
Like with most admin pages, we want to show a list of all the products we have. For every product we want to add product actions buttons like edit and remove.
We will use the material table for that. For this we need to import the MatTableModule in the app.module.ts
.
//... all the other imported modules
import { MatTableModule } from '@angular/material/table'@NgModule({
declarations: [//...],
imports: [
//...
MatTableModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Now we can add the table to our product-item component and get the data from our serverless function with the ProductService
in Angular.
import { Component, OnInit } from ' @angular/core'
import { ProductData } from '../../models/product'
import { ProductService } from '../../service/product.service'@Component({
selector: 'app-product-admin',
templateUrl: './ product-admin.component.html',
styleUrls: ['./product-admin.component.scss'],
})
export class ProductAdminComponent implements OnInit {
public products: ProductData[] = []
public displayedColumns: string[] = ['id', 'name', 'price', 'actions']
public dataSource = null constructor(private productService: ProductService) {} ngOnInit(): void {
console.log('dataSource: ', this.dataSource)
this.productService.getProducts().then((products: ProductData[]) => {
console.log(products) this.products = products
this.dataSource = products
console.log('dataSource: ', this.dataSource)
})
}
}
In the product-admin.component.html
we add the table to show all the data in the right columns.
<header class="admin__header">
<h1>Products admin</h1>
<button mat-flat-button color="primary">New product</button>
</header><mat-table [dataSource]="dataSource">
<!-- ID Column --> <ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef> ID </mat-header-cell>
<mat-cell *matCellDef="let element"> {{ element.id }} </mat-cell>
</ng-container> <!-- Name Column --> <ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
<mat-cell *matCellDef="let element"> {{ element.name }} </mat-cell>
</ng-container> <!-- Price Column --> <ng-container matColumnDef="price">
<mat-header-cell *matHeaderCellDef> Price </mat-header-cell>
<mat-cell *matCellDef="let element"> {{ element.price }} </mat-cell>
</ng-container> <ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef>Action</mat-header-cell>
<mat-cell *matCellDef="let element">
<button [routerLink]="['/admin/product/', element.id]" mat-flat-button color="primary">Edit</button>
</mat-cell>
</ng-container> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *_matRowDef="let row; columns:displayedColumns"></mat-row>
</mat-table>
We can add some CSS to improve the styling.
:host {
width: 100%;
}.admin {
&__header {
margin-bottom: 1rem;
}
}
3. Create a product
We need a view that will display a form to create or update a product. So let’s generate a component for that and add it to the routing module.
ng generate component products/components/product-form
In the routing module we add a few routes.
import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
import { ProductListComponent } from './products/components/product-list/product-list.component'
import { ProductItemComponent } from './products/components/product-item/product-item.component'
import { ProductAdminComponent } from './products/components/product-admin/product-admin.component'
import { ProductFormComponent } from './products/components/product-form/product-form.component'const routes: Routes = [
{
path: '',
component: ProductListComponent,
},
{
path: 'product/:id',
component: ProductItemComponent,
},
{
path: 'admin',
component: ProductAdminComponent,
},
{
path: 'admin/product/:id',
component: ProductFormComponent,
},
{
path: '**',
redirectTo: '',
},
]@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
If you watch the admin page and click the edit button you should get a URL like this “http://localhost:4200/admin/product/266790280843231752”but there is no form yet. So let’s build that form to show the product information in it.
To get the product ID from the URL we need the ActivatedRoute module in our constructor from the ProductFormComponent. In the ngOnInit we need that product ID to get all the data from our product. But in our case we use this component also for
showing a form when creating a new product.
import { Component, OnInit } from '@angular/core'
import { ProductData } from '../../models/product'
import { ProductService } from '../../service/product.service'
import { ActivatedRoute } from '@angular/router'
import { FormGroup } from '@angular/forms'
import { FormlyFieldConfig } from '@ngx-formly/core'@Component({
selector: 'app-product-form',
templateUrl: './product-form.component.html',
styleUrls: ['./product-form.component.scss'],
})
export class ProductFormComponent implements OnInit {
public id: string = ''
public productItem: ProductData = null public productProps: string[] = [] public form = new FormGroup({})
public model = {}
public fields: FormlyFieldConfig[] = [
{
key: 'name',
type: 'input',
templateOptions: {
label: 'Name',
placeholder: 'Enter name',
required: true,
},
},
{
key: 'description',
type: 'input',
templateOptions: {
type: 'text', label: 'Description',
placeholder: 'Enter description',
required: true,
},
},
{
key: 'price',
type: 'input',
templateOptions: {
type: 'number',
label: 'Price',
placeholder: 'Enter price',
required: true,
},
},
{
key: 'quantity',
type: 'input',
templateOptions: {
typpe: 'number',
label: 'Quantity',
placeholder: 'Enter quantity',
required: true,
},
},
{
key: 'backorderLimit',
type: 'input', templateOptions: {
typpe: 'number',
label: 'Backorder limit',
placeholder: 'Enter backorderLimit',
required: true,
},
},
{
key: 'backordered',
type: 'checkbox',
templateOptions: {
label: 'Backordered',
placeholder: 'Enter backordered',
required: true,
},
},
] constructor(private product: ProductService, private route: ActivatedRoute) {
this.route.params.subscribe((params) => {
this.id = params?.id
})
} public ngOnInit(): void {
this.getProduct()
} private getProduct() {
if (this.id !== 'new') {
this.product.getProductById(this.id).then((product) => {
this.productItem = product
})
} else {
this.productItem = new ProductData()
}
} public onSubmit(data) {
console.log(data)
}
}
For the forms we use NGX-formly like we installed a few steps back. Now we need to create a FormGroup and a fields array where we configure all the fields that we want in our form.
The great thing about NGX-formly is that we only have to add a <form>
and <formly>
elements. In the element we add fields and the model. The fields will automatically be created by formly. The model is used to show the data for an existing
product.
<div class="form__wrapper">
<form [formGroup]="form" (ngSubmit)="onSubmit(productItem)">
<formly-form [form]="form" [fields]="fields" [model]="productItem"></formly-form>
<button mat-flat-button color="primary" type="submit" class="btn btn-default">Submit</button>
</form>
</div>
The result should look something like this. But I can imagine that you want to change the styling to make it more pleasing to the eyes of user.
Now that we have prepared the frontend with the edit view, we need to create a serverless function that will save the data for both a new and existing product.
In the product-service.js
I add a new method to post the data for a new product.
createNewProduct(product) {
return new Promise((resolve, reject) => {
if (!product) {
reject('No product data provided')
} this.client
.query(
q.Create(q.Collection('products'), {
data: product,
}),
)
.then((result) => {
resolve(result)
})
.catch((error) => { console.log('createNewProduct', error) reject(error)
})
})
}
For the serverless function I create a new file product-new.js which will result in a new endpoint /product-new
.
import { ProductService } from '../lib/product-service.js'
import { client, headers } from '../lib/config.js'const service = new ProductService({ client })exports.handler = async (event, context) => {
console.log('Function `product-new` invoked') const { body } = event if (event.httpMethod === 'OPTIONS') {
return { statusCode: 200, headers, body: 'Ok' }
} const parsedBody = JSON.parse(body)
if (!parsedBody) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
message: 'Some product data is missing', parsedBody }),
}
} if (event.httpMethod !== 'POST') {
return {
statusCode: 405, headers, body: 'Method Not Allowed' }
} try {
const product = await
service.createNewProduct(parsedBody)
return { statusCode: 200,
headers,
body: JSON.stringify(product),
}
} catch (error) {
console.log('error', error) return {
statusCode: 400,
headers,
body: JSON.stringify(error),
}
}
}
}
In this function I check if there is product data in the body and if that body has data. Otherwise, it will return an error. To test if it accepts my data I test it locally via Insomnia (Postman is also a great tool to test your API).
When you send a POST request from Anguar it will first send an OPTIONS request. For now we accept all of them, but you should make this secure.
This is the data I used to test the endpoint:
{
"name": "iPhone 12",
"description": "The newest iPhone",
"price": 1299,
"quantity": 2000,
"backorderLimit": 10,
"backordered": false,
"image": "https://images.unsplash.com/photo-1577937927133-66ef06acdf18?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=0&q=80"
}
Now that we see the API endpoint works we can connect it in our Angular application. We replace the onSubmit method in the product-form component.
In the Angular product-service we add a method for sending the request to the serverless function.
//.... ProductService
createNewProduct(product) {
return new Promise((resolve, reject) => {
if (!product) {
reject('No product data provided')
} this.client
.query(
q.Create(q.Collection('products'), {
data: product,
}),
)
.then((result) => {
resolve(result)
})
.catch((error) => {
console.log('createNewProduct', error) reject(error)
})
})
}
//...//.... ProductFormComponentpublic async onSubmit(data) {
console.log(data)
const newProduct = await
this.product.createNewProduct(data)
if (newProduct) {
this.router.navigate(['/admin'])
}
}
//....
When you check your browser, fill in the form, click on the submit button you should be able to create a new product. After the creation is done you will be redirected to the
admin page.
Check the Github repo for the complete code.
4. Update a product
Now that we can create a product, we also want to update some of its information. Let’s create a product update serverless function. Be aware that you only have to send the changed fields from a product instead of sending all of them.
In the product service for the serverless function we create the update method. To check which fields are changed compared to the existing product I created a method to filter out the unchanged fields.
import faunadb from 'faunadb'
const q = faunadb.queryexport class ProductService {
// Code from previous steps ....
async updateProduct(productId, product) {
const filterdProduct = await this.filterUnChangedKeys(product) return new Promise((resolve, reject) => {
if (!product || !filterdProduct) {
reject('No product data provided')
} this.client
.query(q.Update(q.Ref(q.Collection('products'), productId), { data: filterdProduct }))
.then((result) => {
resolve(result)
})
.catch((error) => {
console.log('updateProduct', error) reject(error)
})
})
} async filterUnChangedKeys(product) {
const originalProduct = await this.getProductById(product.id)
return new Promise((resolve, reject) => {
if (!originalProduct) {
reject(originalProduct)
}
const tempProduct = {}
for (const key in product) {
const value = product[key]
if (value !== originalProduct.data[key] && key !== 'id' && key !== 'storehouse') {
tempProduct[key] = value
}
}
resolve(tempProduct)
})
}
}
In the directory functions/product-update.js we create the serverless function where we call the service.
import { ProductService } from '../lib/product-service.js'
import { client, headers } from '../lib/config.js'const service = new ProductService({ client })exports.handler = async (event, context) => {
console.log('Function `product-update` invoked') const { body, path } = event
const productId = path.substr(path.lastIndexOf('/') + 1) if (event.httpMethod === 'OPTIONS') {
return { statusCode: 200, headers, body: 'Ok' }
} const parsedBody = JSON.parse(body) if (!parsedBody) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
message: 'Some product data is missing',
parsedBody,
}),
}
} if (event.httpMethod !== 'PUT') {
return {
statusCode: 405,
headers,
body: 'Method Not Allowed',
}
} try {
let product = null
if (event.httpMethod === 'PUT' && productId) {
product = await service.updateProduct(productId, parsedBody)
}
return {
statusCode: 200,
headers,
body: JSON.stringify(product),
}
} catch (error) {
console.log('error', error) return {
statusCode: 400,
headers,
body: JSON.stringify(error),
}
}
}
Now we can use the same form in the frontend to change product information. We made the product form smart with NGX-Formly to show the values when available. In the submit method we now have to choose if it’s a new product or an existing product (product-form.component.ts
).
public async onSubmit(data) {
let product = this.id === 'new' ? await
this.product.createNewProduct(data) : await
this.product.updateProduct(this.id, data)
if (product) {
this.router.navigate(['/admin'])
}
}
If you test to update one of your products it should work.
Check the Github repo for the complete code.
4. Delete a product
Of course we want to delete a product as well. Let’s create a serverless function for deleting a product. In the service for the serverless functions we add a method to call the FaunaDB API to delete the product.
async deleteProduct(productId) {
return new Promise((resolve, reject) => { if (!productId) {
reject('No product ID provided')
} this.client
.query(q.Delete(q.Ref(q.Collection('products'),
productId)))
.then((result) => {
resolve('OK')
})
.catch((error) => {
console.log('deleteProduct', error) reject(error)
})
})
}
The serverless function functions/product-delete.js will look like this.
import { ProductService } from '../lib/product-service.js'
import { client, headers } from '../lib/config.js'const service = new ProductService({ client })exports.handler = async (event, context) => {
console.log('Function `product-delete` invoked') const { path } = event
const productId = path.substr(path.lastIndexOf('/') + 1) if (event.httpMethod === 'OPTIONS') {
return { statusCode: 200, headers, body: 'Ok' }
} if (event.httpMethod !== 'DELETE') {
return {
statusCode: 405,
headers,
body: 'Method Not Allowed',
}
} try {
let product = null
if (event.httpMethod === 'DELETE' && productId) {
product = await service.deleteProduct(productId)
} return {
statusCode: 200,
headers,
body: JSON.stringify(product),
}
} catch (error) {
console.log('error', error) return {
statusCode: 400,
headers,
body: JSON.stringify(error),
}
}
}
If you call the serverless function via Postman or Insomnia with a DELETE method the response body should be OK with this url: “http://localhost:9000/.netlify/functions/product-delete/PRODUCT_ID"
Now we can add the delete functionality in the admin page. The edit button we added before is going to be changed. I think adding an icon is a bit more clear for the user-experience.
Add the MatIcon module to the app.module.ts
to use it.
import { BrowserModule } from '@angular/platform-browser'
import { NgModule } from '@angular/core'import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { MatButtonModule } from '@angular/material/button'import { HttpClientModule } from '@angular/common/http'
import { ProductListComponent } from './products/components/product-list/product-list.component'
import { ProductItemComponent } from './products/components/product-item/product-item.component'
import { ReactiveFormsModule } from '@angular/forms'
import { FormlyModule } from '@ngx-formly/core'
import { FormlyMaterialModule } from '@ngx-formly/material'import { FormlyMatDatepickerModule } from '@ngx-formly/material/datepicker'
import { FormlyMatToggleModule } from '@ngx-formly/material/toggle'
import { MatDatepickerModule } from '@angular/material/datepicker'
import { MatDialogModule } from '@angular/material/dialog'
import { MatFormFieldModule } from '@angular/material/form-field'
import { MatInputModule } from '@angular/material/input'
import { MatRadioModule } from '@angular/material/radio'
import { MatSelectModule } from '@angular/material/select'
import { MatCheckboxModule } from '@angular/material/checkbox'
import { MatNativeDateModule } from '@angular/material/core'
import { MatTableModule } from '@angular/material/table'
// MatIconModule import
import { MatIconModule } from '@angular/material/icon'import { ProductAdminComponent } from './products/components/product-admin/product-admin.component'
import { ProductFormComponent } from './products/components/product-form/product-form.component'@NgModule({
declarations: [
AppComponent,
ProductListComponent,
ProductItemComponent,
ProductItemComponent,
ProductAdminComponent,
ProductFormComponent,
],
imports: [
BrowserModule,
HttpClientModule,
AppRoutingModule,
BrowserAnimationsModule,
MatButtonModule,
ReactiveFormsModule,
FormlyModule.forRoot(),
FormlyMaterialModule,
ReactiveFormsModule,
MatCheckboxModule,
MatDatepickerModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatRadioModule,
MatSelectModule,
MatTableModule,
// MatIconModule import
MatIconModule, MatNativeDateModule,
FormlyMatDatepickerModule,
FormlyMatToggleModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
In the product-admin.component.html
we can change the edit button and add a new one for deleting products.
<header class="admin__header">
<h1>Products admin</h1>
<button [routerLink]="['/admin/product/new']" mat-flat-button color="secondary">New product</button>
</header><mat-table [dataSource]="dataSource">
<!-- ID Column --> <ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef> ID </mat-header-cell>
<mat-cell *matCellDef="let element"> {{ element.id }} </mat-cell>
</ng-container> <!-- Name Column --> <ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
<mat-cell *matCellDef="let element"> {{ element.name }} </mat-cell>
</ng-container> <!-- Price Column --> <ng-container matColumnDef="price">
<mat-header-cell *matHeaderCellDef> Price </mat-header-cell>
<mat-cell *matCellDef="let element"> {{ element.price }} </mat-cell>
</ng-container> <ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef>Action</mat-header-cell>
<mat-cell *matCellDef="let element">
<button
[routerLink]="['/admin/product/', element.id]"
mat-icon-button
color="primary"
aria-label="Edit product"
>
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="error" aria-label="Delete product">
<mat-icon>delete</mat-icon>
</button>
</mat-cell>
</ng-container> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
If you get all kinds of error’s in the frontend, please restart the Angular development server, it might not have imported the MatIconModule correctly.
The result in the browser should look something like this.
In the product.service.ts
we define a method that calls the delete serverless function.
public async deleteProduct(productId: string) {
if (!productId) return let product = null try {
product = await this.http.delete<Product>(environment.apiUrl + 'product-delete/' + productId).toPromise()
} catch (error) {
console.error('error: ', error)
return error }
return product
}
Now we can call this method in our product-admin.component.ts so we can call it via the
the click of a delete button. Since we want to get all new data after we delete a product, we have to create a method which does all of this. So we can re-use it in the ngOnInit()
and deleteProduct()
methods.
import { Component, OnInit } from '@angular/core'
import { ProductData } from '../../models/product'
import { ProductService } from '../../service/product.service'
import { Router } from '@angular/router'@Component({
selector: 'app-product-admin',
templateUrl: './product-admin.component.html',
styleUrls: ['./product-admin.component.scss'],
})
export class ProductAdminComponent implements OnInit {
public products: ProductData[] = []
public displayedColumns: string[] = ['id', 'name', 'price', 'actions']
public dataSource = null constructor(private productService: ProductService, private router: Router) {} ngOnInit(): void {
console.log('dataSource: ', this.dataSource)
this.getProductData()
} deleteProduct(productId: string): void {
this.productService
.deleteProduct(productId)
.then((result) => {
this.getProductData()
})
.catch((error) => {
console.log(error)
})
} getProductData(): void {
this.productService.getProducts().then((products: ProductData[]) => {
console.log(products)
this.products = products
this.dataSource = products
})
}
}
In the product-admin.component.html
we add a click handler to the delete button.
<header class="admin__header">
<h1>Products admin</h1>
<button [routerLink]="['/admin/product/new']" mat-flat-button color="secondary">New product</button>
</header><mat-table [dataSource]="dataSource">
<!-- ID Column --> <ng-container matColumnDef="id">
<mat-header-cell *matHeaderCellDef> ID </mat-header-cell>
<mat-cell *matCellDef="let element"> {{ element.id }} </mat-cell>
</ng-container> <!-- Name Column --> <ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
<mat-cell *matCellDef="let element"> {{ element.name }} </mat-cell>
</ng-container> <!-- Price Column --> <ng-container matColumnDef="price">
<mat-header-cell *matHeaderCellDef> Price </mat-header-cell>
<mat-cell *matCellDef="let element"> {{ element.price }} </mat-cell>
</ng-container> <ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef>Action</mat-header-cell>
<mat-cell *matCellDef="let element">
<button
[routerLink]="['/admin/product/', element.id]"
mat-icon-button
color="primary"
aria-label="Edit product"
>
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="error" aria-label="Delete product" (click)="deleteProduct(element.id)">
<mat-icon>delete</mat-icon>
</button>
</mat-cell>
</ng-container> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
Test it in the browser! In my experience the combination of the easy FaunaDB API and
the Netlify serverless functions are working as fast as a rocket .
Check the Github repo for the complete code
5. Security
Be aware that I didn’t implement a security layer yet. For that reason I won’t deploy this version to my test environment. In the next step we are gonna build our user authentication.
In the meantime play with everything until my next step will be published.
I think you should be very proud of the functionality to create, edit and delete a product. By now I think you would agree with me that Serverless Functions talking to the FaunaDB database isn’t all that difficult.
Happy Coding 🚀