Angular 10 Universal Server Side Rendering (SSR) CRUD Example

by Didin J. on Jul 28, 2020 Angular 10 Universal Server Side Rendering (SSR) CRUD Example

The comprehensive Angular 10 tutorial on building CRUD web application using Angular Universal Server-side rendering (SSR)

In this Angular 10 tutorial, we will show you a practical exercise on building a CRUD web application using Angular Universal Server-side rendering (SSR). Server-side rendering means a client-side (Angular app) render on the server instead of the browser to get less load to the client-side. The server-side using Express-engine and MongoDB for the datastore.

This tutorial divided into several steps:

The Angular Universal application that we will build is a very simple application of create, read, update, delete (CRUD) operation. At the end, we will show you how to deploy it to the VPS.

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

  1. Node.js
  2. Angular Universal with Express-Engine
  3. MongoDB and Mongoose.js
  4. Angular Material
  5. VPS contains Ubuntu, Nginx, PM2, and MongoDB
  6. Terminal or CMD
  7. IDE or Text Editor (we are using VSCode)

Let's get started with the main steps!

You can watch the video tutorial on our YouTube channel here.


Step #1: Create an Angular Universal Application

As usual, we will start this tutorial by preparing the environment to create or build an Angular application inside the Node.js ecosystem. First, make sure you have download the latest and recommended Node.js version and can run NPM in the Terminal or CMD. To check the existing Node and NPM, type this command.

node -v
v12.18.0
npm -v
6.14.5

Install or update the latest Angular CLI by type this command.

sudo npm install -g @angular/cli

Next, create a new Angular application by type this command.

ng new angular-ssr

If there are the questions, give them the answers like this.

? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS   [ https://sass-lang.com/
documentation/syntax#scss                ]

Next, go to the newly created Angular 10 application folder then type this command to add Angular Universal SSR.

cd ./angular-ssr
ng add @nguniversal/express-engine --clientProject angular-ssr

Next, check the Angular Universal SSR application by running this application using these commands.

npm run build:ssr && npm run serve:ssr

Open the browser then go to 'localhost:4000' and you will this standard Angular welcome page.

Angular 10 Universal Server Side Rendering (SSR) CRUD Example - welcome page


Step #2: Add Mongoose.js Schema and Router

We will use Mongoose.js as ODM for MongoDB. The MongoDB collection will be represented by the Mongoose.js model. Add the Mongoose.js and required dependencies to this Angular Universal project by type this command.

npm install --save mongoose @types/mongoose

Next, open this Angular Universal project using your IDE or text editor. To use Visual Studio Code, just type this command.

code .

Next, open and edit `server.ts` in the root project folder then add these imports of Mongoose and body-parser. 

import * as mongoose from 'mongoose';

Next, add these lines of codes inside the export function body after the constant variables to connect to MongoDB.

  mongoose.connect('mongodb://localhost/angular-ssr', { useNewUrlParser: true, useFindAndModify: false, useUnifiedTopology: true })
  .then(() =>  console.log('connection successful'))
  .catch((err) => console.error(err));

Add these lines after the server.set code to parse the JSON body on the request.

  server.use(express.json());
  server.use(express.urlencoded({ extended: true }));

Next, add a new folder for Mongoose models and add a mongoose model inside that folder.

mkdir models
touch models/book.ts

Fill that file with these codes of Mongoose Schema.

import mongoose, { Schema } from 'mongoose';

const BookSchema: Schema = new Schema({
  isbn: { type: String, required: true },
  title: { type: String, required: true },
  author: { type: String, required: true },
  description: { type: String, required: true },
  publisher: { type: String, required: true },
  publishYear: { type: String, required: true },
  updatedAt: { type: Date, default: Date.now }
});

export default mongoose.model('Book', BookSchema);

Next, we will create a route that accesses the MongoDB data via API. Create a new folder call `routes` and a file inside it.

mkdir routes
touch routes/book-route.ts

Open and edit that file then add these imports of Express Request, Response, NextFunction, and Book schema.

import { Request, Response, NextFunction } from 'express';
import Book from '../models/book';

Add these lines of codes to create a CRUD method of REST API. The route path starts with `/book/` that will be called from the Angular 10 application using full URL `http://localhost:4000/book/`.

export class BookRoute {

  public bookRoute(app: any): void {
    app.route('/book/').get((req: Request, res: Response, next: NextFunction) => {
      Book.find((err, books) => {
        if (err) { return next(err); }
        res.json(books);
      });
    });

    app.route('/book/:id').get((req: Request, res: Response, next: NextFunction) => {
      Book.findById(req.params.id, (err, book) => {
        if (err) { return next(err); }
        res.json(book);
      });
    });

    app.route('/book/').post((req: Request, res: Response, next: NextFunction) => {
      console.log(req.body);
      Book.create(req.body, (err, book) => {
        if (err) { return next(err); }
        res.json(book);
      });
    });

    app.route('/book/:id').put((req: Request, res: Response, next: NextFunction) => {
      Book.findByIdAndUpdate(req.params.id, req.body, (err, book) => {
        if (err) { return next(err); }
        res.json(book);
      });
    });

    app.route('/book/:id').delete((req: Request, res: Response, next: NextFunction) => {
      Book.findByIdAndRemove(req.params.id, req.body, (err, book) => {
        if (err) { return next(err); }
        res.json(book);
      });
    });
  }
}

Next, open and edit `server.ts` then add this import.

import { BookRoute } from './routes/book-route';

Declare a constant after other constants.

const bookRoute: BookRoute = new BookRoute();

Add this line before the server.get.

  bookRoute.bookRoute(server);

Don't declare API route after all regular route otherwise the API URL won't working.


Step #3: Add Angular Routes and Navigation

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

ng g component books --skip-import
ng g component book-details --skip-import
ng g component book-add --skip-import
ng g component book-edit --skip-import

There's an additional `--skip-import` variable because there will be a conflict between 2 modules, Angular 10 and Server modules.

More than one module matches. Use skip-import option to skip importing the component into the closest module.

Next, we will manually add or register those components to the `src/app/app.module.ts`. Add these imports of the created Angular components to that file.

import { BooksComponent } from './books/books.component';
import { BookDetailsComponent } from './book-details/book-details.component';
import { BookAddComponent } from './book-add/book-add.component';
import { BookEditComponent } from './book-edit/book-edit.component';

Then add those components to the `@NgModule` declaration.

@NgModule({
  declarations: [
    ...
    BooksComponent,
    BookDetailsComponent,
    BookAddComponent,
    BookEditComponent
  ],
  ...
})

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

import { BooksComponent } from './books/books.component';
import { BookDetailsComponent } from './book-details/book-details.component';
import { BookAddComponent } from './book-add/book-add.component';
import { BookEditComponent } from './book-edit/book-edit.component';

Modify the routes' constant variable to this.

const routes: Routes = [
  {
    path: 'books',
    component: BooksComponent,
    data: { title: 'Books' }
  },
  {
    path: 'book-details/:id',
    component: BookDetailsComponent,
    data: { title: 'Book Details' }
  },
  {
    path: 'book-add',
    component: BookAddComponent,
    data: { title: 'Add Book' }
  },
  {
    path: 'book-edit/:id',
    component: BookEditComponent,
    data: { title: 'Edit Book' }
  },
  { path: '',
    redirectTo: '/books',
    pathMatch: 'full'
  }
];

Next, 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 that will be wrapped inside <router-outlet>.

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


Step #4: Add Angular Service

To access REST API from Angular 10 Universal, we need to create an Angular 10 service that will handle all POST, GET, UPDATE, DELETE requests. The response from the REST 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 { HttpClientModule } from '@angular/common/http';

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

  imports: [
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
    AppRoutingModule,
    HttpClientModule
  ],

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

export class Book {
  _id: string;
  isbn: string;
  title: string;
  author: string;
  description: string;
  publisher: string;
  publishedYear: string;
  updatedAt: Date;
}

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

ng g service api

Next, open and edit `src/app/api.service.ts` then add these imports of RxJS Observable,  throwError, catchError, Angular HttpClient, HttpHeaders, HttpErrorResponse, and Article object model.

import { Observable, throwError } from 'rxjs';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { Book } from './book';

Add this constant of full URL before the `@Injectable`.

const apiUrl = 'http://192.168.0.7:4000/book';

Inject the `HttpClient` module to the constructor.

constructor(private http: HttpClient) {}

Add the error handler function.

  private handleError(error: HttpErrorResponse): any {
    console.error(
      `Backend returned code ${error.status}, ` +
      `body was: ${error.message}`);
    return throwError(
      'Something bad happened; please try again later.');
  }

Add the functions for all CRUD (create, read, update, delete) operation call to REST API.

  getBooks(): Observable<any> {
    return this.http.get<Book[]>(apiUrl)
      .pipe(
        catchError(this.handleError)
      );
  }

  getBook(id: number): Observable<any> {
    const url = `${apiUrl}/${id}`;
    return this.http.get<Book>(url).pipe(
      catchError(this.handleError)
    );
  }

  addBook(book: Book): Observable<any> {
    return this.http.post<Book>(apiUrl, book).pipe(
      catchError(this.handleError)
    );
  }

  updateBook(id: any, book: Book): Observable<any> {
    const url = `${apiUrl}/${id}`;
    return this.http.put(url, book).pipe(
      catchError(this.handleError)
    );
  }

  deleteBook(id: any): Observable<any> {
    const url = `${apiUrl}/${id}`;
    return this.http.delete<Book>(url).pipe(
      catchError(this.handleError)
    );
  }

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


Step #5: Add Angular Material UI/UX for CRUD operation

Before implementing UI/UX for CRUD operation using Angular Material, first, install all required Angular modules and schematics.

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: Indigo/Pink     
   [ Preview: https://material.angular.io?theme=indigo-pink ]
? Set up global Angular Material typography styles? No
? Set up browser animations for Angular Material? Yes

We will register all required Angular 10 Material components or modules to `src/app/app.module.ts`. Open and edit that file then add these required imports of Angular Material components, forms, and reactive form module.

import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';

Register those modules to `@NgModule` imports.

  imports: [
    ...
    FormsModule,
    ReactiveFormsModule,
    MatInputModule,
    MatPaginatorModule,
    MatProgressSpinnerModule,
    MatSortModule,
    MatTableModule,
    MatIconModule,
    MatButtonModule,
    MatCardModule,
    MatFormFieldModule
  ],

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

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

Inject the ApiService to the constructor.

  constructor(private api: ApiService) { }

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

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

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

  ngOnInit(): void {
    this.api.getBooks()
      .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/books/books.component.html` then replace all HTML tags with this Angular Material component that displays the Angular Table.

<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]="['/book-add']"><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]="['/book-details/', row._id]"></tr>
    </table>
  </div>
</div>

Finally, to make a little UI adjustment, open and edit `src/app/books/books.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;
}

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

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

Inject the above modules to the constructor.

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

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

  book: Book = {
    _id: '',
    isbn: '',
    title: '',
    author: '',
    description: '',
    publisher: '',
    publishedYear: '',
    updatedAt: null
  };
  isLoadingResults = true;

Add a function for getting Book data from the API.

  getBookDetails(id: any): void {
    this.api.getBook(id)
      .subscribe((data: any) => {
        this.book = data;
        console.log(this.book);
        this.isLoadingResults = false;
      });
  }

Call that function when the component is initiated.

  ngOnInit(): void {
    this.getBookDetails(this.route.snapshot.paramMap.get('id'));
  }

Add this function to delete a book.

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

For the view, open and edit `src/app/book-detaails/book-detaails.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]="['/books']"><mat-icon>list</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <mat-card-header>
      <mat-card-title><h2>{{book.title}}</h2></mat-card-title>
      <mat-card-subtitle>{{book.author}} | {{book.updatedAt}}</mat-card-subtitle>
    </mat-card-header>
    <mat-card-content>
      <h3>ISBN: {{book.isbn}}</h3>
      <h3>Description: {{book.description}}</h3>
      <h3>Publisher: {{book.publisher}}</h3>
      <h3>Published Year:{{book.publishedYear}}</h3>
    </mat-card-content>
    <mat-card-actions>
      <a mat-flat-button color="primary" [routerLink]="['/book-edit', book._id]"><mat-icon>edit</mat-icon></a>
      <a mat-flat-button color="warn" (click)="deleteBook(book._id)"><mat-icon>delete</mat-icon></a>
    </mat-card-actions>
  </mat-card>
</div>

Finally, open and edit `src/app/book-details/book-details.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;
}

Next, to create a form for adding a book, open and edit `src/app/book-add/book-add.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';

Create a new class 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));
  }
}

Inject the 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, loading spinner trigger, and initialize ErrorStateMatcher inside the form before the constructor.

  bookForm: FormGroup;
  isbn = '';
  book = '';
  author = '';
  description = '';
  publisher = '';
  publishedYear = '';
  isLoadingResults = false;
  matcher = new MyErrorStateMatcher();

Add initial validation for each field.

  ngOnInit(): void {
    this.bookForm = this.formBuilder.group({
      'isbn' : [null, Validators.required],
      'title' : [null, Validators.required],
      'author' : [null, Validators.required],
      'description' : [null, Validators.required],
      'publisher' : [null, Validators.required],
      'publishedYear' : [null, Validators.required]
    });
  }

Create a function for submitting or POST book form.

  onFormSubmit(): void {
    this.isLoadingResults = true;
    this.api.addBook(this.bookForm.value)
      .subscribe((res: any) => {
          const id = res._id;
          this.isLoadingResults = false;
          this.router.navigate(['/book-details', id]);
        }, (err: any) => {
          console.log(err);
          this.isLoadingResults = false;
        });
  }

Next, open and edit `src/app/book-add/book-add.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]="['/books']"><mat-icon>list</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="bookForm" (ngSubmit)="onFormSubmit()">
      <mat-form-field class="example-full-width">
        <input matInput placeholder="ISBN" formControlName="isbn"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('isbn').valid && bookForm.get('isbn').touched">Please enter ISBN</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Title" formControlName="title"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('title').valid && bookForm.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="!bookForm.get('author').valid && bookForm.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="!bookForm.get('description').valid && bookForm.get('description').touched">Please enter Description</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Publisher" formControlName="publisher"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('publisher').valid && bookForm.get('publisher').touched">Please enter Publisher</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Published Year" formControlName="publishedYear"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('publishedYear').valid && bookForm.get('publishedYear').touched">Please enter Published Year</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!bookForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
      </div>
    </form>
  </mat-card>
