Build a web app using Nestjs, Fastify, MongoDB and Angular 8

by Didin J. on jul. 12, 2019 Build a web app using Nestjs, Fastify, MongoDB and Angular 8

Learn to build the efficient, reliable and scalable server-side web app using Nestjs, Fastify, MongoDB and Angular 8

Learn to build the efficient, reliable and scalable server-side web app using NestJS, Fastify, MongoDB and Angular 8. Nest.js is a complete development kit for building scalable server-side applications. The Fastify is alternative for Express.js as HTTP-Server dan claimed faster than Express.js. Angular 8 is the latest, smaller build and faster Angular version. So, this is the perfect combination for building better web apps.

Jumps to the steps:

As usual, we will learn to build the efficient, reliable and scalable server-side web app through the step by step from the scratch. Starting to build the backend application using Nestjs and Fastify then complete with Angular 8 as frontend application. The Angular 8 application access the data layer via RESTful API that provided by Nestjs which served by prefix URL `/api`. Nest.js accessing the MongoDB via Mongoose.js. All required libraries and modules that use by the backend already prepared very well by The NestJS.

The following libraries, frameworks, and modules are required for this tutorial:

  1. Node.js
  2. Angular 8
  3. Nest.js
  4. Fastify.js
  5. Mongoose.js
  6. MongoDB
  7. Terminal or Command Line
  8. IDE or Text Editor
  9. Postman

Before move further, make sure we have installed the Node.js and MongoDB server. To check if they were installed, type these commands in the terminal or command line.

node -v
v10.15.1
npm -v
6.9.0
mongo --version
MongoDB shell version: 3.2.11


Install Nestjs and Create a New Application

We will start the steps by building a backend or server-side application using Nestjs. For that, we have to install the Nest.js first by typing this command in the terminal or command line.

sudo npm i -g @nestjs/cli

You can run that command without `sudo` in the command line. Next, to create a new server-side application, type this command.

nest new nest-angular8

Next, go to the newly created project folder then run the application for the first time by type this command.

npm start

Open the browser then go to `http://localhost:3000` and you should see the text `Hello world!` in the web browser. Next, install the Fastify.js to increase the performance of Nestjs application because by default Nestjs using Express.js. Type this command to install it.

npm i --save @nestjs/platform-fastify

Next, open and edit `src/main.ts` then modify/add these imports of NestFactory, FastifyAdapter, NestFastifyApplication, and AppModule.

import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';

Change the bootstrap function to these lines of codes.

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
  );
  await app.listen(3000);
}
bootstrap();

Now, the Fastify.js will run as the default engine for Nestjs application.


Install and Configure Mongoose.js

We will use Mongoose.js as ORM for MongoDB. Mongoose provides a straight-forward, schema-based solution to model your application data. It includes built-in type casting, validation, query building, business logic hooks and more, out of the box. For that, type this command to install the Nest.js Mongoose.js modules.

npm install --save @nestjs/mongoose mongoose

Next, create a database folder inside the `src` folder. We will create all folder and files manually, if you like, you can generate them using Nestjs CLI.

mkdir database

Add 2 files to the database folder.

touch src/database/database.module.ts
touch src/database/database.provider.ts

Next, open and edit `src/database/database.provider.ts` then add these lines of codes.

import * as mongoose from 'mongoose';

export const databaseProviders = [
    {
        provide: 'DATABASE_CONNECTION',
        useFactory: async (): Promise<typeof mongoose> =>
        await mongoose.connect('mongodb://localhost/angular8-crud', { useNewUrlParser: true, useFindAndModify: false }),
    },
];

Provider in this database module is used for connecting the Nest application with MongoDB database. Next, open and edit `src/database/database.module.ts` then add these lines of codes.

import { Module } from '@nestjs/common';
import { databaseProviders } from './database.providers';

@Module({
    providers: [...databaseProviders],
    exports: [...databaseProviders],
})
export class DatabaseModule {}


