Setting up your new Mac for MEAN Stack development

Setting up your new Mac for MEAN Stack development

In this article, we look at how you can properly setup and configure your new Mac for MEAN Stack Development. Note that this is just one of the many ways, although this has been a tried and tested one, and has worked pretty well for me.

In this article, we look at how you can properly setup and configure your new Mac for MEAN Stack Development. Note that this is just one of the many ways, although this has been a tried and tested one, and has worked pretty well for me.

The Basics: System Update

This part, although pretty obvious, is in fact one of the most important parts of setting up your system. Ensure that your OS and other system software are updated to the latest version. This bit of the setup process is fairly easy too. Assuming that you are running macOS Mojave, follow these steps to get everything up to date:

  1. Choose System Preferences from the Apple () menu, then click Software Update to check for updates.
  2. If any updates are available, click the Update Now button to install them. Or click ”More info” to see details about each update and select specific updates to install.
  3. When Software Update says that your Mac is up to date, macOS and all of its apps are also up to date.

To automatically install macOS updates in the future, including apps downloaded from the App Store, select ”Automatically keep my Mac up to date”. Personally, I recommend this since it will help keep your ssytem up to date even if you forget to check for updates regularly.

Time for Node

Now that your system is up to date, it is time to get the tools for development up and ready and the most fundamental one you will need are Node and the Node Pachkage Manager(NPM). The ’N’ in MEAN(or MERN, for that matter), is the Javascript runtime over which all our Angular and React applications run. NPM meanwhile is a great tool that will help you to — you guessed it right, maange the multitude of packaged you’ll require and work with. There is more than one way to setup Node on your system. but the first thing to do is check if your system by any chance already ahs an installation of Node. This can be done as follows:

  1. Open the Terminal
  2. Enter node -v
  3. If you have Node on your system, it will show the version
  4. Update to the latest version using npm i -g npm. If you get a bunch of checkPermissionswarnings, you should run the command as the superuser like this: sudo npm i -g npm

Assuming that you do not have Node already installed, proceed with one of the following methods:

The Installer

If you wish to go old school and keep it simple and neat, this is the way for you.

  1. Go to https://nodejs.org/
  2. On this page, under normal circumstances, you’ll be presented with links to the installer. You have two options here: the Current version, and the Long Term Support(LTS) version. While both are mostly same, the Current Version will have a few newer features although it may have bugs, in comparison to the LTS.
  3. Download the installer, locate it in your Finder and install Node on your system by following the on-screen instructions.
  4. Once done, verify the installation by running node -v once again. You should get a version number now.

The Homebrew way

If you prefer installing Node through the Terminal, then Homebrew is what you’ll need. As stated on their website(https://brew.sh/), Homebrew is ‘teh missing package manager for macOS’. Basically, they make the process of installing stuff as simple as running a line of command or two. What’s more, they also do away with requiring us to affix sudo with the commands we run in installations etc.

First, you need to install Homebrew and this can be done by running the following command on Termnal:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Note that you may have to install Xcode to ensure that Homebrew works properly. SO if you are prompted to do so, go to the App Store and install Xcode. This might seem like a lot to go through while we can easily install Node through the Terminal. But as a developer, there will come a time sooner than later when you’ll realise that installing Homebrew was a good decision.

Once Homebrew installation completes, all you have to do is run the following command to install Node:

brew install node

And you’re done. Just to be sure, run node -v to verify that the things are in order.

So far, so good? Cool, it gets better.

On to the Database

Now that Node is in place, let’s setup the ‘M’ of MEAN, MongoDB on your system. Here again, you have two ways to go.

Install and Run MongoDB with Homebrew

  • Open the Terminal app and type brew update.
  • After updating Homebrew brew install mongodb
  • After downloading Mongo, create the “db” directory. This is where the Mongo data files will live. You can create the directory in the default location by running mkdir -p /data/db
  • Make sure that the <strong>/data/db</strong> directory has the right permissions by runningsudo chown -R `id -un` /data/db
  • Run the Mongo daemon, in one of your terminal windows run mongod. This should start the Mongo server.
  • Run the Mongo shell, with the Mongo daemon running in one terminal, type mongo in another terminal window. This will run the Mongo shell which is an application to access data in MongoDB.
  • To exit the Mongo shell run quit()
  • To stop the Mongo daemon hit ctrl-c

Install and Run MongoDB by Downloading it Manually

  • **Go to the MongoDB website’s **download section and download the correct version of MongoDB.
  • After downloading Mongo move the gzipped tar file (the file with the extension .tgz that you downloaded) to the folder where you want Mongo installed. In this case, we’ll say that we want Mongo to live in our home folder, and so the commands might look something like this:

<em>mv mongodb-osx-x86_64-3.0.7.tgz ~/</em>> <em>cd Downloads</em>> <em>mv mongodb-osx-x86_64-3.0.7.tgz ~/</em>* cd ~/

  • tar -zxvf mongodb-osx-x86_64-3.0.7.tgz > mv mongodb-osx-x86_64-3.0.7 mongodb
  • Create the directory where Mongo will store data, create the “db” directory. ou can create the directory in the default location by running mkdir -p /data/db
  • Make sure that the <strong>/data/db</strong> directory has the right permissions by runningsudo chown -R `id -un` /data/db
  • Run the Mongo daemon, in one terminal window run ~/mongodb/bin/mongod. This will start the Mongo server.
  • Run the Mongo shell, with the Mongo daemon running in one terminal, type ~/mongodb/bin/mongo in another terminal window. This will run the Mongo shell which is an application to access data in MongoDB.
  • To exit the Mongo shell run quit()
  • To stop the Mongo daemon hit ctrl-c

If you neatly did either iof the above, then your local MongoDb should be good to go.

And then there is Angular

Right now, we have got an updated system, Node, NPm and MongoDB all setup and ready. Finally, it is time for Angular, the powerful javascript framework that completes the MEAN Stack and is making beautiful Single Page Applications a reality.

In comparison to what we have had till now, installing Angular is fairly easy. Open Terminal and run

sudo npm i -g @angular/cli

Enter your password, wait for the process to complete and voila! You are all set to get cracking or rather, developing.

Don’t forget Express

What is MEAN without the ‘E’? Express is the Node.js framework that has made Node.js one of the most preferred server-side languages over the past few years. While Express.js in itself is wprth an entire blog, I will keep this down to setting up Express in your project and that is as simple as running the following code in your project’s Node directory:

npm i express --save

A few more things

While you now have all the runtimes and frameworks ready, there’s more to setting up a system for development than that.

The Text Editor

This may sound silly but for any developer, the right text editor is what sets the right tone and mood to get the proverbial creative juices flowing. True, even something as measly as Notepad(or Notepad++) can do the trick. So can the ‘further along evolution-line’ alternatives like Sublime or Atom. But I strongly prefer Visual Studio Code. Why ?

  • It’s customizable
  • Easy Debugging
  • Extensions
  • Git Integration
  • Integrated terminal
  • Intellisense
  • Theming and more…

In short, VS Code is as good as an editor gets, or has gotten till date. It’s absolutely free and you can get it from https://code.visualstudio.com/

I’ll talk in detail about VS Code in another article.

Chrome DevTools

As someone who has been developing on Angular for some time now, I can tell you first hand that you are going to get a lot of what I call ‘white-screen errors’ — just like that, all you’ll get on the browser will be a blank white page, irrespective of how many lines of code or number of components you have in there. More often than not, this is a result of an unexpected closing tag in the HTML or even worse, a typo in the HTML. This is where the Console comes to rescue — kinda. It shows you a ‘Template Parse Error: Unexpected closing tag ’ or something similar, allowing us to fix the error with relative ease.

Chrome DevTools is a set of web developer tools built directly into the Google Chrome browser. DevTools can help you edit pages on-the-fly and diagnose problems quickly, which ultimately helps you build better websites, faster. The main tools that come with this are:

  • Device Mode is a one-stop solution to evaluate how responsive your website or application works across devices
  • Elements Panel allows you to examine the page in terms of the elements and styles that make it up, letting you tinker around and try out stuff rather than updating the code for every small change and waiting for the page to render again
  • Console Panel, where you get the log of all the processes going on as well as the output of various console.log() statements
  • Network Panel where you can view all the requests and responses from and to your application
  • And much more…

So you see, Safari is a great browser, no arguments there. But if you are a developer and are not using Googlr Chrome and Chrome DevTools, you are missing out on a lot.

nodemon

Node.js development can get really irksome when for every small change you make in the code, you ahve to stop the running server and start it over, again. This is where nodemon becomes a lifesaver.

nodemon is a tool that helps develop node.js based applications by automatically restarting the node application when file changes in the directory are detected.

nodemon does not require any additional changes to your code or method of development. nodemon is a replacement wrapper for node, to use nodemon replace the word node on the command line when executing your script. You can install nodemon on your system by running the following command:

npm i -g nodemon

Alright, those are the bare minimum things you need and/or should have to get up and running with MEAN Stack development on your new Mac. Remember — this is just the way I do it. Somebody else might do it differently and that could be a better approach too. So keep looking out for more things and stay ahead of the curve.

Thanks for reading ❤

Angular 8 Node & Express JS File Upload

Angular 8 Node & Express JS File Upload

In this Angular 8 and Node.js tutorial, we are going to look at how to upload files on the Node server. To create Angular image upload component, we will be using Angular 8 front-end framework along with ng2-file-upload NPM package; It’s an easy to use Angular directives for uploading the files.

In this Angular 8 and Node.js tutorial, we are going to look at how to upload files on the Node server. To create Angular image upload component, we will be using Angular 8 front-end framework along with ng2-file-upload NPM package; It’s an easy to use Angular directives for uploading the files.

We are also going to take the help of Node.js to create the backend server for Image or File uploading demo. Initially, we’ll set up an Angular 8 web app from scratch using Angular CLI. You must have Node.js and Angular CLI installed in your system.

We’ll create the local server using Node.js and multer middleware. Multer is a node.js middleware for handling multipart/form-data, which is primarily used for uploading files. Once we are done setting up front-end and backend for our File uploading demo then, we’ll understand step by step how to configure file uploading in Angular 8 app using Node server.

Prerequisite

In order to show you Angular 8 File upload demo, you must have Node.js and Angular CLI installed in your system. If not then check out this tutorial: Set up Node JS

Run following command to install Angular CLI:

npm install @angular/cli -g

Install Angular 8 App

Run command to install Angular 8 project:

ng new angular-node-file-upload

# ? Would you like to add Angular routing? No
# ? Which stylesheet format would you like to use? CSS
cd angular-node-file-upload

Show Alert Messages When File Uploaded

We are going to install and configure ngx-toastr an NPM package which helps in showing the alert message when the file is uploaded on the node server.

npm install ngx-toastr --save

The ngx-toastr NPM module requires @angular/animations dependency:

npm install @angular/animations --save

Then, add the ngx-toastr CSS in angular.json file:

"styles": [
    "src/styles.css",
    "node_modules/ngx-toastr/toastr.css"
]

Import BrowserAnimationsModule and ToastrModule in app.module.ts file:

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr';
 
@NgModule({
  imports: [
    CommonModule,
    BrowserAnimationsModule, // required animations module
    ToastrModule.forRoot() // ToastrModule added
  ]
})

export class AppModule { }

Install & Configure ng-file-upload Directive

In this step, we’ll Install and configure ng-file-upload library in Angular 8 app. Run command to install ng-file-upload library.

npm install ng2-file-upload

Once the ng2-file-upload directive is installed, then import the FileSelectDirective and FormsModule in app.module.ts. We need FormsModule service so that we can create the file uploading component in Angular.

import { FileSelectDirective } from 'ng2-file-upload';
import { FormsModule } from '@angular/forms';

@NgModule({
  declarations: [
    FileSelectDirective
  ],
  imports: [
    FormsModule
  ]
})

export class AppModule { }

Setting Up Node Backend for File Upload Demo

To upload the file on the server, we need to set up a separate backend. In this tutorial, we will be using Node & Express js to create server locally along with multer, express js, body-parser, and dotenv libraries.

Run command to create backend folder in Angular app’s root directory:

mkdir backend && cd backend

In the next step, create a specific package.json file.

npm init

Run command to install required dependencies:

npm install express cors body-parser multer dotenv --save

In order to get rid from starting the server again and again, install nodemon NPM package. Use –-save-dev along with the npm command to register in the devDependencies array. It will make it available for development purpose only.

npm install nodemon --save-dev

Have a look at final pacakge.json file for file upload demo backend:

{
  "name": "angular-node-file-upload",
  "version": "1.0.0",
  "description": "Angualr 8 file upload demo app",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "author": "Digamber Rawat",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.19.0",
    "cors": "^2.8.5",
    "dotenv": "^8.0.0",
    "express": "^4.17.1",
    "multer": "^1.4.1"
  },
  "devDependencies": {
    "nodemon": "^1.19.1"
  }
}

