Build Angular 8|9 Universal CRUD App with MongoDB SSR

By Digamber Rawat Last updated on
Step by step practical guide on building an Angular 8|9 Universal CRUD (Create, Read, Update, Delete) operation web application with MongoDB Server-side Rendering (SSR).

All the final code of this tutorial can be found by clicking on the below button on my Github repository:

Git Repo

Server-side Rendering (SSR): Intro to Angular Universal

In short, Angular Universal is a pre-rendering solution for Angular 8|9. We know a regular single page application executes in the browser. All the pages render in the DOM in regards to the action taken by the user.

Whereas Angular Universal executes on the server, It generates static application pages, and that content gets bootstrapped on the client-side. This determines the faster application rendering even a user can view the app layout before it becomes fully interactive.

Why Use Angular Universal?

Now, you must be thinking. Why do we need to render an Angular app on the server? Well, executing Angular on the server has some exceptional benefits.

Supports SEO in Angular

It offers “Search Engine Optimization” in Angular, we all know Angular apps are highly dependent on JavaScript. Most of the search engines encounter problems executing the JavaScript they even get into the trouble executing the application content. To make the Angular app SEO friendly, we render our apps on the server. It assists crawler to know what HTML page in an Angular app to index efficiently.

Check out the following Angular 8/9 SEO Tutorial – How to Set Page Title and Meta Description in Angular Universal App Dynamically?

Angular Universal Helps web crawlers (SEO)

When we share any relevant post or content on social media search engine crawlers start looking for titles and description of that content but as i mentioned earlier search engine crawlers ignore JavaScript. For instance, social media sites that use content scrapers are Twitter, Facebook, Reddit and much more.

Better Performance on Mobile Devices

Nowadays, user experience is the key to success, and most of the users visit mobile sites, but there is a downside in a few devices. These devices do not execute or support JavaScript. To enhance performance on mobile devices, we can use server-side-rendering.

Enhance User Experience

Angular universal helps in showing the primitive page instantly with first-contentful paint (FCP). If server-side rendering is implemented correctly, then the Angular app displays a web page immediately. It happens because it contains HTML. As I have said above a regular Angular app is bootstrapped, before anything can be exposed to the user by taking more time.

Add Angular Universal in New Angular 8|9 App

Now we understood the basics, let us start coding the application. For the demo purpose, we will create a basic Angular universal 8/9 CRUD music app with MongoDB Server-side rendering.

In this Angular universal SSR tutorial, we will also learn to create RESTful APIs with Express.js. These APIs will allow us to create, read, update and delete songs data and store that data in the MongoDB database.

The following frameworks, tools, and packages will be used for this tutorial:

  • Node (Latest version)
  • Angular 8/9
  • Angular Universal (SSR)
  • MongoDB (Database)
  • Express
  • Body-parser
  • Mongoose
  • Terminal
  • Text Editor or IDE
  • Postman (API Testing)

Quickly, run the command to create a new Angular 8|9 app and get inside the project:

ng new angular-universal-crud && cd angular-universal-crud

# ? Would you like to add Angular routing? Yes
# ? Which stylesheet format would you like to use? CSS

We can use Bootstrap UI components to build music app if you don’t want to use Bootstrap you can skip this step as well.

npm install bootstrap

Insert the bootstrap.min.css path inside the styles array in the package.json:

"styles": [
      "node_modules/bootstrap/dist/css/bootstrap.min.css",
      "src/styles.css"
]

Next, run the cmd to add Angular Universal Server-side Rendering (SSR) in an Angular 8/9 project.

ng add @nguniversal/express-engine --clientProject angular-universal-crud

Run following cmd to verify Angular Universal SSR app installation:

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

Now, open the browser and go to http://localhost:4000/ to check the Angular Universal SSR app.

Install Third-party Packages in Angular SSR

Now, install the mongoose package to store and fetch the songs data from the MongoDB database. Plus, install the body-parser module and it helps in parsing the request body to the API.

npm install mongoose body-parser --save

Define Mongoose Schema