Create a Mongoose Schema

The next steps to create a CRUD REST API is to create a Mongoose schema or model. Everything in Mongoose starts with a Schema. Each schema maps to a MongoDB collection and defines the shape of the documents within that collection. We will make a modular application for each object, so, for the Article object, we will create a folder for it first.

mkdir src/article

Next, create a schema folder and a schema file inside that folder.

mkdir src/article/schemas
touch src/article/schemas/article.schemas.ts

Next, open and edit that file then add these lines of Typescript codes.

import * as mongoose from 'mongoose';

export const ArticleSchema = new mongoose.Schema({
    title: { type: String, required: true },
    author: { type: String, required: true },
    description: { type: String, required: true },
    content: { type: String, required: true },
    updatedAt: { type: Date, default: Date.now },
});

Next, create a provider file to register the Mongoose schema as a model.

touch src/article/article.providers.ts

Open and edit that file then add these lines of Typescript codes.

import { Connection } from 'mongoose';
import { ArticleSchema } from './schemas/article.schemas';

export const articleProviders = [
    {
        provide: 'ARTICLE_MODEL',
        useFactory: (connection: Connection) => connection.model('Article', ArticleSchema),
        inject: ['DATABASE_CONNECTION'],
    },
];


Create the Routes for Article

Creating REST API using Nestjs similar to ASP Net Core Web API, especially when interacting with the Database. We have to create DTO and Interface to access Mongoose schema. Next, create a folder and a file for DTO inside article folder.

mkdir src/article/dto
touch src/article/dto/article.dto.ts

Open and edit that file then add these lines of Typescript codes.

export class ArticleDto {
    readonly title: string;
    readonly author: string;
    readonly description: string;
    readonly content: string;
}

Next, create a folder and file for the interface inside the article folder.

mkdir src/article/interfaces
touch src/article/interfaces/article.interface.ts

Open and edit that file then add these lines of Typescript codes.

import { Document } from 'mongoose';

export interface Article extends Document {
    readonly title: string;
    readonly author: string;
    readonly description: string;
    readonly content: string;
}

That interface similar with the DTO except the interface extends Mongoose Document module. Next, we will create a service for CRUD operation of Mongoose schema. Create a new file for the service inside the article folder.

touch src/article/article.service.ts

Open and edit that file then add these lines of Typescript codes that contain CRUD communication between Mongoose model, DTO, and objects.