Create a file by the name of server.js inside backend folder:

Configure Server.js

To configure our backend we need to create a server.js file. In this file we’ll keep our backend server’s settings.

touch server.js

Now, paste the following code in backend > server.js file:

const express = require('express'),
  path = require('path'),
  cors = require('cors'),
  multer = require('multer'),
  bodyParser = require('body-parser');

// File upload settings  
const PATH = './uploads';

let storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, PATH);
  },
  filename: (req, file, cb) => {
    cb(null, file.fieldname + '-' + Date.now())
  }
});

let upload = multer({
  storage: storage
});

// Express settings
const app = express();
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
  extended: false
}));

app.get('/api', function (req, res) {
  res.end('File catcher');
});

// POST File
app.post('/api/upload', upload.single('image'), function (req, res) {
  if (!req.file) {
    console.log("No file is available!");
    return res.send({
      success: false
    });

  } else {
    console.log('File is available!');
    return res.send({
      success: true
    })
  }
});

// Create PORT
const PORT = process.env.PORT || 8080;
const server = app.listen(PORT, () => {
  console.log('Connected to port ' + PORT)
})

// Find 404 and hand over to error handler
app.use((req, res, next) => {
  next(createError(404));
});

// error handler
app.use(function (err, req, res, next) {
  console.error(err.message);
  if (!err.statusCode) err.statusCode = 500;
  res.status(err.statusCode).send(err.message);
});

Now, while staying in the backend folder run the below command to start the backend server:

nodemon server.js

If everything goes fine then you’ll get the following output:

[nodemon] 1.19.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node server.js`
Connected to port 8080

Create Angular 8 File Upload Component

In this last step, we are going to create a file upload component in Angular 8 app using Express js API.

Get into the app.component.ts file and include the following code:

import { Component, OnInit } from '@angular/core';
import { FileUploader } from 'ng2-file-upload/ng2-file-upload';
import { ToastrService } from 'ngx-toastr';

const URL = 'http://localhost:8080/api/upload';

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

export class AppComponent implements OnInit {
  public uploader: FileUploader = new FileUploader({
    url: URL,
    itemAlias: 'image'
  });

  constructor(private toastr: ToastrService) { }

  ngOnInit() {
    this.uploader.onAfterAddingFile = (file) => {
      file.withCredentials = false;
    };
    this.uploader.onCompleteItem = (item: any, status: any) => {
      console.log('Uploaded File Details:', item);
      this.toastr.success('File successfully uploaded!');
    };
  }

}

Go to app.component.html file and add the given below code:

<div class="wrapper">
  <h2>Angular Image Upload Demo</h2>

  <div class="file-upload">
    <input type="file" name="image" ng2FileSelect [uploader]="uploader" accept="image/x-png,image/gif,image/jpeg" />
    <button type="button" (click)="uploader.uploadAll()" [disabled]="!uploader.getNotUploadedItems().length">
      Upload
    </button>
  </div>

</div>

Now, It’s time to start the Angular 8 app to check out the File upload demo in the browser. Run the following command:

ng serve --open

Make sure your NODE server must be running to manage the backend.

When you upload the image from front-end you’ll see your image files are saving inside the backend > uploads folder.

Conclusion

In this Angular 8 tutorial, we barely scratched the surface related to file uploading in a Node application. There are various other methods available on the internet through which you can achieve file uploading task quickly. However, this tutorial is suitable for beginners developers. I hope this tutorial will surely help and you if you liked this tutorial, please consider sharing it with others.

MongoDB, Express, Vue.js 2, Node.js (MEVN) and SocketIO Chat App

MongoDB, Express, Vue.js 2, Node.js (MEVN) and SocketIO Chat App

The comprehensive tutorial on MongoDB, Express, Vue.js 2, Node.js (MEVN) and SocketIO Chat Application

The comprehensive tutorial on MongoDB, Express, Vue.js 2, Node.js (MEVN) and SocketIO Chat Application

MEVN Tutorial: The comprehensive tutorial on MongoDB, Express, Vue.js 2, Node.js (MEVN) and SocketIO Chat Application. Previously we have a tutorial on build chat application using MEAN Stack, now we build this chat application using MEVN (MongoDB, Express.js, Vue.js 2, Node.js) Stack. The different just now we use Vue.js 2 and Axios, we keep using MongoDB, Node.js, Express, and Socket.io.

Table of Contents:
  • Create a New Vue.js 2 Application using Vue-CLI
  • Install Express.js as RESTful API Server
  • Install and Configure Mongoose.js
  • Create REST API for Accessing Chat Data
  • Create Mongoose.js Model for Room and Chat
  • Create Vue.js 2 Component and Routing
  • Add Module for RESTful API Access and Styling UI
  • Modify Vue.js 2 Component for Room List
  • Modify Vue.js 2 Component for Add Room
  • Modify Vue.js 2 Component for Join Room
  • Modify Vue.js 2 Component for Chat Room
  • Integrate Socket.io With Existing Non-Real-time Chat Application
  • Run and Test The Chat Application

The scenario is very simple, just the rooms and the chats for each room. The first page will show the list of the rooms. After the user enters the room and fills the username or nickname then the user enters the chats with other users.

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

  1. Node.js (use recommended version)
  2. Express.js
  3. MongoDB
  4. Mongoose.js
  5. Vue.js
  6. Vue-CLI
  7. Socket IO
  8. Axios
  9. Terminal (Mac/Linux) or Node.js Command Line (Windows)
  10. IDE or Text Editor (we use Atom)

We assume that you have already installed Node.js and able to run Node.js command line (Windows) or npm on the terminal (MAC/Linux). Open the terminal or Node command line then type this command to install vue-cli.

sudo npm install -g vue-cli

That where we start the tutorial. We will create the MEVN stack Chat application using vue-cli.

1. Create a New Vue.js 2 Application using Vue-CLI

To create a new Vue.js 2 application using vue-cli simply type this command from terminal or Node command line.

vue init webpack mevn-chat

There will be a lot of questions, just leave it as default by always pressing enter key. Next, go to the newly created Vue.js project folder then install all default required modules by type this command.

cd ./mevn-chat

Now, check the Vue.js 2 application by running the application using this command.

npm run dev

Open your browser then go to localhost:8080 and you should see this page when everything still on the track.

2. Install Express.js as RESTful API Server

Close the running Vue.js 2 app first by press ctrl+c then type this command for adding Express.js modules and its dependencies.

npm install --save express body-parser morgan body-parser serve-favicon

Next, create a new folder called bin then add a file called www on the root of the Vue.js project folder.

mkdir bin
touch bin/www

Open and edit www file then add these lines of codes that contains configuration for an HTTP server, PORT, and error handling.

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('mean-app:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

Next, change the default server what run by npm command. Open and edit package.json then replace startvalue inside scripts.

"scripts": {
  "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
  "start": "npm run build && node ./bin/www",
  "unit": "jest --config test/unit/jest.conf.js --coverage",
  "e2e": "node test/e2e/runner.js",
  "test": "npm run unit && npm run e2e",
  "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
  "build": "node build/build.js"
},

Next, create app.js in the root of project folder.

touch app.js

Open and edit app.js then add this lines of codes.

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var bodyParser = require('body-parser');

var room = require('./routes/room');
var chat = require('./routes/chat');
var app = express();

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({'extended':'false'}));
app.use(express.static(path.join(__dirname, 'dist')));
app.use('/rooms', express.static(path.join(__dirname, 'dist')));
app.use('/api/room', room);
app.use('/api/chat', chat);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

Next, create routes folder then create routes file for the room and chat.

mkdir routes
touch routes/room.js
touch routes/chat.js

Open and edit routes/room.js file then add this lines of codes.

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.send('Express RESTful API');
});

module.exports = router;

Do the same way with routes/chat.js. Now, run the server using this command.

npm start

You will see the previous Vue.js landing page when you point your browser to [http://localhost:3000](http://localhost:3000 "http://localhost:3000"). When you change the address to [http://localhost:3000/api/room](http://localhost:3000/api/room "http://localhost:3000/api/room") or [http://localhost:3000/api/chat](http://localhost:3000/api/chat "http://localhost:3000/api/chat") you will see this page.

3. Install and Configure Mongoose.js

We need to access data from MongoDB. For that, we will install and configure Mongoose.js. On the terminal type this command after stopping the running Express server.

npm install --save mongoose bluebird

Open and edit app.js then add this lines after another variable line.

var mongoose = require('mongoose');
mongoose.Promise = require('bluebird');
mongoose.connect('mongodb://localhost/mevn-chat', { promiseLibrary: require('bluebird') })
  .then(() =>  console.log('connection succesful'))
  .catch((err) => console.error(err));

Now, run MongoDB server on different terminal tab or command line or run from the service.

mongod

Next, you can test the connection to MongoDB run again the Node application and you will see this message on the terminal.

connection succesful

If you are still using built-in Mongoose Promise library, you will get this deprecated warning on the terminal.

(node:42758) DeprecationWarning: Mongoose: mpromise (mongoose's default promise library) is deprecated, plug in your own promise library instead: http://mongoosejs.com/docs/promises.html

That’s the reason why we added bluebird modules and register it as Mongoose Promise library.

4. Create Mongoose.js Model for Room and Chat

Add a models folder on the root of project folder for hold Mongoose.js model files then add Javascript file for Room and Chat.

mkdir models
touch models/Room.js
touch models/Chat.js

Next, open and edit models/Room.js then add this lines of codes.

var mongoose = require('mongoose'), Schema = mongoose.Schema;

var RoomSchema = new mongoose.Schema({
  room_name: String,
  created_date: { type: Date, default: Date.now },
});

module.exports = mongoose.model('Room', RoomSchema);

Open and edit models/Chat.js then add this lines of codes.

var mongoose = require('mongoose'), Schema = mongoose.Schema;

var ChatSchema = new mongoose.Schema({
  room : { type: Schema.Types.ObjectId, ref: 'Room' },
  nickname: String,
  message: String,
  created_date: { type: Date, default: Date.now },
});

module.exports = mongoose.model('Chat', ChatSchema);
5. Create Vue.js Component and Routing

Open and edit again routes/room.js then replace all codes with this.

var express = require('express');
var router = express.Router();
var mongoose = require('mongoose');
var Room = require('../models/Room.js');

/* GET ALL ROOMS */
router.get('/', function(req, res, next) {
  Room.find(function (err, products) {
    if (err) return next(err);
    res.json(products);
  });
});

/* GET SINGLE ROOM BY ID */
router.get('/:id', function(req, res, next) {
  Room.findById(req.params.id, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* SAVE ROOM */
router.post('/', function(req, res, next) {
  Room.create(req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* UPDATE ROOM */
router.put('/:id', function(req, res, next) {
  Room.findByIdAndUpdate(req.params.id, req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* DELETE ROOM */
router.delete('/:id', function(req, res, next) {
  Room.findByIdAndRemove(req.params.id, req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

module.exports = router;

Open and edit again routes/chat.js then replace all codes with this.

var express = require('express');
var router = express.Router();
var mongoose = require('mongoose');
var Chat = require('../models/Chat.js');

/* GET ALL CHATS */
router.get('/', function(req, res, next) {
  Chat.find(function (err, products) {
    if (err) return next(err);
    res.json(products);
  });
});

/* GET SINGLE CHAT BY ID */
router.get('/:id', function(req, res, next) {
  Chat.findById(req.params.id, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* SAVE CHAT */
router.post('/', function(req, res, next) {
  Chat.create(req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* UPDATE CHAT */
router.put('/:id', function(req, res, next) {
  Chat.findByIdAndUpdate(req.params.id, req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* DELETE CHAT */
router.delete('/:id', function(req, res, next) {
  Chat.findByIdAndRemove(req.params.id, req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

module.exports = router;

Run again the Express server then open the other terminal or command line to test the Restful API by type this command.

curl -i -H "Accept: application/json" localhost:3000/api/room

If that command return response like below then REST API is ready to go.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 2
ETag: W/"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w"
Date: Sun, 05 Aug 2018 13:11:30 GMT
Connection: keep-alive

[]

Now, let’s populate Room collection with initial data that sent from RESTful API. Run this command to populate it.

curl -i -X POST -H "Content-Type: application/json" -d '{ "room_name":"Javascript" }' localhost:3000/api/room

You will see this response to the terminal if success.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 109
ETag: W/"6d-OGpcih/JWvJGrYAhMP+KBYQOvNQ"
Date: Sun, 05 Aug 2018 13:35:50 GMT
Connection: keep-alive

{"_id":"5b66fd3581b9291558dc90b7","room_name":"Javascript","created_date":"2018-08-05T13:35:49.803Z","__v":0}

6. Create Vue.js 2 Component and Routing

Now, it’s time for Vue.js 2 or front end part. First, create or add the component of the room list, add a room, join a room, chat room. Create all of those files into the components folder.

touch src/components/RoomList.vue
touch src/components/AddRoom.vue
touch src/components/JoinRoom.vue
touch src/components/ChatRoom.vue

Now, open and edit src/router/index.js then add the import for all above new components.

import Vue from 'vue'
import Router from 'vue-router'
import RoomList from '@/components/RoomList'
import AddRoom from '@/components/AddRoom'
import JoinRoom from '@/components/JoinRoom'
import ChatRoom from '@/components/ChatRoom'

Add the router to each component or page.

export default new Router({
  routes: [
    {
      path: '/',
      name: 'RoomList',
      component: RoomList
    },
    {
      path: '/add-room',
      name: 'AddRoom',
      component: AddRoom
    },
    {
      path: '/join-room/:id',
      name: 'JoinRoom',
      component: JoinRoom
    },
    {
      path: '/chat-room/:id/:nickname',
      name: 'ChatRoom',
      component: ChatRoom
    }
  ]
})

7. Add Axios and Bootstrap-Vue

For UI or styling, we are using Bootstrap Vue. BootstrapVue use to build responsive, mobile-first projects on the web using Vue.js and the world's most popular front-end CSS library Bootstrap v4. To install Bootstrap-Vue type this command on the terminal.

npm i bootstrap-vue [email protected]

Open and edit src/main.js then add the imports for Bootstrap-Vue.

import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue'
import App from './App'
import router from './router'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Add this line after Vue.config.

Vue.use(BootstrapVue)

Next, we are using Axio for accessing REST API provided by Express.js. Axios is a promise-based HTTP client for the browser and node.js. To install it, in the terminal type this command.

npm install axios --save

8. Modify Vue.js 2 Component for Room List

Now, open and edit src/components/RoomList.vue then add this lines of codes.

<template>
  <b-row>
    <b-col cols="12">
      <h2>
        Room List
        <b-link href="#/add-room">(Add Room)</b-link>
      </h2>
      <b-table striped hover :items="rooms" :fields="fields">
        <template slot="actions" scope="row">
          <b-btn size="sm" @click.stop="join(row._id)">Join</b-btn>
        </template>
      </b-table>
      <ul v-if="errors && errors.length">
        <li v-for="error of errors">
          {{error.message}}
        </li>
      </ul>
    </b-col>
  </b-row>
</template>

<script>

import axios from 'axios'

export default {
  name: 'BookList',
  data () {
    return {
      fields: {
        room_name: { label: 'Room Name', sortable: true, 'class': 'text-center' },
        created_date: { label: 'Created Date', sortable: true },
        actions: { label: 'Action', 'class': 'text-center' }
      },
      rooms: [],
      errors: []
    }
  },
  created () {
    axios.get(`http://localhost:3000/api/room`)
    .then(response => {
      this.rooms = response.data
    })
    .catch(e => {
      this.errors.push(e)
    })
  },
  methods: {
    join (id) {
      this.$router.push({
        name: 'JoinRoom',
        params: { id: id }
      })
    }
  }
}
</script>