</div>

Finally, open and edit `src/app/book-add/book-add.component.scss` then add this SCSS 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;
}

Next, open and edit `src/app/book-edit/book-edit.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';

Create a new class before the main class `@Components` to catch the error in the form entry.

/** 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 above 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 of a book form, loading spinner trigger, and ErrorStateMatcher before the constructor.

  bookForm: FormGroup;
  _id = '';
  isbn = '';
  title = '';
  author = '';
  description = '';
  publisher = '';
  publishedYear = '';
  isLoadingResults = false;
  matcher = new MyErrorStateMatcher();

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

  ngOnInit(): void {
    this.getBook(this.route.snapshot.paramMap.get('id'));
    this.bookForm = this.formBuilder.group({
      'isbn' : [null, Validators.required],
      'title' : [null, Validators.required],
      'author' : [null, Validators.required],
      'description' : [null, Validators.required],
      'publisher' : [null, Validators.required],
      'publishedYear' : [null, Validators.required]
    });
  }

Create a function for getting book data that filled to each form field.

  getBook(id: any): void {
    this.api.getBook(id).subscribe((data: any) => {
      this._id = data._id;
      this.bookForm.setValue({
        isbn: data.isbn,
        title: data.title,
        author: data.author,
        description: data.description,
        publisher: data.publisher,
        publishedYear: data.publishedYear
      });
    });
  }

Create a function to update the book changes.

  onFormSubmit(): void {
    this.isLoadingResults = true;
    this.api.updateBook(this._id, this.bookForm.value)
      .subscribe((res: any) => {
          const id = res._id;
          this.isLoadingResults = false;
          this.router.navigate(['/book-details', id]);
        }, (err: any) => {
          console.log(err);
          this.isLoadingResults = false;
        }
      );
  }

Add a function for handling the show article details button.

  bookDetails(): void {
    this.router.navigate(['/book-details', this._id]);
  }

Next, open and edit `src/app/book-edit/book-edit.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)="bookDetails()"><mat-icon>info</mat-icon></a>
  </div>
  <mat-card class="example-card">
    <form [formGroup]="bookForm" (ngSubmit)="onFormSubmit()">
      <mat-form-field class="example-full-width">
        <input matInput placeholder="ISBN" formControlName="isbn"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('isbn').valid && bookForm.get('isbn').touched">Please enter ISBN</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Title" formControlName="title"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('title').valid && bookForm.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="!bookForm.get('author').valid && bookForm.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="!bookForm.get('description').valid && bookForm.get('description').touched">Please enter Description</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Publisher" formControlName="publisher"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('publisher').valid && bookForm.get('publisher').touched">Please enter Publisher</span>
        </mat-error>
      </mat-form-field>
      <mat-form-field class="example-full-width">
        <input matInput placeholder="Published Year" formControlName="publishedYear"
                [errorStateMatcher]="matcher">
        <mat-error>
          <span *ngIf="!bookForm.get('publishedYear').valid && bookForm.get('publishedYear').touched">Please enter Published Year</span>
        </mat-error>
      </mat-form-field>
      <div class="button-row">
        <button type="submit" [disabled]="!bookForm.valid" mat-flat-button color="primary"><mat-icon>save</mat-icon></button>
      </div>
    </form>
  </mat-card>
