Modern Full‑Stack: NestJS + Fastify + MongoDB + Angular

by Didin J. on Jun 20, 2025 Modern Full‑Stack: NestJS + Fastify + MongoDB + Angular

Build a fast, scalable full‑stack web app using NestJS with Fastify, MongoDB, and Angular. Step‑by‑step, end‑to‑end guide included.

In this updated tutorial, you’ll learn how to build a modern, efficient full‑stack web application using NestJS and Fastify for your backend, MongoDB with Mongoose for data storage, and Angular 20+ for the front end. We’ll take you step-by-step—from setting up the backend REST API to integrating the Angular client—showing you how these technologies work together to deliver a fast, scalable, and maintainable app. Whether you're upgrading from Express or starting anew, this guide equips you to develop real-world applications with best practices.


Backend Setup: NestJS with Fastify

1. Set up Nest.js

We will start the steps by building a backend or server-side application using NestJS. For that, we have to install 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-angular

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

cd nest-angular
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 Fastify.js to increase the performance of the Nest.js application because by default, Nest.js uses Express.js. Type this command to install it.

npm i --save @nestjs/platform-fastify

2. Configure 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 { AppModule } from './app.module';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';

Change the bootstrap function to these lines of code.

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

Now, Fastify.js will run as the default engine for Nest.js applications.


Database with Mongoose & MongoDB

1. Install and Configure Mongoose.js

We will use Mongoose.js as an ORM for MongoDB. Mongoose provides a straightforward, 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 and Mongoose.js modules.

npm install --save @nestjs/mongoose mongoose

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

mkdir src/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 code.

import * as mongoose from 'mongoose';

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

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

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

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

2. Create a Mongoose Schema

The next steps to create a CRUD REST API are 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 code.

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 code.

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'],
    },
];


REST CRUD Endpoints

1. Create DTO

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

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

Open and edit that file, then add these lines of TypeScript code.

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 code.

import { Document } from 'mongoose';

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

That interface is similar to the DTO except that the interface extends the Mongoose Document module.

2. Create Service

Next, we will create a service for the CRUD operation of the 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 code that contain CRUD communication between the 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 | null> {
        return await this.articleModel.findById(id).exec();
    }

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

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

Creating the Nestjs routes means creating a controller that will be transformed by Nest.js into a router.

3. Create Controller

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 are marked by Nestjs annotation.

4. Register the Controller

Next, create a file inside the article folder for the provider that registers a connection from the schema to the Database that is handled by the database module.

touch src/article/article.providers.ts

Open and edit that file, the add these lines of TypeScript code.

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, wrap 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 code.

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 a different URL for the RESTful API. For that, create a prefix `/api` for the RESTful API by opening and editing `src/main.ts`, then add this line before the port declaration.

app.setGlobalPrefix('/api');

5. Test the Nest.js REST API using Postman

Now, to test the Nestjs REST API, we will use the Postman application. First, run the Nestjs application again after running the MongoDB server in a 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 a 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 methods, just change the method in Postman and the URL using the ID parameter. If everything works fine, then the Nestjs RESTful API is ready to use with an Angular application.


Angular Front End

1. Install and Create an Angular Application

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

sudo npm install -g @angular/cli

Check the installed version or update the Angular version using this command.

ng version

Angular CLI: 20.0.3
Node: 22.16.0
Package Manager: npm 11.4.1
OS: darwin arm64

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

ng new client

Go to the newly created Angular project.

cd client

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

ng serve

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

2. Create Angular Routes for Page Navigation

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

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

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

import { Routes } from '@angular/router';
import { Articles } from './components/articles/articles';
import { ShowArticle } from './components/show-article/show-article';
import { AddArticle } from './components/add-article/add-article';
import { EditArticle } from './components/edit-article/edit-article';

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

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

Open and edit `src/app/app.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.scss`, then replace all SASS codes with this.

.container {
  padding: 20px;
}

3. Create an Angular Service using HttpClient, Observable, and RXJS

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

import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient()
  ]
};

We will use a 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 code.

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

Next, generate an Angular service by typing this command.

ng g service api

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

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { catchError, Observable, of, tap } from 'rxjs';
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 a URL for API using `/api/article` because at the end we will integrate the Angular application as the static files for the Nestjs Fastify server. Next, inject the `HttpClient` module into 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 calls 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 Observable and RXJS here.

4. Display List of Articles using Angular Material

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