There are template and script in one file. The template block contains HTML tags. Script block contains variables, page lifecycle and methods or functions.

9. Modify Vue.js 2 Component for Add Room

Now, open and edit src/components/AddRoom.vue then add this lines of codes.

<template>
  <b-row>
    <b-col align-self="start">&nbsp;</b-col>
    <b-col cols="6" align-self="center">
      <h2>
        Add Room
        <b-link href="#/">(Room List)</b-link>
      </h2>
      <b-form @submit="onSubmit">
        <b-form-group id="fieldsetHorizontal"
                  horizontal
                  :label-cols="4"
                  breakpoint="md"
                  label="Enter Room Name">
          <b-form-input id="room_name" :state="state" v-model.trim="room.room_name"></b-form-input>
        </b-form-group>
        <b-button type="submit" variant="primary">Save</b-button>
      </b-form>
    </b-col>
    <b-col align-self="end">&nbsp;</b-col>
  </b-row>
</template>

<script>

import axios from 'axios'

export default {
  name: 'AddRoom',
  data () {
    return {
      room: {}
    }
  },
  methods: {
    onSubmit (evt) {
      evt.preventDefault()
      axios.post(`http://localhost:3000/api/room`, this.room)
      .then(response => {
        this.$router.push({
          name: 'RoomList'
        })
      })
      .catch(e => {
        this.errors.push(e)
      })
    }
  }
}
</script>

That code contains the template for room form, the script that contains Vue.js 2 codes for hold room model and methods for saving room to RESTful API.

10. Modify Vue.js 2 Component for Join Room

Now, open and edit src/components/JoinRoom.vue then add this lines of codes.

<template>
  <b-row>
    <b-col cols="6">
      <h2>
        Join Room
        <b-link href="#/">(Room List)</b-link>
      </h2>
      <b-form @submit="onSubmit">
        <b-form-group id="fieldsetHorizontal"
                  horizontal
                  :label-cols="4"
                  breakpoint="md"
                  label="Enter Nickname">
          <b-form-input id="nickname" :state="state" v-model.trim="chat.nickname"></b-form-input>
        </b-form-group>
        <b-button type="submit" variant="primary">Join</b-button>
      </b-form>
    </b-col>
  </b-row>
</template>

<script>

import axios from 'axios'

export default {
  name: 'JoinRoom',
  data () {
    return {
      chat: {}
    }
  },
  methods: {
    onSubmit (evt) {
      evt.preventDefault()
      this.chat.room = this.$route.params.id
      this.chat.message = this.chat.nickname + ' join the room'
      axios.post(`http://localhost:3000/api/chat`, this.chat)
      .then(response => {
        this.$router.push({
          name: 'ChatRoom',
          params: { id: this.$route.params.id, nickname: response.data.nickname }
        })
      })
      .catch(e => {
        this.errors.push(e)
      })
    }
  }
}
</script>

That code contains the template for join room form, the script that contains Vue.js 2 codes for hold chat model and methods for saving room to RESTful API.

11. Modify Vue.js 2 Component for Chat Room

Now, open and edit src/components/JoinRoom.vue then add this lines of codes.

