NodeJS

Created
TypeWeb
LanguageJavascript
Last Edit

Basics

Description

Is a JavaScript runtime built on Chrome’s V8 JavaScript engine.

Open source and cross-platform run-time env for executing javascript code outside of browser.

Used often for backend services.

How It Works

Has non-blocking async nature: Single thread will serve multiple requests when data is being fetched for another request

Blocking sync nature: multiple
thread for multiple requests since each thread waits for data to be fetched.

NodeJS uses EventQueue for managing requests, data returns etc

Architecture

Node.exe is a C++ program which contains Google V8 javascript engine. It contains additional modules like
filesystem, network etc (not available in browsers)

Ideal For

i/o intensive apps: Excessive disk or network access

⚠️
Node should not be used for CPU-intensive apps like video encoding, image manipulation services

Installation

Installation of Node 14 on M1 using Rosetta

$ arch 
# O/p: arm
# Switch to 64
$ arch -x86_64 zsh 
$ arch
# O/p: i386
$ nvm install 14

Module System

Index | Node.js v18.9.1 Documentation
https://nodejs.org/dist/latest-v16.x/docs/api/

ESM

💡
ECMAScript Modules (ESM) can now be used in nodejs projects so that require can be replaced with import statements.

Include "type": "module", in package.json file.

FileSystem Module

Create file with content and append data to it:

const fs = require("fs");

//Write a file
fs.writeFileSync("notes.txt", "This file was created by Node.js!");
//append data to existing file
fs.appendFileSync("notes.txt", " New line appended by node");

Working with JSON

Convert and store JSON data:

const fs = require("fs");

const data = {
  author: "Emily",
  title: "Ego is the Enemy",
};

fs.writeFileSync("1-json.json", JSON.stringify(data));

Read and convert JSON data:

const fs = require("fs");

const dataBuffer = fs.readFileSync("1-json.json");
const dataJSON = dataBuffer.toString();
const data = JSON.parse(dataJSON);

console.log(data);
console.log(data.title);

User Defined Modules

Following code can be used to import local files.

require("./utils.js");
⚠️
Modules have local scope. So in modules it has to be defined which variables and functions can be exported to outside.

utils1.js

const username = "Username";


module.exports = username;

utils2.js

const add = function(a,b) {
	return a + b;
}
module.exports = add

app.js

const name = require("./utils1.js");
console.log(name);

const add = require("./utils2.js");
console.log(add(2,5));

Multiple exports

module.exports = { 
	getNotes: getNotes, 
	addNote: addNote 
};
//or
module.exports = { getNotes, addNote, removeNote };
const notes = require("./notes.js");

notes.getNotes();
notes.addNote(title,body);

NPM

Running following command in terminal to setup package.json to use npm packages.

npm init

After installing a particular package using npm i, it can be imported as follows:

const validator = require("validator");
console.log(validator.isEmail("example.com"));

Chalk

NPM package used to stylise console message.

const chalk = require("chalk");

const error = chalk.bgRed;
const success = chalk.bold.green; // or chalk.green.bold
const warning = chalk.hex("#FFA500"); // Orange color

console.log(success("Success!"));
console.log(warning("Warning!"));
console.log(error("Error!"));

Nodemon

NPM package used to hot reload on save.

nodemon app.js
"scripts": {
    "start": "",
    "dev": "nodemon src/index.js"
  },
npm run dev

Yargs

Package used to parse command line arguments

Yargs Setup

const yargs = require("yargs");

// Customize yargs version
yargs.version("1.1.0");

// Setup commands
yargs.command({
  command: "add",
  describe: "Add a new note",
  handler: function () {
    console.log("Adding a new note!");
  },
});
yargs.command({
  command: "remove",
  describe: "Remove a note",
  handler() {
    console.log("Removing the note!");
  },
});

console.log(yargs.argv);
node app.js add #will print Adding a new note

node app.js —version will print current version of yargs setup

node app.js —help will print all commands setup

⚠️
All of these prints happen only if console.log(yargs.argv) or yargs.parse() is present.

Yargs Builder

yargs.command({
  command: "add",
  describe: "Add a new note",
  builder: {
    title: {
      describe: "Note title",
      demandOption: true,
      type: "string",
    },
  },
  handler(argv) {
    console.log("Title: " + argv.title);
  },
});