import { Api } from '../../api';

Next, inject the API Service into the constructor.

constructor(private api: Api) { }

Next, for the user interface (UI), we will use Angular 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 the existing component. Type this command to install Angular 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

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

import { Component } from '@angular/core';
import { Api } from '../../api';
import { Article } from '../../article';
import { MatTableModule } from '@angular/material/table';
import { MatIconModule } from '@angular/material/icon';
import { RouterModule } from '@angular/router';

@Component({
  selector: 'app-articles',
  imports: [
    MatTableModule,
    MatIconModule,
    RouterModule
  ],
  templateUrl: './articles.html',
  styleUrl: './articles.scss'
})

Declare the variables of Angular 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({
        next: (res) => {
          this.data = res;
          console.log(this.data);
          this.isLoadingResults = false;
        }, error: (err) => {
          console.log(err);
          this.isLoadingResults = false;
        }
      });
  }

Next, open and edit `src/app/articles/articles.html`, then replace all HTML tags with these 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.scss`, then add this SCSS code.

/* 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;
}

5. Show and Delete Article Details using Angular Material

To show article details after clicking or tapping on one of the rows inside the Angular Material table, open and edit `src/app/show-article/show-article.ts`, then add these imports.

import { Component } from '@angular/core';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { Api } from '../../api';
import { Article } from '../../article';
import { MatIconModule } from '@angular/material/icon';
import { MatCardModule } from '@angular/material/card';

@Component({
  selector: 'app-show-article',
  imports: [
    RouterModule,
    MatIconModule,
    MatCardModule
  ],
  templateUrl: './show-article.html',
  styleUrl: './show-article.scss'
})

Inject the above modules into the constructor.

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

Declare the variables before the constructor to hold article data that is obtained 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 to delete an article.

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

For the view, open and edit `src/app/article-detail/article-detail.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.scss`, then add these lines of SCSS code.

/* 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;
}

6. Add an Article using Angular Material

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

import { Component } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { Api } from '../../api';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, FormsModule, NgForm, ReactiveFormsModule, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';

@Component({
  selector: 'app-add-article',
  imports: [
    FormsModule,
    ReactiveFormsModule,
    CommonModule,
    RouterModule,
    MatCardModule,
    MatIconModule,
    MatFormFieldModule,
    MatInputModule,
  ],
  templateUrl: './add-article.html',
  styleUrl: './add-article.scss'
})

Inject the above modules into 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.

  constructor(private router: Router, private api: Api, private formBuilder: FormBuilder) {
    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({
        next: (res) => {
          const id = res._id;
          this.isLoadingResults = false;
          this.router.navigate(['/show-article', id]);
        }, error: (err) => {
          console.log(err);
          this.isLoadingResults = false;
        }
      });
  }

Next, open and edit `src/app/add-article/add-article.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.scss`, then add this SCSS code.

/* 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;
}

7. Edit an Article using Angular Material

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

import { Component } from '@angular/core';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { Api } from '../../api';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, FormsModule, NgForm, ReactiveFormsModule, Validators } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';

@Component({
  selector: 'app-edit-article',
  imports: [
    FormsModule,
    ReactiveFormsModule,
    CommonModule,
    RouterModule,
    MatCardModule,
    MatIconModule,
    MatFormFieldModule,
    MatInputModule,
  ],
  templateUrl: './edit-article.html',
  styleUrl: './edit-article.scss'
})

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 into the constructor.

constructor(private router: Router, private route: ActivatedRoute, private api: Api, 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.

  constructor(private router: Router, private route: ActivatedRoute, private api: Api, private formBuilder: FormBuilder) {
    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 fills 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({
        next: (res) => {
          const id = res._id;
          this.isLoadingResults = false;
          this.router.navigate(['/show-article', id]);
        }, error: (err) => {
          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.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.scss`, then add these lines of SCSS code.

/* 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;
}


Integration & Testing

To integrate the Angular application into the Nestjs/Fastify application, open and edit `src/main.ts`, 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 application.

cd client
ng build --prod

The Angular build should be located in the `client/dist/client` folder. Next, go back to the root of the 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

It's the complete step-by-step tutorial of building a web app using NestJS, Fastify, MongoDB, and Angular. You can find the full source code on our GitHub.

If you don’t want to waste your time designing your 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's just the basics. If you need more deep learning about MEAN Stack, Angular, and Node.js, you can take the following cheap course:

Thanks!