<template>
  <b-row>
    <b-col cols="12">
      <h2>
        Chat Room
      </h2>
      <b-list-group class="panel-body">
        <b-list-group-item v-for="(item, index) in chats" class="chat">
          <div class="left clearfix" v-if="item.nickname === nickname">
            <b-img left src="http://placehold.it/50/55C1E7/fff&text=ME" rounded="circle" width="75" height="75" alt="img" class="m-1" />
            <div class="chat-body clearfix">
              <div class="header">
                <strong class="primary-font">{{ item.nickname }}</strong> <small class="pull-right text-muted">
                <span class="glyphicon glyphicon-time"></span>{{ item.created_date }}</small>
              </div>
              <p>{{ item.message }}</p>
            </div>
          </div>
          <div class="right clearfix" v-else>
            <b-img right src="http://placehold.it/50/55C1E7/fff&text=U" rounded="circle" width="75" height="75" alt="img" class="m-1" />
            <div class="chat-body clearfix">
              <div class="header">
                <strong class="primary-font">{{ item.nickname }}</strong> <small class="pull-right text-muted">
                <span class="glyphicon glyphicon-time"></span>{{ item.created_date }}</small>
              </div>
              <p>{{ item.message }}</p>
            </div>
          </div>
        </b-list-group-item>
      </b-list-group>
      <ul v-if="errors && errors.length">
        <li v-for="error of errors">
          {{error.message}}
        </li>
      </ul>
      <b-form @submit="onSubmit" class="chat-form">
        <b-input-group prepend="Message">
          <b-form-input id="message" :state="state" v-model.trim="chat.message"></b-form-input>
          <b-input-group-append>
            <b-btn type="submit" variant="info">Send</b-btn>
          </b-input-group-append>
        </b-input-group>
      </b-form>
    </b-col>
  </b-row>
</template>

<script>

import axios from 'axios'

export default {
  name: 'ChatRoom',
  data () {
    return {
      chats: [],
      errors: [],
      nickname: this.$route.params.nickname,
      chat: {}
    }
  },
  created () {
    axios.get(`http://localhost:3000/api/chat/` + this.$route.params.id)
    .then(response => {
      this.chats = response.data
    })
    .catch(e => {
      this.errors.push(e)
    })
  },
  methods: {
    logout (id) {
      this.$router.push({
        name: 'JoinRoom',
        params: { id: id }
      })
    },
    onSubmit (evt) {
      evt.preventDefault()
      this.chat.room = this.$route.params.id
      this.chat.nickname = this.$route.params.nickname
      axios.post(`http://localhost:3000/api/chat`, this.chat)
      .then(response => {
        // this.$router.push({
        //   name: 'ChatRoom',
        //   params: { id: this.$route.params.id, nickname: response.data.nickname }
        // })
      })
      .catch(e => {
        this.errors.push(e)
      })
    }
  }
}
</script>

<style>
  .chat .left .chat-body {
    text-align: left;
    margin-left: 100px;
  }

  .chat .right .chat-body {
    text-align: right;
    margin-right: 100px;
  }

  .chat .chat-body p {
    margin: 0;
    color: #777777;
  }

  .panel-body {
    overflow-y: scroll;
    height: 350px;
  }

  .chat-form {
    margin: 20px auto;
    width: 80%;
  }
</style>

That code contains the template of the main chat application consist of chat list and sends message form.

12. Integrate Socket.io With Existing Non-Real-time Chat Application

Previous steps show you a regular and non-realtime transaction chat application. Now, we will make it real-time by using Socket.io. First, install socket.io module by type this command.

npm install --save socketio socket.io-client

Next, open and edit routes/chat.js then declare the Socket IO and http module.

var app = express();
var server = require('http').createServer(app);
var io = require('socket.io')(server);

Add this lines of codes for Socket IO functions.

server.listen(4000);

// socket io
io.on('connection', function (socket) {
  console.log('User connected');
  socket.on('disconnect', function() {
    console.log('User disconnected');
  });
  socket.on('save-message', function (data) {
    console.log(data);
    io.emit('new-message', { message: data });
  });
});

In that code, we are running Socket.io to listen for ‘save-message’ that emitted from the client and emit ‘new-message’ to the clients. Next, open and edit src/components/JoinRoom.vue then add this import.

import * as io from 'socket.io-client'

Declare Socket IO variable.

data () {
  return {
    chat: {},
    socket: io('http://localhost:4000')
  }
},

Add Socket IO emit function after successful join room.

axios.post(`http://localhost:3000/api/chat`, this.chat)
.then(response => {
  this.socket.emit('save-message', { room: this.chat.room, nickname: this.chat.nickname, message: 'Join this room', created_date: new Date() });
  this.$router.push({
    name: 'ChatRoom',
    params: { id: this.$route.params.id, nickname: response.data.nickname }
  })
})
.catch(e => {
  this.errors.push(e)
})

Next, open and edit src/components/ChatRoom.vue then add this imports and use as Vue module.

import Vue from 'vue'
import * as io from 'socket.io-client'
import VueChatScroll from 'vue-chat-scroll'
Vue.use(VueChatScroll)

Declare Socket IO variable.

data () {
  return {
    chats: [],
    errors: [],
    nickname: this.$route.params.nickname,
    chat: {},
    socket: io('http://localhost:4000')
  }
},

Add this Socket IO on function to created method.

created () {
  axios.get(`http://localhost:3000/api/chat/` + this.$route.params.id)
  .then(response => {
    this.chats = response.data
  })
  .catch(e => {
    this.errors.push(e)
  })

  this.socket.on('new-message', function (data) {
    if(data.message.room === this.$route.params.id) {
      this.chats.push(data.message)
    }
  }.bind(this))
},

Add Logout function inside methods and add Socket IO emit method in the POST response.

methods: {
  logout () {
    this.socket.emit('save-message', { room: this.chat.room, nickname: this.chat.nickname, message: this.chat.nickname + ' left this room', created_date: new Date() });
    this.$router.push({
      name: 'RoomList'
    })
  },
  onSubmit (evt) {
    evt.preventDefault()
    this.chat.room = this.$route.params.id
    this.chat.nickname = this.$route.params.nickname
    axios.post(`http://localhost:3000/api/chat`, this.chat)
    .then(response => {
      this.socket.emit('save-message', response.data)
      this.chat.message = ''
    })
    .catch(e => {
      this.errors.push(e)
    })
  }
}

Finally, to make Chat list always scroll to the bottom of Chat element add install this module.

npm install --save vue-chat-scroll

That module already imported and declared above. Next, add to the Chat element.

<b-list-group class="panel-body" v-chat-scroll>
  ...
</b-list-group>

13. Run and Test The MEVN (Vue.js 2) Chat Application**

To run this MEVN (Vue.js 2) Chat Application locally, make sure MongoDB service is running. Type this command to build the Vue.js 2 application then run the Express.js application.

npm start

Next, open the different browser (ex: Chrome and Firefox) then go to the localhost:3000 on both of the browsers. You will see this page and you can start Chat.

That it’s, the MongoDB, Express, Vue.js 2, Node.js (MEVN) and SocketIO Chat App. You can find the full working source code on our GitHub.

Thanks!

=============================

Thanks for reading :heart: If you liked this post, share it with all of your programming buddies! Follow me on Facebook | Twitter

Learn More

☞ MERN Stack Front To Back: Full Stack React, Redux & Node.js

☞ The Complete Node.js Developer Course (3rd Edition)

☞ Angular & NodeJS - The MEAN Stack Guide

☞ NodeJS - The Complete Guide (incl. MVC, REST APIs, GraphQL)

☞ Docker for Node.js Projects From a Docker Captain

☞ Intro To MySQL With Node.js - Learn To Use MySQL with Node!

☞ Node.js Absolute Beginners Guide - Learn Node From Scratch

☞ React Node FullStack - Social Network from Scratch to Deploy

☞ Selenium WebDriver - JavaScript nodeJS webdriver IO & more!

☞ Complete Next.js with React & Node - Beautiful Portfolio App

☞ Build a Blockchain & Cryptocurrency | Full-Stack Edition

Node, Express, Angular 7, GraphQL and MongoDB CRUD Web App

Node, Express, Angular 7, GraphQL and MongoDB CRUD Web App

In this tutorial, we will go to the walkthrough of building GraphQL query language API for communication between Node-Express-MongoDB on Server side and Angular 7 on the Client side.

In this tutorial, we will go to the walkthrough of building GraphQL query language API for communication between Node-Express-MongoDB on Server side and Angular 7 on the Client side.

The comprehensive step by step tutorial on building CRUD (Create, Read, Update, Delete) Web Application using Node.js, Express.js, Angular 7, MongoDB and GraphQL. This is our first tutorial that using GraphQL, you can find more reference and guide on their official site.

On the server side, we are using Express-Graphql modules and it’s dependencies. For the client side, we are using Apollo Angular modules and dependencies.