Next, define the Mongoose schema or model which outlines input fields in the MongoDB database. Create models folder and a song.ts file and declare the input fields and collection name in it:

// models/song.ts

import mongoose, { Schema } from 'mongoose';

let SongSchema: Schema = new Schema({
    name: {
        type: String
    },
    artist: {
        type: String
    }
}, {
    collection: 'songs'
})

export default mongoose.model('Song', SongSchema);

Create REST APIs using Express

Next, create REST APIs using Express Router and Mongoose schema. It helps in accessing the data in MongoDB using REST APIs. Create routes folder and a song-route.ts file in it:

// routes/song-route.ts

import { Request, Response, NextFunction } from 'express';
import Song from '../models/song';


export class SongRoute {
    songRoute(app): void {

        // Create Song
        app.route('/api/create-song').post((req: Request, res: Response, next: NextFunction) => {
            Song.create(req.body, (error, data) => {
                if (error) {
                    return next(error)
                } else {
                    res.json(data)
                }
            })
        });

        // Get All Songs
        app.route('/api/get-songs').get((req: Request, res: Response, next: NextFunction) => {
            Song.find((error, data) => {
                if (error) {
                    return next(error)
                } else {
                    res.json(data)
                }
            })
        })

        // Get Single Song
        app.route('/api/get-song/:id').get((req: Request, res: Response, next: NextFunction) => {
            Song.findById(req.params.id, (error, data) => {
                if (error) {
                    return next(error)
                } else {
                    res.json(data)
                }
            })
        })


        // Update Song
        app.route('/api/update-song/:id').put((req: Request, res: Response, next: NextFunction) => {
            Song.findByIdAndUpdate(req.params.id, {
                $set: req.body
            }, (error, data) => {
                if (error) {
                    return next(error);
                } else {
                    res.json(data)
                    console.log('Data updated successfully')
                }
            })
        })

        // Delete Song
        app.route('/api/delete-song/:id').delete((req: Request, res: Response, next: NextFunction) => {
            Song.findOneAndRemove(req.params.id, (error, data) => {
                if (error) {
                    return next(error);
                } else {
                    res.status(200).json({
                        msg: data
                    })
                }
            })
        })

    }
}

Configure MongoDB Database, PORT, Express API in Server.ts

Next, add the following code inside the server.ts file to configure MongoDB Database, Angular Universal SSR PORT and Express APIs. Add the following code inside the server.ts file:

// server.ts

/**
 * *** NOTE ON IMPORTING FROM ANGULAR AND NGUNIVERSAL IN THIS FILE ***
 *
 * If your application uses third-party dependencies, you'll need to
 * either use Webpack or the Angular CLI's `bundleDependencies` feature
 * in order to adequately package them for use on the server without a
 * node_modules directory.
 *
 * However, due to the nature of the CLI's `bundleDependencies`, importing
 * Angular in this file will create a different instance of Angular than
 * the version in the compiled application code. This leads to unavoidable
 * conflicts. Therefore, please do not explicitly import from @angular or
 * @nguniversal in this file. You can export any needed resources
 * from your application's main.server.ts file, as seen below with the
 * import for `ngExpressEngine`.
 */

import 'zone.js/dist/zone-node';

import * as express from 'express';
import { join } from 'path';
import * as mongoose from 'mongoose';
import bodyParser from 'body-parser';
import { SongRoute } from './routes/song-route';

const songRoute: SongRoute = new SongRoute();

// Express Server
const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist/browser');

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap } = require('./dist/server/main');

// MongoDB database settings
mongoose.connect('mongodb://localhost/song-db', {
  useNewUrlParser: true,
  useFindAndModify: false,
  useUnifiedTopology: true
})
  .then(() => console.log('Database connected successfully!'))
  .catch((err) => console.error(err));

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

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

app.set('view engine', 'html');
app.set('views', DIST_FOLDER);

// Example Express Rest API endpoints
// app.get('/api/**', (req, res) => { });
// Serve static files from /browser
app.get('*.*', express.static(DIST_FOLDER, {
  maxAge: '1y'
}));