node app.js add will now show title as required and of type string.

Bcrypt

A library to help you hash passwords.

const bcrypt = require("bcrypt");

const hashFunction = async () => {
  const password = "Red12345!";
  const hashedPassword = await bcrypt.hash(password, 8);

  const isMatch = await bcrypt.compare("Red12345!", hashedPassword);
  console.log(isMatch);
};
hashFunction();

Jsonwebtoken

npm i jsonwebtoken

Multer

Multi-part upload handler. Used for file uploads.

npm i multer

Sharp

Convert large images in common formats to smaller, web-friendly JPEG, PNG, WebP, GIF and AVIF images of varying dimensions.

npm i sharp

Debugging

Setup node debugging using inspect shell command:

node inspect app.js read --title "New Item 2"

Edge

Goto URL edge://inspect

Chrome

Goto URL chrome://inspect

Breakpoint

Adding the following line of code in place where you want to pause execution of code:

debugger;

After code execution is paused, local variables can be printed in the console using their respective names.

Debugger> restart (shell) can be used to restart session in chrome/edge after closing the console window.

Async Basics

Event loop can’t run async callbacks until call stack is empty.

console.log("START");

setTimeout(() => {
  console.log("2 Second Timer");
}, 2000);

setTimeout(() => {
  console.log("0 Second Timer");
}, 0);

console.log("STOP");

Output:

START
STOP
0 Second Timer
2 Second Timer

HTTP Requests

Can use following packages to make HTTP Request from NodeJS:

Error Handling

Axios

const params = {
  access_key: "d4720a6be64ce712d4e1a31cf1d6caa4",
  query: "12what",
};
const url = "http://api.weatherstack.com/current";

axios
  .get(url, { params })
  .then((response) => {
    if (!response.data.success && response.data.error) throw 404;
    else
      console.log(
        `${response.data.current.weather_descriptions[0]}. It is currently ${response.data.current.temperature} degress out. There is a ${response.data.current.precip}% chance of rain.`
      );
  })
  .catch((error) => {
    if (error === 404) {
      console.log("Unable to find location.");
    } else {
      console.log("Unable to connect to location services.");
    }
  });

Express Library

Fetch From Browser

Accesssing weather route to get weather

index.hbs

<body>
    <div class="main-content">
        {{>header}}

        <p>Use this site to get your weather!</p>
        <form>
            <input placeholder="Location"/>
            <button>Get Weather</button>
        </form>

        <p id="address"></p>
        <p id="forecast"></p>
    </div>

    {{>footer}}
    <script src="/js/app.js"></script>
</body>

js/app.js:

const weatherForm = document.querySelector("form");
const search = document.querySelector("input");

const address = document.querySelector("#address");
const forecast = document.querySelector("#forecast");

weatherForm.addEventListener("submit", (e) => {
  e.preventDefault();
  address.textContent = "Loading...";
  forecast.textContent = "";
  fetch("/weather?location=" + search.value).then((response) => {
    response.json().then((data) => {
      if (data.error) {
        address.textContent = data.error;
      } else {
        address.textContent = data.location;
        forecast.textContent = data.forecast;
      }
    });
  });
});

nodejs: src/app.js:

app.get("/weather", (req, res) => {
  if (!req.query.location) {
    return res.send({
      error: "You must provide a location",
    });
  }
  params.query = req.query.location;
  weatherInfo(params, (data, error) => {
    if (error)
      return res.send({
        error: error,
      });
    return res.send(data);
  });
});

MongoDB

Mongoose

Authentication

User Schema Update

const userSchema = new mongoose.Schema({
//...
tokens: [
    {
      token: {
        type: String,
        required: true,
      },
    },
  ],
//...
});

JWT (JSON Web Token)

const jwt = require("jsonwebtoken");

const tokenFunction = async () => {
  const token = jwt.sign({ _id: "abc123" }, "thisismynewcourse", {
    expiresIn: "7 days",
  });
  console.log(token);

  const data = jwt.verify(token, "thisismynewcourse");
  console.log(data);
};

Generate JWT For A User

// JWT Token generate
userSchema.methods.generateAuthToken = async function () {
  const user = this;

  const token = jwt.sign({ _id: user._id.toString() }, "thisismynewcourse");
  user.tokens = user.tokens.concat({ token });
  await user.save();

  return token;
};

