MEAN Stack Tutorial for Beginners

MEAN Stack Tutorial for Beginners

In this MEAN Stack tutorial you will learn how to build a MEAN application from scratch. This advanced computer science course will provide an overview of several of the most popular web and mobile app development toolkits, including MongoDB, ExpressJS, AngularJS, and Node.js. Together, these tools are called the “MEAN stack.” This is an advanced course for experienced web developers. In this course, we will introduce the essential concepts of the MEAN stack using a fast-paced, learn-by- doing approach. As a concrete example, we will walk through building a retail application. This online store will enable users to search for products, add them to their cart, and check out using Stripe, an online payment processor. You’ll build your own REST API (Representational State Transfer Application Programming Interface) server from scratch using MongoDB schema design principles, as well as a web application using AngularJS and a hybrid mobile application using the Ionic framework. When you complete the course, you will receive a discount to MongoDB Professional Certification, enabling you to demonstrate your skills, show recruiters and hiring managers your expertise, and earn a spot in the MongoDB Certified Professional Finder. What you'll learn Installing and working with MongoDB and Node.js MongoDB schema design fundamentals Building REST APIs with Node.js and Express Building single page applications with AngularJS Building mobile apps with the Ionic framework

Angular 9 Tutorial: Learn to Build a CRUD Angular App Quickly

What's new in Bootstrap 5 and when Bootstrap 5 release date?

What’s new in HTML6

How to Build Progressive Web Apps (PWA) using Angular 9

What is new features in Javascript ES2020 ECMAScript 2020

NodeJS API Development with Express MongoDB and Mongoose

NodeJS API Development with Express MongoDB and Mongoose

NodeJS API Development with Express MongoDB and Mongoose. MongoDB is a NoSQL document-oriented database. It’s popular in the Node.js community and a viable database solution for building real-world applications.

MongoDB is different from traditional, SQL databases like MySQL and PostgreSQL in that data is stored in binary JSON-like objects called BSON). This structure lends itself well to building Javascript applications that communicate with JSON. Additionally, MongoDB has flexible schema. This means there aren’t database migrations to worry about and data models can grow and change.

In this tutorial, we’re going to set up a Node.js server application, connect it to MongoDB and demonstrate how relationships work between MongoDB Collections. In the table below (provided by MongoDB) you’ll see how traditional aspects of SQL databases stack up against their MongoDB equivalents. You can find the whole source code for this tutorial in this GitHub repo.

In SQL databases, we get database relationships using joins. For example, if we had a SQL database with two tables, books and authors, we could get all the books that belong to an author like so:

SELECT b.id AS ‘Post ID’, 
b.title AS ‘Book Title’, 
a.name AS ‘Author Name`, 
a.id AS ‘Author ID’
FROM books b
JOIN authors ON b.author_id = a.id
WHERE a.id = 1234;

This will grab information from both tables and display the results in a single dataset for us. Frameworks like Ruby On Rails and Laravel have abstracted this functionality for developers, making it possible to write PHP or Ruby to grab related information.

In Ruby On Rails, using Active Record finding, an author and related posts could look like:

authorWithBooks = Author.find 1234, :include => [:books]

In Laravel, using Eloquent, we could do:

$authorWithBooks = Author::find(1234)->books();

These results would give us the author with id 1234 and all the books that they've written. On the books table, we'd store an author_id, setting up the relationship between authors and books in the SQL world. MongoDB doesn't use joins though, so how do we achieve this functionality?

There is a helper npm package for working with MongoDB called mongoose that we’re going to use for illustrative purposes in this tutorial. Mongoose is an ORM (stands for Object Relationship Mapper) that is a helper for MongoDB kind of like how ActiveRecord and Eloquent are helpers for working with relational data.

Create Database Models with Mongoosejs

The first thing to do is set up our models in Mongoose. These schemas are flexible but help us define what we want our data to look like.

For the author model, we define a model schema that can reference documents in another collection:

const mongoose = require('mongoose');
const authorModel = mongoose.Schema({
  name: { 
   type: String, 
   required: '{PATH} is required!'
  },
  bio: {
   type: String
  },
  website: {
   type: String
  },
  books: [
    { type: mongoose.Schema.Types.ObjectId, ref: 'Book' }
  ]
}, {
  timestamps: true
});
module.exports = mongoose.model('Author', authorModel);

In the above model, we define that in the authors MongoDB collection authors have name, bio, website and an array of books. Each element in the books array will reference the book id on the books collection. We’ll define that below. The second argument, saying timestamps = true will include "updated at" and "created at" fields when we create author records.

The Books schema models what our book data will look like. The schema has a reference to find the id of an associated author. In this example, I’m saying that a book is written by only one author, though in the real world that’s not always the case! Here’s what a belongs-to relationship could look like using Mongoose.js:

const mongoose = require('mongoose');
const bookModel = mongoose.Schema({
  title: { 
    type: String, 
    required: '{PATH} is required!'
  },
  subtitle: {
    type: String
  },
  author: { 
    type: mongoose.Schema.Types.ObjectId, 
    ref: 'Author' 
  }
}, {
  timestamps: true
});
module.exports = mongoose.model('Book', bookModel);

Instead of an array of authors, the book references one author id as the author of the book. We’re using timestamps again for the “updated at” and “created at fields”.

In the root models directory, I added an index to register the models:

module.exports = {
  'Author': require('./Author'),
  'Book': require('./Book'),
};
Register Routes to Return JSON From Express 4

Now that we have the authors and book models defined, it’s time to return and show the data via a JSON API. For that, I set up a controllers for Authors called AuthorsController and one for Books called BooksController. The controllers are responsible for handling the request after the router determines which route to use. Below, we'll define a method for rendering a JSON response of all authors and the JSON of one author based on an id.

The authors controller looks like this:

const { Author } = require('../models');
const AuthorsController = {
  async index(req, res){
    const authors = await Author
       .find()
       .populate('books');
    res.send(authors);
  },
  async show(req, res){
    const author = await Author
       .findById(req.params.id)
       .populate(‘books’);
    res.send(author);
  }
};
module.exports = AuthorsController;

Here, I’m importing the author model, grabbing all of them and populating the query result with the related books. To use the async-await functionality with Express 4, I pulled in a package called express-async-errors and registered it in like so: require('express-async-errors');.

Following that Express 4 requires some server boilerplate setup:

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors());
app.use(methodOverride());
app.use(cookieParser());
app.use(express.static(__dirname + '/public'));
require('./server/routes')(app);

In the **server/routes.js** file, I register API routes for showing all the authors with their books and individual author with their books:

const express = require('express'),
  path = require('path'),
  rootPath = path.normalize(__dirname + '/../'),
  router = express.Router(),
  { AuthorsController, 
    BooksController } = require('./controllers');
module.exports = function(app){
  router.get('/authors', AuthorsController.index);
  router.get('/authors/:id', AuthorsController.show);
  app.use('/api', router);
};

Now we have a working API that returns authors with the books that they’ve written. The only problem is that there are no authors or books stored in MongoDB yet! To fix that, we’ll need to set up code to seed the database with records. If you visit /api/authors now all you'll see is an empty array.

Seed Records Into MongoDB

We need to make sure that the Express 4 server connects properly to MongoDB. For that, we can connect via an URL and listen for successful connection events like so:

const mongoose = require('mongoose'),
  env = process.env.NODE_ENV = process.env.NODE_ENV || 'development',
  envConfig = require('../server/env')[env];
mongoose.Promise = require('bluebird');
mongoose.connect(envConfig.db, { useMongoClient: true, });
mongoose.connection.on('connected', function () {  
  console.log(`Database connection open to ${mongoose.connection.host} ${mongoose.connection.name}`);
});
mongoose.connection.on('error',function (err) {  
  console.log('Mongoose default connection error: ' + err);
});
mongoose.connection.on('disconnected', function () {  
  console.log('Mongoose default connection disconnected'); 
});

With the environment config file defined like so:

var path = require('path'),
  rootPath = path.normalize(__dirname + '/../../');
  
module.exports = {
  development: {
    rootPath: rootPath,
    db: 'mongodb://localhost/mongodb-relationships',
    port: process.env.PORT || 3000
  },
  production: {
    rootPath: rootPath,
    db: process.env.MONGOLAB_URI || 'you can add a mongolab uri here ($ heroku config | grep MONGOLAB_URI)',
    port: process.env.PORT || 80
  }
};

The seeder itself we’re going to run from the command line. It’s a bit verbose but goes through the process of creating and updating records in MongoDB with Mongoose.js.

require('./index');
const mongoose = require('mongoose');
const { Author, Book } = require('../server/models');
async function seedAuthors() {
  console.log('Seeding authors to ' + mongoose.connection.name + '...');
  const authors = [
    { name: 'JK Rowling', bio: 'J.K. Rowling is the author of the much-loved series of seven Harry Potter novels, originally published between 1997 and 2007.' },
    { name: 'Tony Robbins', bio: 'Tony Robbins is an entrepreneur, best-selling author, philanthropist and the nation\'s #1 Life and Business Strategist.' },
  ];
for (author of authors) {
    var newAuthor = new Author(author);
    await newAuthor.save();
  }
const a = await Author.find();
  console.log('authors: ', a);
}
async function seedBooks() {
  console.log('Seeding books to ' + mongoose.connection.name + '...');
const jkRowling = await Author.findOne({ name: 'JK Rowling' });
  const tonyRobbins = await Author.findOne({ name: 'Tony Robbins' });
let harryPotter = new Book({ title: 'Harry Potter', author: jkRowling._id });
  let awakenGiant = new Book({ title: 'Awaken the Giant Within', author: tonyRobbins._id });
await harryPotter.save();
  await awakenGiant.save();
jkRowling.books.push(harryPotter);
  tonyRobbins.books.push(awakenGiant);
await jkRowling.save();
  await tonyRobbins.save();
}
seedAuthors();
seedBooks();

This will create a new connection to the MongoDB database and then convert a normal array of JavaScript objects into data we can persistently access. The author will have an array of books with one book for each author in the array. To add more books, we can push to the books array and save the changes. Each book will have one author. MongoDB stores these relationships via the id. Using the populate method in our controller above, we'll be able to view the entire object.

After running the seeder, you should be able to see your records in MongoDB Compass, as shown below. Compass is a GUI for viewing, creating, deleting, querying and editing MongoDB data.

Test The API

Now, to view this data from MongoDB via the API, start the Node server with npm run start and visit localhost:3000/api/authors in the web browser.

The final data will look something like:

[ 
   { 
      "_id":"5d51ea23acaf6f3380bcab56",
      "updatedAt":"2019-08-12T22:38:46.925Z",
      "createdAt":"2019-08-12T22:37:23.430Z",
      "name":"JK Rowling",
      "bio":"J.K. Rowling is the author of the much-loved series of seven Harry Potter novels, originally published between 1997 and 2007.",
      "__v":1,
      "books":[ 
         { 
            "_id":"5d51ea76f607f9339d5a76f6",
            "updatedAt":"2019-08-12T22:38:46.919Z",
            "createdAt":"2019-08-12T22:38:46.919Z",
            "title":"Harry Potter",
            "author":"5d51ea23acaf6f3380bcab56",
            "__v":0
         }
      ]
   },
   { 
      "_id":"5d51ea23acaf6f3380bcab57",
      "updatedAt":"2019-08-12T22:38:46.937Z",
      "createdAt":"2019-08-12T22:37:23.475Z",
      "name":"Tony Robbins",
      "bio":"Tony Robbins is an entrepreneur, best-selling author, philanthropist and the nation's #1 Life and Business Strategist.",
      "__v":1,
      "books":[ 
         { 
            "_id":"5d51ea76f607f9339d5a76f7",
            "updatedAt":"2019-08-12T22:38:46.921Z",
            "createdAt":"2019-08-12T22:38:46.921Z",
            "title":"Awaken the Giant Within",
            "author":"5d51ea23acaf6f3380bcab57",
            "__v":0
         }
      ]
   }
]

Congratulations, you’ve built an API with Node.js, Express 4 and MongoDB!

Lastly, a word from the Jscrambler team — before shipping your web apps, make sure you are protecting their JavaScript source code against reverse-engineering, abuse, and tampering. 2 minutes is all it takes to begin your free Jscrambler trial and start protecting JavaScript.

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 ❤