Securing MEAN Stack (Angular 5) Web Application using Passport

by Didin J. on Feb 17, 2018 Securing MEAN Stack (Angular 5) Web Application using Passport

Step by step tutorial on securing MEAN (MongoDB, Express.js, Angular 5, Node.js) Stack Web Application using Passport.js

A comprehensive step by step tutorial on securing MEAN (MongoDB, Express.js, Angular 5, Node.js) Stack Web Application using Passport.js. Securing web application in this tutorial is make a specific web page accessible only to the authorized user. That's the point of using Passport.js is to make authentication system for MEAN (Angular 5) stack web application. For the MEAN stack with Angular 5, you can find in the previous tutorial. Now, we add the passport.js module for authenticating user. Don't worry, we always create a tutorial from scratch although we use same steps as the previous tutorial.

We will use RESTful API security approach for this tutorial because of it simple to do. Passport.js will handle authority that comes to RESTful API request then response the right Authentication code for the client (Angular 5).

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

- Node.js
- MongoDB
- Angular 5
- Express.js
- Passport.js
- Terminal or Command Line
- IDE or Text Editor

Before the move to the main steps of this tutorial, make sure that you have installed Node.js and MongoDB on your machine. You can check the Node.js version after installing it from the terminal or Node.js command line.

node -v

We use this version for this tutorial.

v8.9.4


1. Create Angular 5 Application using Angular CLI

As usual, we starting the tutorial from scratch which means start from zero. First, we have to install the Angular CLI. Go to your Node project folder then type this command for installing the Angular-CLI.

sudo npm install -g @angular/cli

You can use `sudo` if using Mac/Linux terminal, it's not necessary when using Windows command line. Next, type this command to create new Angular 5 application.

ng new mean-secure

That command will create a new Angular 5 app, also installing required `npm` modules. Go to the newly created application folder.

cd ./mean-secure

Run the Angular 5 application by typing this command.

ng serve

If you find the error below, install the module that shows in the error message.

Error: Cannot find module '@angular-devkit/core'

npm install --save-dev @angular-devkit/core

Now, run again the Angular 5 application.

ng serve

Open the browser then go to this URL `localhost:4200` and you will see this Angular 5 landing page.

Securing MEAN Stack (Angular 5) Web Application using Passport - Angular 5 Home


2. Combine Angular 5 with Express.js

Next, we will use Express.js as web and RESTful API server. After close the running Angular 5 application by press `CTRL+C`, type this command to install the required Express modules and dependencies.

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

Add a new folder named `bin` to the root of project folder and `www` file inside that folder. Actually, you are free to put or name starting file anywhere you like.

mkdir bin
touch ./bin/www

Open and edit `./bin/www` file then add this lines of codes.

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('mean-secure: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, we have to change the starting application to Express.js by open and edit `package.json` then change `start` value in `script` block.

"scripts": {
  "ng": "ng",
  "start": "ng build && node ./bin/www",
  "build": "ng build",
  "test": "ng test",
  "lint": "ng lint",
  "e2e": "ng e2e"
},

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

touch app.js

Open and edit app.js then add all these 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 api = require('./routes/api');
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('/', express.static(path.join(__dirname, 'dist')));
app.use('/api', api);

// 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 profile RESTful API endpoint.

mkdir routes
touch routes/api.js

Open and edit `routes/api.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;

Now, run the server using this command.

npm start

You will see the previous Angular landing page when you point your browser to `http://localhost:3000`. When you change the address to `http://localhost:3000/api/profile` you will see RESTful API response in the browser.

Securing MEAN Stack (Angular 5) Web Application using Passport - Express RESTful API


3. Setup Mongoose.js and Passport.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 bcrypt-nodejs jsonwebtoken morgan passport passport-jwt

We will make separate files for the configuration. For that, create the new folder in the root folder.

mkdir config

Create a configuration file for Database and Passport.js.

touch config/database.js
touch config/passport.js

Open and edit `config/database.js` then add this lines of codes.

module.exports = {
  'secret':'meansecure',
  'database': 'mongodb://localhost/mean-secure'
};

This config holds database connection parameter and secret for generating JWT token. Next, open and edit `config/passport.js` then add this lines of codes.

var JwtStrategy = require('passport-jwt').Strategy,
    ExtractJwt = require('passport-jwt').ExtractJwt;

// load up the user model
var User = require('../models/user');
var config = require('../config/database'); // get db config file

module.exports = function(passport) {
  var opts = {};
  opts.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme("jwt");
  opts.secretOrKey = config.secret;
  passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
    User.findOne({id: jwt_payload.id}, function(err, user) {
          if (err) {
              return done(err, false);
          }
          if (user) {
              done(null, user);
          } else {
              done(null, false);
          }
      });
  }));
};