Login Request

Unauthorised request

router.post("/users/login", async (req, res) => {
  try {
    const user = await User.findByCredentials(
      req.body.email,
      req.body.password
    );
    const token = await user.generateAuthToken();
    res.send({ user, token });
  } catch (error) {
    res.status(400).send(error);
  }
});

Create User Request

Unauthorised request

router.post("/users", async (req, res) => {
  const user = new User(req.body);
  try {
    await user.save();
    const token = await user.generateAuthToken();
    res.status(201).send({ user, token });
  } catch (e) {
    res.status(400).send(e);
  }
});

Middleware Auth

const jwt = require("jsonwebtoken");
const User = require("../models/user");

const auth = async (req, res, next) => {
  try {
    const token = req.header("Authorization").replace("Bearer ", "");
    const decoded = jwt.verify(token, "thisismynewcourse");
    const user = await User.findOne({ _id: decoded._id, "tokens.token": token });
    if (!user) throw new Error();

    req.user = user; // storing user in req.user so that route handler can access it
		req.token = token;
    next();
  } catch (error) {
    res.status(401).send({ error: "Please authenticate." });
  }
};

module.exports = auth;

Login

const auth = require("../middleware/auth");

router.get("/users/profile", auth, async (req, res) => {
  res.send(req.user);
});

Logout

router.post("/users/logout", auth, async (req, res) => {
  try {
    req.user.token = req.user.tokens.filter(
      (token) => token.token !== req.token
    );
    await req.user.save();
    res.send();
  } catch (error) {
    res.status(500).send(error);
  }
});

Logout All

Logout from all active sessions (devices).

router.post("/users/logoutAll", auth, async (req, res) => {
  try {
    req.user.tokens = [];
    await req.user.save();
    res.send();
  } catch (error) {
    res.status(500).send(error);
  }
});

Query Params

Data Filtering

router.get("/tasks", auth, async (req, res) => {
  const match = {};
  if (req.query.completed) {
    match.completed = req.query.completed === "true";
  }
  try {
    await req.user.populate({
      path: "tasks",
      match,
    });
    res.send(req.user.tasks);
  } catch (error) {
    res.status(500).send(error.message);
  }
});

If request url is in the form: {{host}}/tasks?completed=false then, match object will contain completed as false and only those tasks will be returned.

Pagination

// GET /tasks?limit=10&skip=0
router.get("/tasks", auth, async (req, res) => {
  const options = {};

  if (req.query.limit) {
    options.limit = parseInt(req.query.limit);
  }

  if (req.query.skip) {
    options.skip = parseInt(req.query.skip);
  }

  try {
    await req.user.populate({
      path: "tasks",
      match,
      options,
    });
    res.send(req.user.tasks);
  } catch (error) {
    res.status(500).send(error.message);
  }
});

Sorting

// GET /tasks?sortBy=createdAt:desc
router.get("/tasks", auth, async (req, res) => {
  const options = {};
  if (req.query.sortBy) {
    const parts = req.query.sortBy.split(":");
    options.sort = {};
    options.sort[parts[0]] = parts[1] === "desc" ? -1 : 1;
  }

  try {
    await req.user.populate({
      path: "tasks",
      match,
      options,
    });
    res.send(req.user.tasks);
  } catch (error) {
    res.status(500).send(error.message);
  }
});

Websockets

ws package

import { WebSocketServer } from "ws";
import ws from "../src/config/ws.js";

const websocketport = process.env.WEBSOCKET_PORT || 443;

const wss = new WebSocketServer({
	port: websocketport,
	perMessageDeflate: false,
});
console.log(`WebSocket is running on port ${websocketport}`);

wss.on("connection", (ws) => {
	console.log("Client connected");
	clients.add(ws);

	// Handle messages from the client
	ws.on("message", (message) => {
		console.log(`Received: ${message}`);
		// Echo the message back to the client
		ws.send(`Server received: ${message}`);
	});

	ws.on("error", (error) => {
		console.error("WebSocket error:", error);
	});

	ws.on("close", () => {
		console.log("Client disconnected");
		clients.delete(ws);
	});
});
const clients = new Set();

const broadcast = (message) => {
	clients.forEach((client) => {
		if (client.readyState === WebSocket.OPEN) {
			client.send(message);
		}
	});
};

export default { clients, broadcast };