A comprehensive step by step tutorial to building a full CRUD Angular 8 Universal and MongoDB Server-side Rendering (SSR)

We will practically use Angular 8 universal for a CRUD (Create, Read, Update, Delete) operation application that store the data to the MongoDB server.

The Angular 8 application that we will build is a very simple application about Article CRUD. This might be a starting point to build your own complete blog engine or news engine because this application should be matched for SEO requirements.

Almost all codes in this application using Typescript and ES6 except a few files that still use plain ES5 code that uses by Express.js. For that, we need your suggestion for converting to working Angular 8/Typescript code styles. The following tools, frameworks, and modules are required for this tutorial:

As usual, before we move forward to the main steps of Angular 8 Universal and MongoDB Server-side Rendering (SSR). We have to check the latest Node and Angular versions then update them if necessary. Just type this command to see it.

ng version

You will see the currently installed Node, Angular and Angular CLI versions.

Angular CLI: 8.0.6
Node: 10.15.1
OS: darwin x64
Angular: 8.0.3

Create a New Angular 8 Application and Add Angular Universal

First, we will create a new Angular 8 application by type this command in the terminal.

ng new article-crud

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

ng add @nguniversal/express-engine --clientProject article-crud

That command just added and updated a few files.

CREATE src/main.server.ts (361 bytes)
CREATE src/app/app.server.module.ts (427 bytes)
CREATE tsconfig.server.json (204 bytes)
CREATE webpack.server.config.js (1466 bytes)
CREATE server.ts (1980 bytes)
UPDATE package.json (1844 bytes)
UPDATE angular.json (4263 bytes)
UPDATE src/main.ts (432 bytes)
UPDATE src/app/app.module.ts (438 bytes)

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

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

Now, after go to localhost:4000 you see this standard Angular application page and feel the difference with the page that rendered in the browser.

Angular 8 Universal

Install and Configure Mongoose.js and Required Modules

To store and retrieve data from the MongoDB server, we will use Mongoose.js. To install it, just run these commands that including body-parser modules.

npm install --save mongoose body-parser

In this Angular 8 Universal SSR, body parser will use to parse the request body to the API. Next, open and edit server.ts in the root project folder then add these imports.

import * as mongoose from 'mongoose';
import bodyParser from "body-parser";

Next, add these lines of codes after the imports lines to connect to the MongoDB and use body-parser for every JSON requests.

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

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

Create a Mongoose Model and Router

We will create a Mongoose model that represent MongoDB fields. Create a models folder and a file to holds all required Article fields.

mkdir models
touch models/article.ts

Next, fill that file with this Mongoose Schema.

import mongoose, { Schema } from 'mongoose';

const ArticleSchema: Schema = new 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 }
});

export default mongoose.model('Article', ArticleSchema);

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/article-route.ts

Open and edit that file then add these imports.

import { Request, Response, NextFunction } from 'express';
import Article from '../models/article';

Add these lines of codes to create a CRUD method of RESTful API. The

route path starts with /api/ that will be called from the Angular 8