Table of Contents:
  • Create Express.js App
  • Install and Configure Mongoose.js Modules for Accessing MongoDB
  • Create Mongoose.js Model for the Book Document
  • Install GraphQL Modules and Dependencies
  • Create GraphQL Schemas for the Book
  • Add Mutation for CRUD Operation to the Schema
  • Test GraphQL using GraphiQL
  • Create Angular 7 Application
  • Install and Configure Required Modules and Dependencies
  • Create Routes for Navigation between Angular Pages/Component
  • Display List of Books using Angular 7 Material
  • Show and Delete Books
  • Add a New Book using Angular 7 Material
  • Edit a Book using Angular 7 Material
  • Run and Test GraphQL CRUD from the Angular 7 Application

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

We assume that you have installed Node.js. Now, we need to check the Node.js and NPM versions. Open the terminal or Node command line then type this commands.

node -v
v8.12.0
npm -v
6.4.1

That’s the Node.js and NPM version that we are using. Now, you can go to the main steps.

1. Create Express.js App

If Express.js Generator hasn’t installed, type this command from the terminal or Node.js command prompt.

sudo npm install express-generator -g

The sudo keyword is using in OSX or Linux Terminal otherwise you can use that command without sudo. Before we create an Express.js app, we have to create a root project folder inside your projects folder. From the terminal or Node.js command prompt, type this command at your projects folder.

mkdir node-graphql

Go to the newly created directory.

cd ./node-graphql

From there, type this command to generate Express.js application.

express server

Go to the newly created Express.js app folder.

cd ./server

Type this command to install all required NPM modules that describe in package.json dependencies.

npm install

To check the Express.js app running smoothly, type this command.

nodemon

or

npm start

If you see this information in the terminal or command prompt that means your Express.js app is ready to use.

[nodemon] 1.18.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node ./bin/www`

2. Install and Configure Mongoose.js Modules for Accessing MongoDB

To install Mongoose.js and it’s required dependencies, type this command.

npm install mongoose bluebird --save

Next, open and edit app.js then declare the Mongoose module.

var mongoose = require('mongoose');

Create a connection to the MongoDB server using this lines of codes.

mongoose.connect('mongodb://localhost/node-graphql', { promiseLibrary: require('bluebird'), useNewUrlParser: true })
  .then(() =>  console.log('connection successful'))
  .catch((err) => console.error(err));

Now, if you re-run again Express.js server after running MongoDB server or daemon, you will see this information in the console.

[nodemon] 1.18.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node ./bin/www`
connection successful

That’s mean, the connection to the MongoDB is successful.

3. Create Mongoose.js Model for the Book Document

Before creating a Mongoose.js model that represent Book Document, we have to create a folder at the server folder for hold Models. After that, we can create a Mongoose.js model file.

mkdir models
touch models/Book.js

Open and edit server/models/Book.js then add these lines of codes.

var mongoose = require('mongoose');

var BookSchema = new mongoose.Schema({
  id: String,
  isbn: String,
  title: String,
  author: String,
  description: String,
  published_year: { type: Number, min: 1945, max: 2019 },
  publisher: String,
  updated_date: { type: Date, default: Date.now },
});

module.exports = mongoose.model('Book', BookSchema);

4. Install GraphQL Modules and Dependencies

Now, the GraphQL time. Type this command to install GraphQL modules and it’s dependencies.

npm install express express-graphql graphql cors --save

Next, open and edit server/app.js then declare all of those modules and dependencies.

var graphqlHTTP = require('express-graphql');
var schema = require('./graphql/bookSchema');
var cors = require("cors");

The schema is not created yet, we will create it in the next steps. Next, add these lines of codes for configuring GraphQL that can use over HTTP.

app.use('*', cors());
app.use('/graphql', cors(), graphqlHTTP({
  schema: schema,
  rootValue: global,
  graphiql: true,
}));

That’s configuration are enabled CORS and the GraphiQL. GraphiQL is the user interface for testing GraphQL query.

5. Create GraphQL Schemas for the Book

Create a folder at the server folder for hold GraphQL Schema files then create a Javascript file for the schema.

mkdir graphql
touch graphql/bookSchemas.js

Next, open and edit server/graphql/bookSchemas.js then declares all required modules and models.

var GraphQLSchema = require('graphql').GraphQLSchema;
var GraphQLObjectType = require('graphql').GraphQLObjectType;
var GraphQLList = require('graphql').GraphQLList;
var GraphQLObjectType = require('graphql').GraphQLObjectType;
var GraphQLNonNull = require('graphql').GraphQLNonNull;
var GraphQLID = require('graphql').GraphQLID;
var GraphQLString = require('graphql').GraphQLString;
var GraphQLInt = require('graphql').GraphQLInt;
var GraphQLDate = require('graphql-date');
var BookModel = require('../models/Book');

Create a GraphQL Object Type for Book models.

var bookType = new GraphQLObjectType({
  name: 'book',
  fields: function () {
    return {
      _id: {
        type: GraphQLString
      },
      isbn: {
        type: GraphQLString
      },
      title: {
        type: GraphQLString
      },
      author: {
        type: GraphQLString
      },
      description: {
        type: GraphQLString
      },
      published_year: {
        type: GraphQLInt
      },
      publisher: {
        type: GraphQLString
      },
      updated_date: {
        type: GraphQLDate
      }
    }
  }
});

Next, create a GraphQL query type that calls a list of book and single book by ID.

var queryType = new GraphQLObjectType({
  name: 'Query',
  fields: function () {
    return {
      books: {
        type: new GraphQLList(bookType),
        resolve: function () {
          const books = BookModel.find().exec()
          if (!books) {
            throw new Error('Error')
          }
          return books
        }
      },
      book: {
        type: bookType,
        args: {
          id: {
            name: '_id',
            type: GraphQLString
          }
        },
        resolve: function (root, params) {
          const bookDetails = BookModel.findById(params.id).exec()
          if (!bookDetails) {
            throw new Error('Error')
          }
          return bookDetails
        }
      }
    }
  }
});

Finally, exports this file as GraphQL schema by adding this line at the end of the file.

module.exports = new GraphQLSchema({query: queryType});

6. Add Mutation for CRUD Operation to the Schema

For completing CRUD (Create, Read, Update, Delete) operation of the GraphQL, we need to add a mutation that contains create, update and delete operations. Open and edit server/graphql/bookSchemas.js then add this mutation as GraphQL Object Type.

var mutation = new GraphQLObjectType({
  name: 'Mutation',
  fields: function () {
    return {
      addBook: {
        type: bookType,
        args: {
          isbn: {
            type: new GraphQLNonNull(GraphQLString)
          },
          title: {
            type: new GraphQLNonNull(GraphQLString)
          },
          author: {
            type: new GraphQLNonNull(GraphQLString)
          },
          description: {
            type: new GraphQLNonNull(GraphQLString)
          },
          published_year: {
            type: new GraphQLNonNull(GraphQLInt)
          },
          publisher: {
            type: new GraphQLNonNull(GraphQLString)
          }
        },
        resolve: function (root, params) {
          const bookModel = new BookModel(params);
          const newBook = bookModel.save();
          if (!newBook) {
            throw new Error('Error');
          }
          return newBook
        }
      },
      updateBook: {
        type: bookType,
        args: {
          id: {
            name: 'id',
            type: new GraphQLNonNull(GraphQLString)
          },
          isbn: {
            type: new GraphQLNonNull(GraphQLString)
          },
          title: {
            type: new GraphQLNonNull(GraphQLString)
          },
          author: {
            type: new GraphQLNonNull(GraphQLString)
          },
          description: {
            type: new GraphQLNonNull(GraphQLString)
          },
          published_year: {
            type: new GraphQLNonNull(GraphQLInt)
          },
          publisher: {
            type: new GraphQLNonNull(GraphQLString)
          }
        },
        resolve(root, params) {
          return BookModel.findByIdAndUpdate(params.id, { isbn: params.isbn, title: params.title, author: params.author, description: params.description, published_year: params.published_year, publisher: params.publisher, updated_date: new Date() }, function (err) {
            if (err) return next(err);
          });
        }
      },
      removeBook: {
        type: bookType,
        args: {
          id: {
            type: new GraphQLNonNull(GraphQLString)
          }
        },
        resolve(root, params) {
          const remBook = BookModel.findByIdAndRemove(params.id).exec();
          if (!remBook) {
            throw new Error('Error')
          }
          return remBook;
        }
      }
    }
  }
});