This config is used for getting user by matching JWT token with token get from the client. This configuration needs to create User model later.

Now, Open and edit `app.js` then declare required library for initializing with the server by adding this lines of requires.

var morgan = require('morgan');
var mongoose = require('mongoose');
var passport = require('passport');
var config = require('./config/database');

Create a connection to MongoDB.

mongoose.Promise = require('bluebird');
mongoose.connect(config.database, { promiseLibrary: require('bluebird') })
  .then(() =>  console.log('connection succesful'))
  .catch((err) => console.error(err));

Declare a variable for API route.

var api = require('./routes/api');

Initialize passport by add this line.

app.use(passport.initialize());

Add API route to the endpoint URL after other `use` function.

app.use('/api', api);


4. Create Mongoose.js Models

First, we have to create a folder for holds all model files. Create and name it `models` in the root of the project folder.

mkdir models

Create new Javascript file that uses for Mongoose.js model. We will create a model of User collection and Book collection.

touch models/User.js
touch models/Book.js

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

var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var bcrypt = require('bcrypt-nodejs');

var UserSchema = new Schema({
  username: {
        type: String,
        unique: true,
        required: true
    },
  password: {
        type: String,
        required: true
    }
});

UserSchema.pre('save', function (next) {
    var user = this;
    if (this.isModified('password') || this.isNew) {
        bcrypt.genSalt(10, function (err, salt) {
            if (err) {
                return next(err);
            }
            bcrypt.hash(user.password, salt, null, function (err, hash) {
                if (err) {
                    return next(err);
                }
                user.password = hash;
                next();
            });
        });
    } else {
        return next();
    }
});

UserSchema.methods.comparePassword = function (passw, cb) {
    bcrypt.compare(passw, this.password, function (err, isMatch) {
        if (err) {
            return cb(err);
        }
        cb(null, isMatch);
    });
};

module.exports = mongoose.model('User', UserSchema);

On than schema you will find function of password encryption and password comparison for login purpose. We use `Bcrypt` library for password encryption. Next, open and edit "models/book.js" then add this lines of codes.

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

var BookSchema = new Schema({
  isbn: {
    type: String,
    required: true
  },
  title: {
    type: String,
    required: true
  },
  author: {
    type: String,
    required: true
  },
  publisher: {
    type: String,
    required: true
  }
});

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


5. Create Router for Authentication and Book RESTful API

We will create Router for authenticating the user and restrict book resources. In the routes folder creates new Javascript file by type this command.

touch routes/api.js

Open and edit `routes/api.js` then declares all requires variables.

var mongoose = require('mongoose');
var passport = require('passport');
var config = require('../config/database');
require('../config/passport')(passport);
var express = require('express');
var jwt = require('jsonwebtoken');
var router = express.Router();
var User = require("../models/user");
var Book = require("../models/book");

Create router for signup or register the new user.

router.post('/signup', function(req, res) {
  if (!req.body.username || !req.body.password) {
    res.json({success: false, msg: 'Please pass username and password.'});
  } else {
    var newUser = new User({
      username: req.body.username,
      password: req.body.password
    });
    // save the user
    newUser.save(function(err) {
      if (err) {
        return res.json({success: false, msg: 'Username already exists.'});
      }
      res.json({success: true, msg: 'Successful created new user.'});
    });
  }
});