songRoute.songRoute(app);

// All regular routes use the Universal engine
app.get('*', (req, res) => {
  res.render('index', { req });
});

// Start up the Node server
app.listen(PORT, () => {
  console.log(`Server connected on http://localhost:${PORT}`);
});

Test Angular 8|9 Universal SSR REST APIs with Postman

In this step, we will learn to test Angular 8/9 universal SSR REST APIs with the Postman app.

First, open the terminal and run the following command to start the mongoDb:

mongod

Let’s start the Angular universal project using below command in another terminal:

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

Here are the REST APIs we have created in our Angular SSR app and the base path begins with `/api/` that will be called from the Angular 8 /9 app using absolute URL http://localhost:4000/api/.

Methods REST API
POST Create Song /api/create-song
GET Get All Songs /api/get-songs
GET Get Single Song /api/get-song/:id
PUT Update Song /api/update-song/:id
DELETE Delete Song /api/delete-song/:id

Start the Postman app and set the HTTP method to POST and insert the `http://localhost:4000/api/create-song` API to create the song. Then, choose the body tab from the options and next select the JSON data type from the dropdown menu.

Angular 8|9 Universal SSR

As you can see, we can create a song using the REST API we have just created. The same way you can test GET, PUT and DELETE API. All you have to do just change the HTTP method and the API URL in the Postman app.

Create Components and Define Angular Routes

To manage the CRUD operations, we need to create the following components. We have two modules so we will be using `--skip-import` attribute to avoid the conflict.

ng g c components/songs --skip-import
ng g c components/add-article --skip-import
ng g c components/edit-article --skip-import

Now, we have to import and register these components in app/app.module.ts file manually:

import { AddSongComponent } from '../app/components/add-song/add-song.component';
import { EditSongComponent } from '../app/components/edit-song/edit-song.component';
import { SongsComponent } from '../app/components/songs/songs.component';

@NgModule({
  declarations: [
    AddSongComponent,
    EditSongComponent,
    SongsComponent
  ],
  imports: [...],
  providers: [...],
  bootstrap: [...]
})

export class AppModule { }

Then, go to `/app/app-routing.module.ts` file and add the following code.

// src/app-routing.module.ts

import { NgModule } from '@angular/core';
import { AddSongComponent } from '../app/components/add-song/add-song.component';
import { EditSongComponent } from '../app/components/edit-song/edit-song.component';
import { SongsComponent } from '../app/components/songs/songs.component';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    redirectTo: '/add-song',
    pathMatch: 'full'
  },
  {
    path: 'add-song',
    component: AddSongComponent,
    data: { title: 'Add Song' }
  },
  {
    path: 'edit-song/:id',
    component: EditSongComponent,
    data: { title: 'Edit Song' }
  },
  {
    path: 'songs',
    component: SongsComponent,
    data: { title: 'Songs' }
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})

export class AppRoutingModule { }

Next, go to app.component.html and add the following code to enable the routing service in Angular universal app:

// app/app.component.html

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" routerLink="/add-song">Angular Universal SSR</a>

  <div class="collapse navbar-collapse" id="navbarText">
    <ul class="navbar-nav ml-auto">
      <li class="nav-item">
        <button type="button" routerLink="/add-song" class="btn btn-outline-primary">Add Song</button>
      </li>
      <li class="nav-item active">
        <button type="button" routerLink="/songs" class="btn btn-danger">View Songs</button>
      </li>
    </ul>
  </div>
</nav>

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

Create Angular 8/9 Service with HttpClient to Consume REST APIs

To handle REST APIs we need to create service in Angular 8|9 app. First, create a shared folder and create song.ts file in it and add the following code to define the song data type.

// app/shared/song.ts

export class Song {
    name: string;
    artist: string;
}

Next, import and register HttpClient and Reactive Forms services in AppModule.

// app/app.module.ts

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