Finally, add this mutation to the GraphQL Schema exports.

module.exports = new GraphQLSchema({query: queryType, mutation: mutation});

7. Test GraphQL using GraphiQL

To test the queries and mutations of CRUD operations, re-run again the Express.js app then open the browser. Go to this address <a href="http://localhost:3000/graphql" target="_blank">http://localhost:3000/graphql</a> to open the GraphiQL User Interface.

To get the list of books, replace all of the text on the left pane with this GraphQL query then click the Play button.

To get a single book by ID, use this GraphQL query.

{
  book(id: "5c738dd4cb720f79497de85c") {
    _id
    isbn
    title
    author
    description
    published_year
    publisher
    updated_date
  }
}

To add a book, use this GraphQL mutation.

mutation {
  addBook(
    isbn: "12345678",
    title: "Whatever this Book Title",
    author: "Mr. Bean",
    description: "The short explanation of this Book",
    publisher: "Djamware Press",
    published_year: 2019
  ) {
    updated_date
  }
}

You will the response at the right pane like this.

{
  "data": {
    "addBook": {
      "updated_date": "2019-02-26T13:55:39.160Z"
    }
  }
}

To update a book, use this GraphQL mutation.

mutation {
  updateBook(
    id: "5c75455b146dbc2504b94012",
    isbn: "12345678221",
    title: "The Learning Curve of GraphQL",
    author: "Didin J.",
    description: "The short explanation of this Book",
    publisher: "Djamware Press",
    published_year: 2019
  ) {
    _id,
    updated_date
  }
}

You will see the response in the right pane like this.

{
  "data": {
    "updateBook": {
      "_id": "5c75455b146dbc2504b94012",
      "updated_date": "2019-02-26T13:58:35.811Z"
    }
  }
}

To delete a book by ID, use this GraphQL mutation.

mutation {
  removeBook(id: "5c75455b146dbc2504b94012") {
    _id
  }
}

You will see the response in the right pane like this.

{
  "data": {
    "removeBook": {
      "_id": "5c75455b146dbc2504b94012"
    }
  }
}

8. Create Angular 7 Application

Before creating an Angular 7 application, we have to install Angular 7 CLI first. Type this command to install it.

sudo npm install -g @angular/cli

Next, create a new Angular 7 Web Application using this Angular CLI command at the root of this project folder.

ng new client

If you get the question like below, choose Yes and SCSS (or whatever you like to choose).

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

Next, go to the newly created Angular 7 project folder.

cd client

Type this command to run the Angular 7 application using this command.

ng serve

Open your browser then go to this address localhost:4200 to check if Angular 7 created successfully.

9. Install and Configure Required Modules and Dependencies

Now, we have to install and configure all of the required modules and dependencies. Type this command to install the modules.

npm install --save apollo-angular apollo-angular-link-http apollo-link apollo-client apollo-cache-inmemory graphql-tag graphql

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

import { HttpClientModule } from '@angular/common/http';
import { ApolloModule, Apollo } from 'apollo-angular';
import { HttpLinkModule, HttpLink } from 'apollo-angular-link-http';

Add these modules to the @NgModule imports.

imports: [
  ...,
  HttpClientModule,
  ApolloModule,
  HttpLinkModule,
  ...
],

Create a constructor inside class AppModule then inject above modules and create a connection to the GraphQL in the Express.js server.

export class AppModule {
  constructor(
    apollo: Apollo,
    httpLink: HttpLink
  ) {
     apollo.create({
      link: httpLink.create({ uri: 'http://localhost:3000/graphql'}),
      cache: new InMemoryCache()
    });
  }
}

10. Create Routes for Navigation between Angular Pages/Component

The Angular 7 routes already added when we create new Angular 7 application in the previous step. Before configuring the routes, type this command to create a new Angular 7 components.

ng g component books
ng g component books/detail
ng g component books/add
ng g component books/edit

Open client/src/app/app.module.ts then you will see those components imported and declared in @NgModule declarations. Next, open and edit src/app/app-routing.module.ts then add these imports.

import { BooksComponent } from './books/books.component';
import { DetailComponent } from './books/detail/detail.component';
import { AddComponent } from './books/add/add.component';
import { EditComponent } from './books/edit/edit.component';

Add these arrays to the existing empty array of routes constant.

const routes: Routes = [
  {
    path: 'books',
    component: BooksComponent,
    data: { title: 'List of Books' }
  },
  {
    path: 'books/detail/:id',
    component: DetailComponent,
    data: { title: 'Book Details' }
  },
  {
    path: 'books/add',
    component: AddComponent,
    data: { title: 'Add Book' }
  },
  {
    path: 'books/edit/:id',
    component: EditComponent,
    data: { title: 'Edit Book' }
  },
  { path: '',
    redirectTo: '/books',
    pathMatch: 'full'
  }
];

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

<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
  <img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
</div>

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

Finally, open and edit src/app/app.component.scss then replace all SASS codes with this.

.container {
  padding: 20px;
}

11. Display List of Books using Angular 7 Material

We will be using Angular 7 Material as UI/UX component. First, we have to install these modules to the Angular 7 application. Type this Angular 7 Schema to install it.

ng add @angular/material

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

? Enter a prebuilt theme name, or "custom" for a custom theme: purple-green
? Set up HammerJS for gesture recognition? Yes
? Set up browser animations for Angular Material? Yes

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

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

Of course we will use Angular 7 Reactive Form module, for that, modify FormsModule import to add ReactiveFormsModule.

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

Register the above modules to @NgModule imports array.

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

Next, to display a list of Books. Open and edit client/src/app/books/books.component.ts that previously generated then add these imports.

import { Apollo } from 'apollo-angular';
import gql from 'graphql-tag';
import { Book } from './book';

Declare all required variables for hold response, data, Angular Material table column and loading spinner control.

displayedColumns: string[] = ['title', 'author'];
data: Book[] = [];
resp: any = {};
isLoadingResults = true;

Inject the Apollo Angular module to the constructor.

constructor(private apollo: Apollo) {
}

Add a gql query inside ngOnInit() function.

ngOnInit() {
  this.apollo.query({
    query: gql `{ books { _id, title, author } }`
  }).subscribe(res => {
    this.resp = res;
    this.data = this.resp.data.books;
    console.log(this.data);
    this.isLoadingResults = false;
  });
}

Next, open and edit client/src/app/books/books.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/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">

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

      <!-- Product Price 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]="['/books/detail/', row._id]"></tr>
    </table>
  </div>
</div>

Finally, add some styles for this page by open and edit client/src/app/books/books.component.scss then add these lines of SCSS 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;
}

12. Show and Delete Books

On the list of Books page we have a clickable row that can redirect to the show details page. Next, open and edit client/src/app/books/detail/detail.component.ts then add these imports.

import { ActivatedRoute, Router } from '@angular/router';
import { Apollo, QueryRef } from 'apollo-angular';
import gql from 'graphql-tag';
import { Book } from '../book';

Declare a constant variable before the class name for query and delete a book by ID.

const bookQuery = gql`
  query book($bookId: String) {
    book(id: $bookId) {
      _id
      isbn
      title
      author
      description
      published_year
      publisher
      updated_date
    }
  }
`;

const deleteBook = gql`
  mutation removeBook($id: String!) {
    removeBook(id:$id) {
      _id
    }
  }
`;

Next, declare all required variables before the constructor.

book: Book = { id: '', isbn: '', title: '', author: '', description: '', publisher: '', publishedYear: null, updatedDate: null };
isLoadingResults = true;
resp: any = {};
private query: QueryRef<any>;

Inject above imported modules to the constructor.

constructor(private apollo: Apollo, private router: Router, private route: ActivatedRoute) { }

Add a function for get a single Book data by ID.

getBookDetails() {
  const id = this.route.snapshot.params.id;
  this.query = this.apollo.watchQuery({
    query: bookQuery,
    variables: { bookId: id }
  });

  this.query.valueChanges.subscribe(res => {
    this.book = res.data.book;
    console.log(this.book);
    this.isLoadingResults = false;
  });
}