Create router for login or sign-in.

router.post('/signin', function(req, res) {
  User.findOne({
    username: req.body.username
  }, function(err, user) {
    if (err) throw err;

    if (!user) {
      res.status(401).send({success: false, msg: 'Authentication failed. User not found.'});
    } else {
      // check if password matches
      user.comparePassword(req.body.password, function (err, isMatch) {
        if (isMatch && !err) {
          // if user is found and password is right create a token
          var token = jwt.sign(user.toJSON(), config.secret);
          // return the information including token as JSON
          res.json({success: true, token: 'JWT ' + token});
        } else {
          res.status(401).send({success: false, msg: 'Authentication failed. Wrong password.'});
        }
      });
    }
  });
});

Create router for add new book that only accessible to authorized user.

router.post('/book', passport.authenticate('jwt', { session: false}), function(req, res) {
  var token = getToken(req.headers);
  if (token) {
    console.log(req.body);
    var newBook = new Book({
      isbn: req.body.isbn,
      title: req.body.title,
      author: req.body.author,
      publisher: req.body.publisher
    });

    newBook.save(function(err) {
      if (err) {
        return res.json({success: false, msg: 'Save book failed.'});
      }
      res.json({success: true, msg: 'Successful created new book.'});
    });
  } else {
    return res.status(403).send({success: false, msg: 'Unauthorized.'});
  }
});

Create router for getting list of books that accessible for authorized user.

router.get('/book', passport.authenticate('jwt', { session: false}), function(req, res) {
  var token = getToken(req.headers);
  if (token) {
    Book.find(function (err, books) {
      if (err) return next(err);
      res.json(books);
    });
  } else {
    return res.status(403).send({success: false, msg: 'Unauthorized.'});
  }
});

Create function for parse authorization token from request headers.

getToken = function (headers) {
  if (headers && headers.authorization) {
    var parted = headers.authorization.split(' ');
    if (parted.length === 2) {
      return parted[1];
    } else {
      return null;
    }
  } else {
    return null;
  }
};

Finally, export router as a module.

module.exports = router;


6. Create Angular 5 Component for Login

To create Angular 5 Component, simply run this command.

ng g component login

That command will generate all required files for build login component and also automatically added login component to app.module.ts.

create src/app/login/login.component.css (0 bytes)
create src/app/login/login.component.html (24 bytes)
create src/app/login/login.component.spec.ts (621 bytes)
create src/app/login/login.component.ts (265 bytes)
update src/app/app.module.ts (468 bytes)

Before add any functionality to the component, we need to add `HttpClientModule` to `app.module.ts`. Open and edit `src/app/app.module.ts` then add this import.

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

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

imports: [
  BrowserModule,
  FormsModule,
  HttpClientModule
],

Now, we will posting to login RESTful API using this Angular `HttpClient` module. Open and edit `src/app/login/login.component.ts` then add this import.

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from "@angular/router";
import { Observable } from 'rxjs/Observable';
import { tap, catchError } from 'rxjs/operators';
import { of } from 'rxjs/observable/of';

Inject `HttpClient` and `Router` to the constructor.

constructor(private http: HttpClient, private router: Router) { }

Add object variable for holding login data, message and response data before the constructor.

loginData = { username:'', password:'' };
message = '';
data: any;

Add login function and a function for catch error response.

login() {
  this.http.post('/api/signin',this.loginData).subscribe(resp => {
    this.data = resp;
    localStorage.setItem('jwtToken', this.data.token);
    this.router.navigate(['books']);
  }, err => {
    this.message = err.error.msg;
  });
}

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