</div>

Finally, open and edit `src/app/book-edit/book-edit.component.scss` then add this lines of SCSS 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;
}


Step #6: Deploy, Run, and Test the Angular Universal SSR on VPS

Before installing PM2 and Nginx on your VPS, make sure you have installed the latest recommended Node.js and NPM  on your VPS.  After that, install PM2 using this command.

sudo npm install pm2@latest -g

Next, build the Angular Universal application as SSR.

npm run build:ssr --prod

Next, upload the angular-ssr/dist/angular-ssr/server/main.js to your VPS then move it to /var/www/. Then run this Angular SSR application using PM2.

pm2 start /var/www/main.js --name angular-ssr

And here's the PM2 status look like.

Angular 10 Universal Server Side Rendering (SSR) CRUD Example - pm2 status

To stop the PM2 type this command.

pm2 stop angular-ssr

Next, to make the PM2 start and run the app every time server starting type this command.

sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu

`ubuntu` is your server username. To check the status of the startup system, type this command.

systemctl status pm2-ubuntu

You can see this log or output console of the PM2 service.

Angular 10 Universal Server Side Rendering (SSR) CRUD Example - Nginx status

Next, install Nginx for your VPS operating system then edit the nginx.conf or /etc/nginx/sites-available/default file.

sudo nano /etc/nginx/sites-available/default

Replace the location body with these lines.

location / {
    proxy_pass http://localhost:4000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
}

Restart your Nginx server.

sudo service nginx restart

Now, open your browser then go to http://your_ip_address (e.g: http://192.168.0.7), and here's the application look like.

Angular 10 Universal Server Side Rendering (SSR) CRUD Example - demo 1
Angular 10 Universal Server Side Rendering (SSR) CRUD Example - demo 2
Angular 10 Universal Server Side Rendering (SSR) CRUD Example - demo 3

If there's an error caused by CORS because of using the application using port 80 instead of its origin port 4000. Just install the cors module.

npm install cors @types/cors

Open and edit server.ts then add this import.

import * as cors from 'cors';

Enable cors by adding this line after another server.use.

server.use(cors());

Also, change the API URL in src/app/api.service.ts to this.

const apiUrl = 'http://192.168.0.7/book';

Rebuild again this Angular 10 Universal application then deploy it to your VPS. Stop and start again PM2, and you will not see the CORS error.

That it's, the Angular 10 Universal Server Side Rendering (SSR) CRUD Example. You can find full source code from 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!