Call that function from ngOnInit function.

ngOnInit() {
  this.getBookDetails();
}

Add a function for delete a book by ID.

deleteBook() {
  this.isLoadingResults = true;
  const bookId = this.route.snapshot.params.id;
  this.apollo.mutate({
    mutation: deleteBook,
    variables: {
      id: bookId
    }
  }).subscribe(({ data }) => {
    console.log('got data', data);
    this.isLoadingResults = false;
    this.router.navigate(['/books']);
  }, (error) => {
    console.log('there was an error sending the query', error);
    this.isLoadingResults = false;
  });
}

For the view, open and edit client/src/app/books/detail/detail.component.html then replace all HTML tags with these lines of HTML 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]="['/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}}</mat-card-subtitle>
    </mat-card-header>
    <mat-card-content>
      <dl>
        <dt>ISBN:</dt>
        <dd>{{book.isbn}}</dd>
        <dt>Description:</dt>
        <dd>{{book.description}}</dd>
        <dt>Publisher:</dt>
        <dd>{{book.publisher}}</dd>
        <dt>Published Year:</dt>
        <dd>{{book.published_year}}</dd>
        <dt>Update Date:</dt>
        <dd>{{book.updated_date}}</dd>
      </dl>
    </mat-card-content>
    <mat-card-actions>
      <a mat-flat-button color="primary" [routerLink]="['/books/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>

To adjust the style, open and edit client/src/app/books/detail/detail.component.scss then add these lines of SCSS 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;
}

13. Add a New Book using Angular 7 Material

In the list of Book we have an Add button that will redirect to the Add Page. Next, open and edit client/src/app/books/add/add.component.ts then add these imports.

import { Router } from '@angular/router';
import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { Apollo } from 'apollo-angular';
import gql from 'graphql-tag';

Add a constant of gql query after the imports for submitting or post a new Book data.

const submitBook = gql`
  mutation addBook(
    $isbn: String!,
    $title: String!,
    $author: String!,
    $description: String!,
    $publisher: String!,
    $published_year: Int!) {
    addBook(
      isbn: $isbn,
      title: $title,
      author: $author,
      description: $description,
      publisher: $publisher,
      published_year: $published_year) {
      _id
    }
  }
`;

Declare all required variables before the constructor.

book: any = { isbn: '', title: '', author: '', description: '', publisher: '', publishedYear: null, updatedDate: null };
isLoadingResults = false;
resp: any = {};
bookForm: FormGroup;
isbn = '';
title = '';
author = '';
description = '';
publisher = '';
publishedYear: number = null;

Inject above imported modules to the constructor.

constructor(
  private apollo: Apollo,
  private router: Router,
  private formBuilder: FormBuilder
) { }

Initialize Angular 7 form group inside ngOnInit function.

ngOnInit() {
  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]
  });
}

Add a function to get the form controls from the form group.

get f() {
  return this.bookForm.controls;
}

Add a function to submit or post a new book data.

onSubmit(form: NgForm) {
  this.isLoadingResults = true;
  const bookData = form.value;
  this.apollo.mutate({
    mutation: submitBook,
    variables: {
      isbn: bookData.isbn,
      title: bookData.title,
      author: bookData.author,
      description: bookData.description,
      publisher: bookData.publisher,
      published_year: bookData.publishedYear
    }
  }).subscribe(({ data }) => {
    console.log('got data', data);
    this.isLoadingResults = false;
    this.router.navigate(['/books/detail/', data.addBook._id]);
  }, (error) => {
    console.log('there was an error sending the query', error);
    this.isLoadingResults = false;
  });
}

Next, open and edit client/src/app/books/add/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" #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate>
      <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">
        <textarea matInput placeholder="Description" formControlName="description"
               [errorStateMatcher]="matcher"></textarea>
        <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" type="number" 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>

Give a litle style by open and edit client/src/app/books/add/add.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() {
  margin-bottom: 10px;
}

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

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

14. Edit a Book using Angular 7 Material

We have put an edit button inside the Book Detail component for a redirect to Edit page. Now, open and edit client/src/app/books/edit/edit.component.ts then add these imports.

import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder, FormGroup, NgForm, Validators } from '@angular/forms';
import { Apollo, QueryRef } from 'apollo-angular';
import gql from 'graphql-tag';

Add gql query before the class name for getting single Book by ID and submit book data.

const bookQuery = gql`
  query book($bookId: String) {
    book(id: $bookId) {
      _id
      isbn
      title
      author
      description
      published_year
      publisher
      updated_date
    }
  }
`;

const submitBook = gql`
  mutation updateBook(
    $id: String!,
    $isbn: String!,
    $title: String!,
    $author: String!,
    $description: String!,
    $publisher: String!,
    $published_year: Int!) {
    updateBook(
      id: $id,
      isbn: $isbn,
      title: $title,
      author: $author,
      description: $description,
      publisher: $publisher,
      published_year: $published_year) {
      updated_date
    }
  }
`;

Add all required variables before the constructor.

book: any = { _id: '', isbn: '', title: '', author: '', description: '', publisher: '', publishedYear: null, updatedDate: null };
isLoadingResults = true;
resp: any = {};
private query: QueryRef<any>;
bookForm: FormGroup;
id = '';
isbn = '';
title = '';
author = '';
description = '';
publisher = '';
publishedYear: number = null;

Inject above imported modules to the constructor.

constructor(
  private apollo: Apollo,
  private route: ActivatedRoute,
  private router: Router,
  private formBuilder: FormBuilder) { }

Initialize the Angular 7 form group to the ngOnInit function.

ngOnInit() {
  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]
  });
}

Add a function to get the form controls from the form group.

get f() {
  return this.bookForm.controls;
}

Add a function to get a single book data by ID.

getBookDetails() {
  const id = this.route.snapshot.params.id;
  this.query = this.apollo.watchQuery({
    query: bookQuery,
    variables: { bookId: id }
  });

  this.query.valueChanges.subscribe(res => {
    this.book = res.data.book;
    console.log(this.book);
    this.id = this.book._id;
    this.isLoadingResults = false;
    this.bookForm.setValue({
      isbn: this.book.isbn,
      title: this.book.title,
      author: this.book.author,
      description: this.book.description,
      publisher: this.book.publisher,
      publishedYear: this.book.published_year
    });
  });
}

Call that function from the ngOnInit function.

ngOnInit() {
  ...
  this.getBookDetails();
}

Add a function for submitting the Book data to the GraphQL.

onSubmit(form: NgForm) {
  this.isLoadingResults = true;
  console.log(this.id);
  const bookData = form.value;
  this.apollo.mutate({
    mutation: submitBook,
    variables: {
      id: this.id,
      isbn: bookData.isbn,
      title: bookData.title,
      author: bookData.author,
      description: bookData.description,
      publisher: bookData.publisher,
      published_year: bookData.publishedYear
    }
  }).subscribe(({ data }) => {
    console.log('got data', data);
    this.isLoadingResults = false;
  }, (error) => {
    console.log('there was an error sending the query', error);
    this.isLoadingResults = false;
  });
}

Add a function to enter the Book details after click a Details button.

bookDetails() {
  this.router.navigate(['/books/detail/', this.id]);
}

Next, open and edit client/src/app/books/edit/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" #f="ngForm" (ngSubmit)="onSubmit(f)" novalidate>
      <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">
        <textarea matInput placeholder="Description" formControlName="description"
               [errorStateMatcher]="matcher"></textarea>
        <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" type="number" 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>

For styling, open and edit client/src/app/books/edit/edit.component.scss then add these 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() {
  margin-bottom: 10px;
}

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

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

15. Run and Test GraphQL CRUD from the Angular 7 Application

Before the test, the GraphQL CRUD from the Angular 7 Application, just makes sure that you have run MongoDB server and Express.js server. If not yet, run those servers in different Terminal tabs. Next, run the Angular 7 application from the different terminal tabs.

ng serve

In the browser go to this URL localhost:4200 and here the whole application looks like.

That it’s, we have finished the Node, Express, Angular 7, GraphQL and MongoDB CRUD Web App. If you can’t follow the steps of the tutorial, you can compare it with the working source code from our GitHub.

Thanks for reading ❤