application using full URL [http://localhost:4000/api/.](http://localhost:4000/api/`.)

export class ArticleRoute {

  public articleRoute(app): void {
    app.route('/api/').get((req: Request, res: Response, next: NextFunction) => {
      Article.find((err, articles) => {
        if (err) { return next(err); }
        res.json(articles);
      });
    });

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

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

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

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

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

import { ArticleRoute } from './routes/article-route';

Declare a constant after the import lines.

const articleRoute: ArticleRoute = new ArticleRoute();

Add this line before the all regular route lines.

articleRoute.articleRoute(app);

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

Test the Angular 8 Universal SSR RESTful API

We will test the Angular 8 Universal SSR RESTful API using the Postman app. First, run the app using this command.

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

Open the postman app then change the method to GET and address to localhost:4000/api/ then press SEND button. If there’s any data in your MongoDB server then it’s will be similar like this.

MongoDB SSR

To save data, change the method to POST with the same address then choose RAW body with type JSON. Fill the RAW body with this example or your own data example.

{
    "title": "Odong",
    "author": "Odong",
    "description": "Odong",
    "content": "Odong"
}

You will see this result for successful POST data.

CRUD Angular 8

Next, you can test the rest of PUT and DELETE using your own data to make sure the Angular 8 Universal SSR API working.

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 --skip-import
ng g component show-article --skip-import
ng g component add-article --skip-import
ng g component edit-article --skip-import

There’s an additional --skip-import variable because there will be a conflict between 2 modules, Angular 8 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 to that file.

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

Then add those components to @NgModule declaration.

@NgModule({
  declarations: [
    ...
    ArticlesComponent,
    ShowArticleComponent,
    AddArticleComponent,
    EditArticleComponent
  ],
  ...
})

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

const routes: Routes = [
  {
    path: 'articles',
    component: ArticlesComponent,
    data: { title: 'Articles' }
  },
  {
    path: 'show-article/:id',
    component: ShowArticleComponent,
    data: { title: 'Show Article' }
  },
  {
    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.


  

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 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/";

Inject HttpClient module to the constructor.

constructor(private http: HttpClient) { }

Add the error handler function.

private handleError(operation = 'operation', result?: T) {
  return (error: any): Observable => {
    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 {
  return this.http.get(apiUrl)
    .pipe(
      tap(articles => console.log('fetched Articles')),
      catchError(this.handleError('getArticles', []))
    );
}

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

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

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

deleteArticle(id: any): Observable {
  const url = `${apiUrl}/${id}`;
  return this.http.delete(url, httpOptions).pipe(
    tap(_ => console.log(`deleted Article id=${id}`)),
    catchError(this.handleError('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 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-loading-shade"
       *ngIf="isLoadingResults">
    
  
  
    add
  
  
    <table mat-table [dataSource]="data" class="example-table"
           matSort matSortActive="title" matSortDisableClear matSortDirection="asc">

      
      
        Title
        {{row.title}}
      

      
      
        Author
        $ {{row.author}}
      

      
      
    
  

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-loading-shade"
       *ngIf="isLoadingResults">
    
  
  
    list
  
  
    
      ## {{article.title}}

      {{article.author}} | {{article.updatedAt}}
    
    
      ### {{article.description}}

      
{{article.content}}

    
    
      edit
      delete
    
  

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 a 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';

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;

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, add this import for implementing ErrorStateMatcher.

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

Instantiate that MyErrorStateMatcher as a variable in main class.

matcher = new MyErrorStateMatcher();

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


  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    
  
  
    list
  
  
    
      
        <input matInput placeholder="Title" formControlName="title"
               [errorStateMatcher]="matcher">
        
          Please enter Title
        
      
      
        <input matInput placeholder="Author" formControlName="author"
               [errorStateMatcher]="matcher">
        
          Please enter Author
        
      
      
        <input matInput placeholder="Description" formControlName="description"
               [errorStateMatcher]="matcher">
        
          Please enter Description
        
      
      
        <textarea matInput placeholder="Content" formControlName="content"
               [errorStateMatcher]="matcher">
        
          Please enter Content
        
      
      
        save
      
    
  

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

Inject 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 for the article form before the constructor.

articleForm: FormGroup;
_id = '';
title = '';
author = '';
description = '';
content = '';
isLoadingResults = false;

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, add this import for implementing ErrorStateMatcher.

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

Instantiate that MyErrorStateMatcher as a variable in main class.

matcher = new MyErrorStateMatcher();

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


  <div class="example-loading-shade"
       *ngIf="isLoadingResults">
    
  
  
    info
  
  
    
      
        <input matInput placeholder="Title" formControlName="title"
               [errorStateMatcher]="matcher">
        
          Please enter Title
        
      
      
        <input matInput placeholder="Author" formControlName="author"
               [errorStateMatcher]="matcher">
        
          Please enter Author
        
      
      
        <input matInput placeholder="Description" formControlName="description"
               [errorStateMatcher]="matcher">
        
          Please enter Description
        
      
      
        <textarea matInput placeholder="Content" formControlName="content"
               [errorStateMatcher]="matcher">
        
          Please enter Content
        
      
      
        save
      
    
  

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

Test and Run Angular 8 Universal and MongoDB Server-side Rendering (SSR)

It’s time to test and try the performance of Angular 8 Universal and MongoDB Server-side Rendering (SSR). First, run the MongoDB server in the different terminal or command line.

mongod

Next, run the Angular 8 Universal SSR in the current terminal and project folder.

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

After build finished for both client and server then open the

application in the browser [http://localhost:4200/](http://localhost:4200/`) and you will see and

feel this Angular 8 application as below.

That it’s, the example of Angular 8 Universal and MongoDB Server-side Rendering (SSR). You can get the full source codes from our Github.

#angular #mongodb #web-development #node-js #database

Building a full CRUD Angular 8 Universal and MongoDB SSR
266.30 GEEK