@NgModule({
  declarations: [...],
  imports: [
    ReactiveFormsModule,
    FormsModule,
    HttpClientModule
  ],
  providers: [...],
  bootstrap: [...]
})

export class AppModule { }

Next, run the command to generate song service in shared folder:

ng g service shared/song

Then, add the following code inside shared/song.service.ts file:

import { Injectable } from '@angular/core';
import { Song } from './song';
import { Observable, of } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { HttpClient, HttpHeaders } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})

export class SongService {

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

  constructor(
    private http: HttpClient
  ) { }

  addSong(song: Song): Observable<any> {
    return this.http.post<Song>('/api/create-song', song, this.httpOptions)
      .pipe(
        catchError(this.handleError<Song>('Add Song'))
      );
  }

  getSongs(): Observable<Song[]> {
    return this.http.get<Song[]>('/api/get-songs')
      .pipe(
        tap(songs => console.log('Songs retrieved!')),
        catchError(this.handleError<Song[]>('Get Songs', []))
      );
  }

  getSong(id): Observable<Song[]> {
    return this.http.get<Song[]>('/api/get-song/' + id)
      .pipe(
        tap(_ => console.log(`Song retrieved: ${id}`)),
        catchError(this.handleError<Song[]>(`Get Song id=${id}`))
      );
  }

  updateSong(id, song: Song): Observable<any> {
    return this.http.put('/api/update-song/' + id, song, this.httpOptions)
      .pipe(
        tap(_ => console.log(`Song updated: ${id}`)),
        catchError(this.handleError<Song[]>('Update Song'))
      );
  }

  deleteSong(id): Observable<Song[]> {
    return this.http.delete<Song[]>('/api/delete-song/' + id, this.httpOptions)
      .pipe(
        tap(_ => console.log(`Song deleted: ${id}`)),
        catchError(this.handleError<Song[]>('Delete Song'))
      );
  }


  private handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {
      // TODO: send the error to remote logging infrastructure
      console.error(error);
      // TODO: better job of transforming error for user consumption
      console.log(`${operation} failed: ${error.message}`);
      // Let the app keep running by returning an empty result.
      return of(result as T);
    };
  }

}

Add Song with Angular 8|9 Bootstrap

To add a song, we will be using the Bootstrap form component, go to app/components/add-song.component.html, and include the given below code in it.

// components/add-song.component.html

<form [formGroup]="songForm" (ngSubmit)="submit()">
    <div class="form-group">
        <label>Song</label>
        <input type="text" class="form-control" formControlName="name" required="required" />
    </div>
    <div class="form-group">
        <label>Artist</label>
        <input type="text" class="form-control" formControlName="artist" required="required" />
    </div>
    <button type="submit" class="btn btn-primary btn-block">Add Song</button>
</form>

Now, go to app/components/add-song.component.ts, and add the following code inside of it.

// components/add-song.component.ts

import { Component, OnInit } from '@angular/core';
import { SongService } from '../../shared/song.service';
import { FormGroup, FormBuilder } from "@angular/forms";

@Component({
  selector: 'app-add-song',
  templateUrl: './add-song.component.html',
  styleUrls: ['./add-song.component.css']
})

export class AddSongComponent implements OnInit {
  songForm: FormGroup;

  constructor(
    private songService: SongService,
    public fb: FormBuilder
  ) {
    this.form()
  }

  ngOnInit() { }

  form() {
    this.songForm = this.fb.group({
      name: [''],
      artist: ['']
    })
  }

  submit() {
    if (!this.songForm.valid) {
      return false;
    } else {
      this.songService.addSong(this.songForm.value)
        .subscribe((res) => {
          console.log(res)
          this.songForm.reset();
        })
    }
  }
}

Display and Delete Song Details in Angular Universal

To display and delete song details, go to components/songs/songs.component.html, and include the given below code.