<div class="container">
    <form class="form-signin" (ngSubmit)="login()" #loginForm="ngForm">
        <div class="alert alert-warning alert-dismissible" role="alert" *ngIf="message !== ''">
          <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
          {{message}}
        </div>
        <h2 class="form-signin-heading">Please sign in</h2>
        <label for="inputEmail" class="sr-only">Email address</label>
        <input type="email" class="form-control" placeholder="Email address" [(ngModel)]="loginData.username" name="username" required/>
        <label for="inputPassword" class="sr-only">Password</label>
        <input type="password" class="form-control" placeholder="Password" [(ngModel)]="loginData.password" name="password" required/>
        <button class="btn btn-lg btn-primary btn-block" type="submit" [disabled]="!loginForm.form.valid">Sign in</button>
        <p>
            Not a member? <a [routerLink]="['/signup']">Signup here</a>
        </p>
    </form>
</div>

Give a style to the login page. Open and edit `src/app/login/login.component.css` then add this styles.

body {
  padding-top: 40px;
  padding-bottom: 40px;
  background-color: #eee;
}

.form-signin {
  max-width: 330px;
  padding: 15px;
  margin: 0 auto;
}
.form-signin .form-signin-heading,
.form-signin .checkbox {
  margin-bottom: 10px;
}
.form-signin .checkbox {
  font-weight: normal;
}
.form-signin .form-control {
  position: relative;
  height: auto;
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
  padding: 10px;
  font-size: 16px;
}
.form-signin .form-control:focus {
  z-index: 2;
}
.form-signin input[type="email"] {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
.form-signin p {
  margin-top: 10px;
}

Above HTML tags include style class from Bootstrap CSS library. Open and edit `src/index.html` then add the Bootstrap CSS and JS library.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MeanSecure</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">

  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <!-- Optional theme -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
</head>
<body>
  <app-root></app-root>

  <!-- Latest compiled and minified JavaScript -->
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
</body>
</html>


7. Create Angular 5 Component for Signup

Same as the previous step, run this command to create a new Angular 5 component.

ng g component signup

Open and edit `src/app/signup/signup.component.ts` then add this import.

import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from "@angular/router";
import { Observable } from 'rxjs/Observable';
import { tap, catchError } from 'rxjs/operators';
import { of } from 'rxjs/observable/of';

Inject `HttpClient` and `Router` to the constructor.

constructor(private http: HttpClient, private router: Router) { }

Add object variable for holding login data and message before the constructor.

signupData = { username:'', password:'' };
message = '';

Add a function for sign up.

signup() {
  this.http.post('/api/signup',this.signupData).subscribe(resp => {
    console.log(resp);
    this.router.navigate(['login']);
  }, err => {
    this.message = err.error.msg;
  });
}

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

<div class="container">
    <form class="form-signin" (ngSubmit)="signup()" #signupForm="ngForm">
        <div class="alert alert-warning alert-dismissible" role="alert" *ngIf="message !== ''">
          <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
          {{message}}
        </div>
        <h2 class="form-signin-heading">Sign Up Now</h2>
        <label for="inputEmail" class="sr-only">Email address</label>
        <input type="email" class="form-control" placeholder="Email address" [(ngModel)]="signupData.username" name="username" required/>
        <label for="inputPassword" class="sr-only">Password</label>
        <input type="password" class="form-control" placeholder="Password" [(ngModel)]="signupData.password" name="password" required/>
        <button class="btn btn-lg btn-primary btn-block" type="submit" [disabled]="!signupForm.form.valid">Sign Up</button>
    </form>
</div>

Give a style to the signup page. Open and edit `src/app/signup/signup.component.css` then add this styles.

body {
  padding-top: 40px;
  padding-bottom: 40px;
  background-color: #eee;
}

.form-signin {
  max-width: 330px;
  padding: 15px;
  margin: 0 auto;
}
.form-signin .form-signin-heading,
.form-signin .checkbox {
  margin-bottom: 10px;
}
.form-signin .checkbox {
  font-weight: normal;
}
.form-signin .form-control {
  position: relative;
  height: auto;
  -webkit-box-sizing: border-box;
          box-sizing: border-box;
  padding: 10px;
  font-size: 16px;
}
.form-signin .form-control:focus {
  z-index: 2;
}
.form-signin input[type="email"] {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
.form-signin p {
  margin-top: 10px;
}


7. Create Angular 5 Component for Book

Same as the previous step, run this command to create a new Angular 5 component.

ng g component book

Open and edit `src/app/book/book.component.ts` then add this import.

import { Component, OnInit } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Router } from "@angular/router";
import { Observable } from 'rxjs/Observable';
import { tap, catchError } from 'rxjs/operators';
import { of } from 'rxjs/observable/of';

Inject `HttpClient` and `Router` to the constructor.

constructor(private http: HttpClient, private router: Router) { }

Add array variable for holding books data before the constructor.

books: any;

Add a few lines of codes for getting a list of book data from RESTful API inside `ngOnInit` function.

ngOnInit() {
  let httpOptions = {
    headers: new HttpHeaders({ 'Authorization': localStorage.getItem('jwtToken') })
  };
  this.http.get('/api/book', httpOptions).subscribe(data => {
    this.books = data;
    console.log(this.books);
  }, err => {
    if(err.status === 401) {
      this.router.navigate(['login']);
    }
  });
}

Add a function for log out by simply delete JWT token.

logout() {
  localStorage.removeItem('jwtToken');
  this.router.navigate(['login']);
}

Now, we can display the book list on the page. Open and edit `src/app/book/book.component.html` then replace all tags with this lines of HTML tags.

<div class="container">
  <h1>Book List <button class="btn btn-success" (click)="logout()">Logout</button></h1>
  <table class="table">
    <thead>
      <tr>
        <th>ISBN</th>
        <th>Title</th>
        <th>Author</th>
        <th>Publisher</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let book of books">
        <td>{{ book.isbn }}</td>
        <td>{{ book.title }}</td>
        <td>{{ book.author }}</td>
        <td>{{ book.publisher }}</td>
      </tr>
    </tbody>
  </table>
</div>


8. Create Angular 5 Router for All Pages

To make navigation working on Angular 5, we need to add Router to the main module. Open and edit `src/app/app.module.ts` then add this import.

import { RouterModule, Routes } from '@angular/router';

Create a constant for routing before `@NgModule`.

const appRoutes: Routes = [
  {
    path: 'books',
    component: BookComponent,
    data: { title: 'Book List' }
  },
  {
    path: 'login',
    component: LoginComponent,
    data: { title: 'Login' }
  },
  {
    path: 'signup',
    component: SignupComponent,
    data: { title: 'Sign Up' }
  },
  { path: '',
    redirectTo: '/books',
    pathMatch: 'full'
  }
];

Add that router to `@NgModule` imports.

imports: [
  BrowserModule,
  FormsModule,
  HttpClientModule,
  RouterModule.forRoot(
    appRoutes,
    { enableTracing: true } // <-- debugging purposes only
  )
],

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

<router-outlet></router-outlet>

To make `books` routing accessible via URL, open and edit `app.js` then change this line.

app.use('/', express.static(path.join(__dirname, 'dist')));

To

app.use('/books', express.static(path.join(__dirname, 'dist')));


9. Run and Test The MEAN (Angular 5) Stack Secure Web Application

Before running the MEAN stack application, run MongoDB server in another terminal tab.

mongod

Now, run the application.

npm start

Now, open the browser then go to this URL `localhost:3000`. You will be redirected to the login page.

Securing MEAN Stack (Angular 5) Web Application using Passport - Login Page

Next, signup with email and password. After login successful, you will be entering the Book List page.

That it's for now. You can get the full source code for comparison with this tutorial on our GitHub.

That just the basic. If you need more deep learning about MEAN Stack, Angular, and Node.js, you can find the following books:

For more detailed on MEAN stack and Node.js, you can take the following course:

Thanks!

The following resources might be useful for you:

Loading…