import { Inject, Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { ArticleDto } from './dto/article.dto';
import { Article } from './interfaces/article.interface';

@Injectable()
export class ArticleService {

  constructor(@Inject('ARTICLE_MODEL') private readonly articleModel: Model<Article>) {}

  async create(articleDto: ArticleDto): Promise<Article> {
    const createdArticle = new this.articleModel(articleDto);
    return await createdArticle.save();
  }

  async findAll(): Promise<Article[]> {
    return await this.articleModel.find().exec();
  }

  async find(id: string): Promise<Article[]> {
    return await this.articleModel.findById(id).exec();
  }

  async update(id: string, articleDto: ArticleDto): Promise<Article> {
    return await this.articleModel.findByIdAndUpdate(id, articleDto);
  }

  async delete(id: string, articleDto: ArticleDto): Promise<Article> {
    return await this.articleModel.findByIdAndRemove(id);
  }
}

Creating the Nestjs routes means to create a controller that will transform by the Nest.js as a router. Type this command to create a file for the controller inside the article folder.

touch src/article/article.controller.ts

Open and edit that file then add these lines of Typescript for all Post, Put, Get, and Delete routes.

import { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common';
import { ArticleDto } from './dto/article.dto';
import { ArticleService } from './article.service';
import { Article } from './interfaces/article.interface';

@Controller('article')
export class ArticleController {
    constructor(private readonly articleService: ArticleService) {}

    @Post()
        async create(@Body() articleDto: ArticleDto) {
        return this.articleService.create(articleDto);
    }

    @Get()
        async findAll(): Promise<Article[]> {
        return this.articleService.findAll();
    }

    @Get(':id')
        async find(@Param('id') id: string) {
        return this.articleService.find(id);
    }

    @Put(':id')
        async update(@Param('id') id: string, @Body() articleDto: ArticleDto) {
        return this.articleService.update(id, articleDto);
    }

    @Delete(':id')
        async delete(@Param('id') id: string, @Body() articleDto: ArticleDto) {
        return this.articleService.delete(id, articleDto);
    }
}

The routes for Post, Get, Put, Delete, Param, and Body marked by Nestjs annotation. Next, create a file inside the article folder for the provider that register a connection from the schema to the Database that handles by database module.

touch src/article/article.providers.ts

Open and edit that file the add these lines of Typescript codes.

import { Connection } from 'mongoose';
import { ArticleSchema } from './schemas/article.schemas';

export const articleProviders = [
    {
        provide: 'ARTICLE_MODEL',
        useFactory: (connection: Connection) => connection.model('Article', ArticleSchema),
        inject: ['DATABASE_CONNECTION'],
    },
];

Next, wrapping all required files to build an article module as the RESTful API by creating a file inside the article folder for the module.

touch src/article/article.module.ts

Open and edit that file then add these lines of Typescript codes.

import { Module } from '@nestjs/common';
import { ArticleController } from './article.controller';
import { ArticleService } from './article.service';
import { articleProviders } from './article.providers';
import { DatabaseModule } from '../database/database.module';

@Module({
    imports: [DatabaseModule],
    controllers: [ArticleController],
    providers: [ArticleService, ...articleProviders],
})
export class ArticleModule {}

Finally, register the article module to the main Nestjs module. Open and edit `src/app.module.ts` then add this import.

import { ArticleModule } from './article/article.module';

Add to the `@Module` imports array.

@Module({
  imports: [ArticleModule],
})

We have to use different URL for the RESTful API. For that, create a prefix `/api` for the RESTful API by open and edit `src/main.ts` then add this line before the port declaration.

app.setGlobalPrefix('/api');


Test the Nestjs REST API using the Postman

Now, to test the Nestjs REST API, we will use the Postman application. First, run again the Nestjs application after running the MongoDB server in the different terminal tab.

npm start

Next, open the Postman application then make a GET request like below with the results.

Nestjs, Fastify, MongoDB and Angular 8 - Postman GET

To make a GET request for single result by ID, change the URL to this.

http://localhost:3000/api/article/5d234c34420cfcf038ee72c9

You should see the single result of the request like below.

Nestjs, Fastify, MongoDB and Angular 8 - Postman GET by ID

To make a POST request, change the method to POST then address to `http://localhost:3000/api/article`. Fill the body as raw data with this JSON data.

{
    "title": "A test article",
    "author": "Me",
    "description": "The article that live in the Nestjs server",
    "content": "The article that live in the Nestjs server."
}

You will see this response for the successful POST request.

{
    "_id": "5d28759e4f5e134b379e0dbb",
    "title": "A test article",
    "author": "Me",
    "description": "The article that live in the Nestjs server",
    "content": "The article that live in the Nestjs server.",
    "updatedAt": "2019-07-12T11:57:18.205Z",
    "__v": 0
}

For the PUT and DELETE method, just change the method in the Postman and the URL using the ID parameter. If everything works fine then the Nestjs RESTful API is ready to use with Angular 8 application.


Create a New Angular 8 Application

Now, for the frontend, we will create a new Angular 8 application. We will put the Angular 8 project folder inside the root of Nestjs folder. Before creating the new Angular 8 application, update the Angular CLI to the latest version.

sudo npm install -g @angular/cli

Check the installed or update Angular version using this command.

ng --version
Angular CLI: 8.0.6

Next, type this command to create a new Angular 8 application inside the Nestjs root folder.

ng new client

Go to the newly created Angular 8 project.

cd client

Run the Angular 8 application for the first time to make sure everything is on the track.

ng serve

You will see the Angular 8 started page in the browser when you point the browser address to `http://localhost:4200`.


Create Angular 8 Routes for Page Navigation

To create Angular 8 Routes for navigation between Angular 8 pages/component, add or generate all required component.

ng g component articles
ng g component show-article
ng g component add-article
ng g component edit-article

Next, open and edit `src/app/app-routing.module.ts` then add these imports.

import { ArticlesComponent } from './articles/articles.component';
import { ShowArticleComponent } from './show-article/show-article.component';
import { AddArticleComponent } from './add-article/add-article.component';
import { EditArticleComponent } from './edit-article/edit-article.component';

Add the route from the Angular 8 component to the routes constant variable.

const routes: Routes = [
  {
    path: 'articles',
    component: ArticlesComponent,
    data: { title: 'List of Articles' }
  },
  {
    path: 'show-article/:id',
    component: ShowArticleComponent,
    data: { title: 'Show Product' }
  },
  {
    path: 'add-article',
    component: AddArticleComponent,
    data: { title: 'Add Article' }
  },
  {
    path: 'edit-article/:id',
    component: EditArticleComponent,
    data: { title: 'Edit Article' }
  },
  { path: '',
    redirectTo: '/articles',
    pathMatch: 'full'
  }
];

Open and edit `src/app/app.component.html` and you will see the existing router outlet. Next, modify this HTML page to fit the CRUD page.

<div class="container">
  <router-outlet></router-outlet>
</div>

Open and edit `src/app/app.component.scss` then replace all SASS codes with this.

.container {
  padding: 20px;
}


Create an Angular 8 Service using HttpClient, Observable and RXJS

To access the Nestjs RESTful API from Angular 8, we need to create an Angular 8 service which will handle all POST, GET, UPDATE, DELETE requests. The response from the RESTful API emitted by Observable that can subscribe and read from the Components. For error handler and data Extraction, we will use RXJS Library. Before creating a service for RESTful API access, first, we have to install or register `HttpClientModule`. Open and edit `src/app/app.module.ts` then add this import.

import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

Add it to `@NgModule` imports after `BrowserModule`.

imports: [
  BrowserModule,
  FormsModule,
  HttpClientModule,
  AppRoutingModule
],

We will use type specifier to get a typed result object. For that, create a new Typescript file `src/app/article.ts` then add these lines of Typescript codes.

export class Article {
  _id: string;
  title: string;
  author: string;
  description: string;
  content: string;
  updatedAt: Date;
}

Next, generate an Angular 8 service by typing this command.

ng g service api

Next, open and edit `src/app/api.service.ts` then add these imports.

import { Observable, of, throwError } from 'rxjs';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { catchError, tap, map } from 'rxjs/operators';
import { Article } from './article';

Add these constants before the `@Injectable`.

const httpOptions = {
  headers: new HttpHeaders({'Content-Type': 'application/json'})
};
const apiUrl = '/api/article';

We are using URL for API using `/api/article` because at the end we will integrate the Angular 8 application as the static files for the Nestjs Fastify server. Next, inject `HttpClient` module to the constructor.

constructor(private http: HttpClient) { }

Add the error handler function.

private handleError<T>(operation = 'operation', result?: T) {
  return (error: any): Observable<T> => {
    console.error(error); // log to console instead
    return of(result as T);
  };
}

Add the functions for all CRUD (create, read, update, delete) RESTful call of article data.

getArticles(): Observable<Article[]> {
  return this.http.get<Article[]>(apiUrl)
    .pipe(
      tap(article => console.log('fetched articles')),
      catchError(this.handleError('getArticles', []))
    );
}

getArticle(id: number): Observable<Article> {
  const url = `${apiUrl}/${id}`;
  return this.http.get<Article>(url).pipe(
    tap(_ => console.log(`fetched article id=${id}`)),
    catchError(this.handleError<Article>(`getArticle id=${id}`))
  );
}

addArticle(article: Article): Observable<Article> {
  return this.http.post<Article>(apiUrl, article, httpOptions).pipe(
    tap((art: Article) => console.log(`added article w/ id=${art._id}`)),
    catchError(this.handleError<Article>('addArticle'))
  );
}

updateArticle(id: any, article: Article): Observable<any> {
  const url = `${apiUrl}/${id}`;
  return this.http.put(url, article, httpOptions).pipe(
    tap(_ => console.log(`updated article id=${id}`)),
    catchError(this.handleError<any>('updateArticle'))
  );
}

deleteArticle(id: any): Observable<Article> {
  const url = `${apiUrl}/${id}`;
  return this.http.delete<Article>(url, httpOptions).pipe(
    tap(_ => console.log(`deleted article id=${id}`)),
    catchError(this.handleError<Article>('deleteArticle'))
  );
}

You can find more details about Angular 8 Observable and RXJS here.


Display List of Articles using Angular 8 Material

We will display the list of articles published from API Service. The data published from the API service read by subscribing as an Article model in the Angular 8 component. For that, open and edit `src/app/articles/articles.component.ts` then add these imports.

import { ApiService } from '../api.service';

Next, inject the API Service to the constructor.

constructor(private api: ApiService) { }

Next, for the user interface (UI) we will use Angular 8 Material and CDK. There's a CLI for generating a Material component like Table as a component, but we will create or add the Table component from scratch to existing component. Type this command to install Angular 8 Material.

ng add @angular/material

If there are questions like below, just use the default answer.

? Choose a prebuilt theme name, or "custom" for a custom theme: Purple/Green       [ Preview: h
ttps://material.angular.io?theme=purple-green ]
? Set up HammerJS for gesture recognition? Yes
? Set up browser animations for Angular Material? Yes

We will register all required Angular 8 Material components or modules to `src/app/app.module.ts`. Open and edit that file then add these imports.

import {
  MatInputModule,
  MatPaginatorModule,
  MatProgressSpinnerModule,
  MatSortModule,
  MatTableModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatFormFieldModule } from "@angular/material";

Also, modify `FormsModule` import to add `ReactiveFormsModule`.

import { FormsModule, ReactiveFormsModule } from '@angular/forms';

Register the above modules to `@NgModule` imports.

imports: [
  BrowserModule,
  FormsModule,
  HttpClientModule,
  AppRoutingModule,
  ReactiveFormsModule,
  BrowserAnimationsModule,
  MatInputModule,
  MatTableModule,
  MatPaginatorModule,
  MatSortModule,
  MatProgressSpinnerModule,
  MatIconModule,
  MatButtonModule,
  MatCardModule,
  MatFormFieldModule
],

Next, back to `src/app/articles/articles.component.ts` then add these imports.

import { Article } from '../article';

Declare the variables of Angular 8 Material Table Data Source before the constructor.

displayedColumns: string[] = ['title', 'author'];
data: Article[] = [];
isLoadingResults = true;

Modify the `ngOnInit` function to get a list of articles immediately.

ngOnInit() {
  this.api.getArticles()
    .subscribe((res: any) => {
      this.data = res;
      console.log(this.data);
      this.isLoadingResults = false;
    }, err => {
      console.log(err);
      this.isLoadingResults = false;
    });
}

Next, open and edit `src/app/articles/articles.component.html` then replace all HTML tags with this Angular Material tags.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <div class="button-row">
    <a mat-flat-button color="primary" [routerLink]="['/add-article']"><mat-icon>add</mat-icon></a>
  </div>
  <div class="mat-elevation-z8">
    <table mat-table [dataSource]="data" class="example-table"
           matSort matSortActive="title" matSortDisableClear matSortDirection="asc">

      <!-- Article Title Column -->
      <ng-container matColumnDef="title">
        <th mat-header-cell *matHeaderCellDef>Title</th>
        <td mat-cell *matCellDef="let row">{{row.title}}</td>
      </ng-container>

      <!-- Article Author Column -->
      <ng-container matColumnDef="author">
        <th mat-header-cell *matHeaderCellDef>Author</th>
        <td mat-cell *matCellDef="let row">$ {{row.author}}</td>
      </ng-container>

      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: displayedColumns;" [routerLink]="['/show-article/', row._id]"></tr>
    </table>
  </div>
</div>

Finally, to make a little UI adjustment, open and edit `src/app/articles/articles.component.scss` then add this CSS codes.

/* Structure */
.example-container {
  position: relative;
  padding: 5px;
}

.example-table-container {
  position: relative;
  max-height: 400px;
  overflow: auto;
}

table {
  width: 100%;
}

.example-loading-shade {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 56px;
  right: 0;
  background: rgba(0, 0, 0, 0.15);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}

.example-rate-limit-reached {
  color: #980000;
  max-width: 360px;
  text-align: center;
}

/* Column Widths */
.mat-column-number,
.mat-column-state {
  max-width: 64px;
}

.mat-column-created {
  max-width: 124px;
}

.mat-flat-button {
  margin: 5px;
}


Show and Delete an Article Details using Angular 8 Material

To show article details after click or tap on the one of a row inside the Angular 8 Material table, open and edit `src/app/show-article/show-article.component.ts` then add these imports.

import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from '../api.service';
import { Article } from '../article';

Inject above modules to the constructor.

constructor(private route: ActivatedRoute, private api: ApiService, private router: Router) { }

Declare the variables before the constructor for hold article data that get from the API.

article: Article = { _id: '', title: '', author: '', description: '', content: '', updatedAt: null };
isLoadingResults = true;

Add a function for getting Article data from the API.

getArticleDetails(id: any) {
  this.api.getArticle(id)
    .subscribe((data: any) => {
      this.article = data;
      console.log(this.article);
      this.isLoadingResults = false;
    });
}

Call that function when the component is initiated.

ngOnInit() {
  this.getArticleDetails(this.route.snapshot.params.id);
}

Add this function for delete article.

deleteArticle(id: any) {
  this.isLoadingResults = true;
  this.api.deleteArticle(id)
    .subscribe(res => {
        this.isLoadingResults = false;
        this.router.navigate(['/articles']);
      }, (err) => {
        console.log(err);
        this.isLoadingResults = false;
      }
    );
}

For the view, open and edit `src/app/article-detail/article-detail.component.html` then replace all HTML tags with this.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <div class="button-row">
    <a mat-flat-button color="primary" [routerLink]="['/articles']"><mat-icon>list</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <mat-card-header>
      <mat-card-title><h2>{{article.title}}</h2></mat-card-title>
      <mat-card-subtitle>{{article.author}} | {{article.updatedAt}}</mat-card-subtitle>
    </mat-card-header>
    <mat-card-content>
      <h3>{{article.description}}</h3>
      <p>{{article.content}}</p>
    </mat-card-content>
    <mat-card-actions>
      <a mat-flat-button color="primary" [routerLink]="['/edit-article', article._id]"><mat-icon>edit</mat-icon></a>
      <a mat-flat-button color="warn" (click)="deleteArticle(article._id)"><mat-icon>delete</mat-icon></a>
    </mat-card-actions>
  </mat-card>
</div>

Finally, open and edit `src/app/article-detail/article-detail.component.scss` then add this lines of CSS codes.

/* Structure */
.example-container {
  position: relative;
  padding: 5px;
}

.example-loading-shade {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 56px;
  right: 0;
  background: rgba(0, 0, 0, 0.15);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}

.mat-flat-button {
  margin: 5px;
}


Add an Article using Angular 8 Material

To create a form for adding an Article, open and edit `src/app/add-article/add-article.component.ts` then add these imports.

import { Router } from '@angular/router';
import { ApiService } from '../api.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';

Inject above modules to the constructor.

constructor(private router: Router, private api: ApiService, private formBuilder: FormBuilder) { }

Declare variables for the Form Group and all of the required fields inside the form before the constructor.

articleForm: FormGroup;
title = '';
author = '';
description = '';
content = '';
isLoadingResults = false;
matcher = new MyErrorStateMatcher();

Next, create a class for `MyErrorStateMatcher` before the main class `@Components`.

/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
  }
}

Add initial validation for each field.

ngOnInit() {
  this.articleForm = this.formBuilder.group({
    'title' : [null, Validators.required],
    'author' : [null, Validators.required],
    'description' : [null, Validators.required],
    'content' : [null, Validators.required]
  });
}

Create a function for submitting or POST article form.

onFormSubmit() {
  this.isLoadingResults = true;
  this.api.addArticle(this.articleForm.value)
    .subscribe((res: any) => {
        const id = res._id;
        this.isLoadingResults = false;
        this.router.navigate(['/show-article', id]);
      }, (err: any) => {
        console.log(err);
        this.isLoadingResults = false;
      });
}

Next, open and edit `src/app/add-article/add-article.component.html` then replace all HTML tags with this.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <div class="button-row">
    <a mat-flat-button color="primary" [routerLink]="['/articles']"><mat-icon>list</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="articleForm" (ngSubmit)="onFormSubmit()">
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Title" formControlName="title"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!articleForm.get('title').valid && articleForm.get('title').touched">Please enter Title</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Author" formControlName="author"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!articleForm.get('author').valid && articleForm.get('author').touched">Please enter Author</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Description" formControlName="description"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!articleForm.get('description').valid && articleForm.get('description').touched">Please enter Description</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <textarea matInput placeholder="Content" formControlName="content"
               [errorStateMatcher]="matcher"></textarea>
        <mat-error>
          <span *ngIf="!articleForm.get('content').valid && articleForm.get('content').touched">Please enter Content</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!articleForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
      </div>
    </form>
  </mat-card>
</div>

Finally, open and edit `src/app/article-add/article-add.component.scss` then add this CSS codes.

/* Structure */
.example-container {
  position: relative;
  padding: 5px;
}

.example-form {
  min-width: 150px;
  max-width: 500px;
  width: 100%;
}

.example-full-width {
  width: 100%;
}

.example-full-width:nth-last-child(0) {
  margin-bottom: 10px;
}

.button-row {
  margin: 10px 0;
}

.mat-flat-button {
  margin: 5px;
}


Edit an Article using Angular 8 Material

We have put an edit button inside the Article Detail component for call Edit page. Now, open and edit `src/app/edit-article/edit-article.component.ts` then add these imports.

import { Router, ActivatedRoute } from '@angular/router';
import { ApiService } from '../api.service';
import { FormControl, FormGroupDirective, FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';

Next, create a class for `ErrorStateMatcher` before the main class `@Compoents`.

/** Error when invalid control is dirty, touched, or submitted. */
export class MyErrorStateMatcher implements ErrorStateMatcher {
  isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
    const isSubmitted = form && form.submitted;
    return !!(control && control.invalid && (control.dirty || control.touched || isSubmitted));
  }
}

Inject the imported modules to the constructor.

constructor(private router: Router, private route: ActivatedRoute, private api: ApiService, private formBuilder: FormBuilder) { }

Declare the Form Group variable and all of the required variables for the articles form before the constructor.

articleForm: FormGroup;
_id = '';
title = '';
author = '';
description = '';
content = '';
isLoadingResults = false;
matcher = new MyErrorStateMatcher();

Next, add validation for all fields when the component is initiated.

ngOnInit() {
  this.getArticle(this.route.snapshot.params.id);
  this.articleForm = this.formBuilder.group({
    'title' : [null, Validators.required],
    'author' : [null, Validators.required],
    'description' : [null, Validators.required],
    'content' : [null, Validators.required]
  });
}

Create a function for getting article data that filled to each form fields.

getArticle(id: any) {
  this.api.getArticle(id).subscribe((data: any) => {
    this._id = data._id;
    this.articleForm.setValue({
      title: data.title,
      author: data.author,
      description: data.description,
      content: data.content
    });
  });
}

Create a function to update the article changes.

onFormSubmit() {
  this.isLoadingResults = true;
  this.api.updateArticle(this._id, this.articleForm.value)
    .subscribe((res: any) => {
        const id = res._id;
        this.isLoadingResults = false;
        this.router.navigate(['/show-article', id]);
      }, (err: any) => {
        console.log(err);
        this.isLoadingResults = false;
      }
    );
}

Add a function for handling the show article details button.

articleDetails() {
  this.router.navigate(['/show-article', this._id]);
}

Next, open and edit `src/app/edit-article/edit-article.component.html` then replace all HTML tags with this.

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    <mat-spinner *ngIf="isLoadingResults"></mat-spinner>
  </div>
  <div class="button-row">
    <a mat-flat-button color="primary" (click)="articleDetails()"><mat-icon>info</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="articleForm" (ngSubmit)="onFormSubmit()">
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Title" formControlName="title"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!articleForm.get('title').valid && articleForm.get('title').touched">Please enter Title</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Author" formControlName="author"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!articleForm.get('author').valid && articleForm.get('author').touched">Please enter Author</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Description" formControlName="description"
               [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!articleForm.get('description').valid && articleForm.get('description').touched">Please enter Description</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <textarea matInput placeholder="Content" formControlName="content"
               [errorStateMatcher]="matcher"></textarea>
        <mat-error>
          <span *ngIf="!articleForm.get('content').valid && articleForm.get('content').touched">Please enter Content</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!articleForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
      </div>
    </form>
  </mat-card>
</div>

Finally, open and edit `src/app/edit-article/edit-article.component.scss` then add this lines of CSS codes.

/* Structure */
.example-container {
  position: relative;
  padding: 5px;
}

.example-form {
  min-width: 150px;
  max-width: 500px;
  width: 100%;
}

.example-full-width {
  width: 100%;
}

.example-full-width:nth-last-child(0) {
  margin-bottom: 10px;
}

.button-row {
  margin: 10px 0;
}

.mat-flat-button {
  margin: 5px;
}


Integrate, Test and Run the Complete Nestjs, Fastify, MongoDB and Angular 8 Web Apps

To integrate the Angular 8 application into the Nestjs/Fastify application, open and edit `src/main.js` then add this import.

import { join } from 'path';

Add this line before the port listener line.

app.useStaticAssets({
  root: join(__dirname, '..', 'client/dist/client'),
  prefix: '/',
});

Next, make sure the MongoDB server is running then build the Angular 8 application.

cd client
ng build --prod

The Angular 8 build should be located in `client/dist/client` folder. Next, back to the root of Nestjs folder then run again the Nestjs application.

npm start

Open the browser and go to `http://localhost:3000` and you should see this application.

Nestjs, Fastify, MongoDB and Angular 8 - List of Articles
Nestjs, Fastify, MongoDB and Angular 8 - Add an Article
Nestjs, Fastify, MongoDB and Angular 8 - Show an Article

That it's, the complete step by step tutorial of building a web app using Nestjs, Fastify, MongoDB and Angular 8. You can find the full source code in our GitHub.

If you don’t want to waste your time design your own front-end or your budget to spend by hiring a web designer then Angular Templates is the best place to go. So, speed up your front-end web development with premium Angular templates. Choose your template for your front-end project here.

That just the basic. If you need more deep learning about MEAN Stack, Angular, and Node.js, you can take the following cheap course:

Thanks!

Loading…