<table class="table">
    <thead class="table-primary">
        <tr>
            <th scope="col">#</th>
            <th scope="col">Song name</th>
            <th scope="col">Artist name</th>
            <th scope="col">Action</th>
        </tr>
    </thead>
    <tbody>
        <tr *ngFor="let song of Songs">
            <th scope="row">{{song._id}}</th>
            <td>{{song.name}}</td>
            <td>{{song.artist}}</td>
            <td>
                <span class="edit" [routerLink]="['/edit-song/', song._id]">Edit</span>
                <span class="delete" (click)="removeSong(song, i)">Delete</span>
            </td>
        </tr>
    </tbody>
</table>

Next, go to components/songs/songs.component.ts and add the following code.

import { Component, OnInit } from '@angular/core';
import { SongService } from '../../shared/song.service';

@Component({
  selector: 'app-songs',
  templateUrl: './songs.component.html',
  styleUrls: ['./songs.component.css']
})

export class SongsComponent implements OnInit {
  Songs: any = [];

  constructor(private songService: SongService) {
    this.songService.getSongs().subscribe((item) => {
      this.Songs = item;
    });
  }

  ngOnInit() { }

  removeSong(employee, i) {
    if (window.confirm('Are you sure?')) {
      this.songService.deleteSong(employee._id)
        .subscribe((res) => {
          this.Songs.splice(i, 1);
        }
        )
    }
  }
}

Angular 8|9 SSR Edit Data with Bootstrap

We added the edit-song url in song details table, it redirects to edit-song template. Next, go to components/edit-song/edit-song.component.html, and include the given below code.

<form [formGroup]="updateSongForm" (ngSubmit)="updateSong()">
    <div class="form-group">
        <label>Song name</label>
        <input type="text" class="form-control" formControlName="name" required="required" />
    </div>
    <div class="form-group">
        <label>Artist name</label>
        <input type="text" class="form-control" formControlName="artist" required="required" />
    </div>
    <button type="submit" class="btn btn-primary btn-block">Update Song</button>
</form>

Next, go to components/edit-song/edit-song.component.ts and add the following code.

import { Component, OnInit } from '@angular/core';
import { SongService } from '../../shared/song.service';
import { FormGroup, FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";

@Component({
  selector: 'app-edit-song',
  templateUrl: './edit-song.component.html',
  styleUrls: ['./edit-song.component.css']
})

export class EditSongComponent implements OnInit {
  updateSongForm: FormGroup;

  constructor(
    private songService: SongService,
    private actRoute: ActivatedRoute,
    private router: Router,
    public fb: FormBuilder
  ) { }

  ngOnInit() {
    this.songForm();
    let id = this.actRoute.snapshot.paramMap.get('id');
    this.updateSongForm = this.fb.group({
      name: [''],
      artist: ['']
    })
    this.showEmp(id)
  }

  showEmp(id) {
    this.songService.getSong(id).subscribe((res) => {
      this.updateSongForm.setValue({
        name: res['name'],
        artist: res['artist']
      })
    })
  }

  songForm() {
    this.updateSongForm = this.fb.group({
      name: [''],
      artist: ['']
    })
  }

  updateSong() {
    if (!this.updateSongForm.valid) {
      return false;
    } else {
      let id = this.actRoute.snapshot.paramMap.get('id');
      this.songService.updateSong(id, this.updateSongForm.value)
        .subscribe(() => {
          this.router.navigateByUrl('/songs');
          console.log('Content updated successfully!')
        })
    }
  }
}

Conclusion

Finally, we have developed an Angular 8|9 Universal CRUD App with MongoDB Server-side Rendering. In this tutorial, we talked about why Angular SSR is beneficial. I highlighted the positive points, such as how it impacts performance on large and small devices, social media integration, web crawlers for SEO, and faster loading time.

Plus, we have learned how to build a simple Angular universal app with Node, Express, and MongoDB. I hope you have learned a lot from this tutorial.

Digamber Rawat
Digamber Rawat

Full stack developer with a passion for UI/UX design. I create beautiful and useful digital products to solve people’s problem and make their life easy.

Thanks for checking this tutorial out :) Let me know if you have any suggestions or issue related to this tutorial, or you want to request a new tutorial. Just shoot me a mail here.