Let's Learn NodeJS

Overview of NodeJS

What is NodeJS?

  • NodeJS is an open-source, cross-platform, JavaScript runtime environment.

  • You can run JavaScript outside of the browser.

  • JavaScript can also talk to native machine because of C++.

  • You can create web servers in JavaScript Language.

Installation of NodeJS

  • Go to Node.js and download the Long-term support (LTS) version.

  • Check your Node version in terminal using node -v or node --version

  • Check you NPM version in terminal using npm -v or npm --version

  • Node Package Manager (NPM) is automatically install with Node.js

Use REPL

  • REPL stands for Read Evaluate Print Loop.

  • In your terminal execute node command to use REPL.

First code in NodeJS

Create a file: fileName.js in your code editor like VS Code.

console.log("Hello! This is my first Node.js code");

Execute the above code in your own terminal. Run node fileName.js or node fileName and your code will executed inside your terminal (outside the browser).

Note:window objects and DOM features are excluded from Node, but file system, HTTP Module, Asynchronous and Event-Driven.

When creating a new project, we should initialize npm by running the npm init command. This command creates a file called package.json, which is a configuration file.

In package.json, we should create our own scripts inside the scripts object for executing code. Scripts like 'start': node fileName.

Modules in NodeJS

Module in Node.js is a simple or complex functionality organized in single or multiple JavaScript files which can be reused throughout the Node.js application.

Modular programming is a method for designing software that divides the program into smaller, self-contained parts called modules. Each module handles a specific task or feature, making it easier to manage and reuse code.

Create math.js

function add(a,b) {
    return a+b;
}

function sub(a,b) {
    return a-b;
}

console.log(add(2,5)); // => 7

module.exports = add; 
module.exports = "Mark"; // It overrides the export. 
// To get rid of the override, you should use a JavaScript object.
module.exports = {addFn:add, subFn:sub};

OR we use anonymous function using exports.propertyName

exports.add = (a,b) => a+b;
exports.sub = (a,b) => a-b;

Use require similar to import to import one module from another module.

Create index.js

const math = require('./math.js')
// OR destructures the function from math.js
const { addFn, subFn } = require('./math.js')

console.log("Math value is " + math) // => Math value is Mark
console.log(math.addFn(3,5)) // => 8
console.log(math.subFn(10,5)) // => 5

Note:./ means current directory.

File Handling in NodeJS

Reading and Writing the file in Node.js is done by using one of the built-in Node.js modules called fs module.

The Node.js file system module allows you to work with the file system on your computer.

The file can be read and written in node.js in both Synchronous and Asynchronous ways.

Synchronous methods block the code execution until they finish, while Asynchronous methods allow the code to continue running while they complete their tasks. Asynchronous methods often use callbacks or return promises to handle completion or errors.

Import fs module

const fs = require('fs');

Common use for the File System module:

  • Create files

  • Read files

  • Update files

  • Delete files

  • Rename files

Create files

The methods for creating new files:

  • fs.writeFileSync()

      fs.writeFileSync('./textSync.txt', 'Hello World Sync');
    
  • fs.writeFile()

      fs.writeFile('./textAsync.txt', 'Hello World Async', (error) => {
          if(error) throw err;
          console.log('Saved!');
      });
    
  • fs.appendFileSync()

      fs.appendFileSync('./textSync.txt', `\nYou access at ${Date.now()}`)
    
  • fs.appendFile()

      fs.appendFile('./textAsync.txt', `\nYou access at ${Date.now()}`, (error) => {
          error ? console.log("Error", err) : console.log('Saved!');
      })
    
  • fs.open()

      // File path
      const filePath = 'newfile.txt';
    
      // Data to write to the file
      const data = 'Hello, this is a new file created using Node.js!';
    
      // Open the file in write mode ('w')
      fs.open(filePath, 'w', (err, fileDescriptor) => {
        if (err) {
          console.error('Error occurred while opening the file:', err);
          return;
        }
    
        // Write data to the file
        fs.write(fileDescriptor, data, (err) => {
          if (err) {
            console.error('Error occurred while writing to the file:', err);
            return;
          }
    
          console.log('Data has been written to the file successfully.');
    
          // Close the file
          fs.close(fileDescriptor, (err) => {
            if (err) {
              console.error('Error occurred while closing the file:', err);
              return;
            }
            console.log('File has been closed.');
          });
        });
      });
    

    fileDescriptor: is the variable that holds the unique ID number (file descriptor) given to the file by the computer. This number is used by your program to work with the file.

Read files

The methods for reading files:

  • fs.readFileSync()

      const result = fs.readFileSync('./textSync.txt', 'utf-8');
      console.log(result);
    
  • fs.readFile()

      fs.readFile('./textAsync.txt', 'utf-8', (error, result) => {
        error ? console.log("Error: ", error) : console.log(result);
      })
    

Update files

  • fs.append()

      fs.appendFile('./textAsync.txt', `\nYou access at ${Date.now()}`, (error) => {
          error ? console.log("Error", err) : console.log('Updated!');
      })
    
  • fs.writeFile()

      fs.writeFile('./textAsync.txt', 'Hello World Async', (error) => {
          if(error) throw err;
          console.log('Replaced!');
      });
    

Delete files

  • fs.unlink()

      fs.unlink('./newfile.txt', (error) => {
        if(error) throw error;
        console.log('File deleted!')
      })
    
  • fs.unlinkSync()

      fs.unlinkSync('./newfile.txt')
    

Rename files

  • fs.rename()

      fs.rename('myfile.txt', 'myrenamedfile.txt', function (err) {
        if (err) throw err;
        console.log('File Renamed!');
      });
    

Other operations:

  • fs.cp()

      fs.cp('./test.txt', './copy.txt', (error) => {
        if(error) throw error;
        console.log('File copied!')
      });
    
  • fs.cpSync()

      fs.cpSync('./text.txt', './copy.txt')
    
  • fs.stat()

      fs.stat('./test.txt', (err, stats) => {
        if (err) {
          console.error(err);
        }
        console.log(stats);
      });
    
  • fs.statSync()

      console.log(fs.statSync('./test.txt'))
    
  • fs.mkdirSync()

      fs.mkdirSync('DSA/Array/Easy', {recursive:true})
    
  • fs.mkdir()

      const path = require('path');
    
      fs.mkdir(path.join(__dirname, 'templates'), error => {
        if(error) console.log(error);
        console.log('Folder was created successfully!')
      })
    

NodeJS Architecture

What is NodeJS Architecture?

Node.js architecture consists of several key components that work together to provide an efficient and scalable runtime environment for JavaScript applications.

  1. V8 Engine:

    • Node.js is built on top of the V8 JavaScript engine, which is developed by Google and also used in the Chrome web browser.

    • V8 is responsible for executing JavaScript code and provides features such as Just-In-Time (JIT) compilation, garbage collection, and optimized performance.

  2. Libuv:

    • Libuv is a cross-platform library that provides asynchronous I/O capabilities, event loop implementation, and other core functionalities for Node.js.

    • It abstracts away the differences in I/O operations between different operating systems, allowing Node.js to be highly portable and efficient.

    • Libuv handles tasks such as file system operations, networking, timers, and threading, making Node.js suitable for building scalable network applications.

  3. Event Loop:

    • The event loop is a fundamental concept in Node.js architecture that enables non-blocking, asynchronous behavior.

    • It allows Node.js to handle multiple I/O operations concurrently without blocking the execution of other code.

    • The event loop continuously checks for events (such as I/O operations, timers, and callbacks) in the event queue and processes them in a single-threaded manner.

    • Asynchronous operations in Node.js are achieved through the event loop and callback functions.

  4. Core Modules:

    • Node.js provides a set of built-in core modules that offer essential functionalities for building applications.

    • Core modules include modules for file system operations (fs), networking (http, https, net), path manipulation (path), and more.

    • These modules are implemented in C/C++ for performance and are exposed to JavaScript through the Node.js runtime environment.

  5. Node.js APIs:

    • Node.js provides additional APIs for interacting with the runtime environment, accessing operating system functionalities, and extending the capabilities of JavaScript applications.

    • APIs include the process object for accessing information about the current Node.js process, the os module for operating system-related tasks, and the util module for utility functions.

  6. Third-Party Modules and NPM:

    • Node.js has a vibrant ecosystem of third-party modules available through the Node Package Manager (NPM).

    • Developers can easily install and use third-party modules to extend the functionality of their Node.js applications, making it easy to integrate with databases, web frameworks, authentication systems, and more.

Overall, Node.js architecture is designed to be efficient, scalable, and suitable for building high-performance network applications that can handle a large number of concurrent connections. Read this article gain a better understanding.

What is blocking and non-blocking?

Blocking OperationsNon-Blocking Operations
Blocking operations are synchronous, meaning the program execution waits until the operation completes before moving on to the next instruction.Non-blocking operations are asynchronous, meaning the program execution continues immediately after the operation is initiated without waiting for it to complete.
During a blocking operation, the entire program execution is paused until the operation finishes.During a non-blocking operation, the program doesn't wait for the operation to finish and can continue executing other tasks.
If there are multiple blocking operations in sequence, each operation will be executed one after the other, blocking the program's execution until all operations are complete.Non-blocking operations are well-suited for I/O-bound tasks where waiting for I/O operations to complete would result in wasted CPU cycles and decreased performance.
Blocking operations are typically used in traditional programming environments.Non-blocking operations are a key feature of Node.js, allowing it to efficiently handle multiple I/O operations concurrently without blocking the event loop.

What is a difference between Sync and Async?

Synchronous (Sync) OperationsAsynchronous (Async) Operations
Synchronous operations execute in a sequential manner, one after the other.Asynchronous operations do not block the program's execution. Instead, they allow the program to continue executing while waiting for the operation to complete.
When a synchronous operation is called, the program waits for it to complete before moving on to the next line of code.When an asynchronous operation is called, it is initiated, and the program moves on to the next line of code without waiting for the operation to finish.
Synchronous operations block the execution of the program until they finish.Asynchronous operations typically accept a callback function that gets executed once the operation completes or encounters an error.
Example: fs.readFileSync() in Node.js, which reads a file synchronously. During the file read operation, the program is blocked until the entire file is read.Example: fs.readFile() in Node.js, which reads a file asynchronously. After initiating the file read operation, the program continues executing while the file is being read. Once the file read is complete, the callback function is called.

Which one should I use?

  • It depends on the specific use case and requirements of your application.

  • Synchronous operations are simpler to understand and use when dealing with simple sequences of actions. However, they can block the event loop and lead to decreased performance, especially in I/O-bound applications.

  • Asynchronous operations are more efficient and scalable, particularly in I/O-bound scenarios where waiting for I/O operations to complete would result in wasted CPU cycles. They allow Node.js to handle multiple operations concurrently without blocking the event loop.

  • In general, it's recommended to use asynchronous operations in Node.js applications, especially for I/O-bound tasks like file I/O, network requests, and database queries, to ensure optimal performance and scalability. However, synchronous operations may be suitable for simpler, CPU-bound tasks or situations where simplicity and sequential execution are more important than performance.

💡
Blocking methods execute synchronously and non-blocking methods execute asynchronously.

Blocking (Sync) Code

const fs = require('fs');

console.log('Hello World');
const result = fs.readFileSync('./textSync.txt', 'utf-8');
console.log(result);
console.log('Bye World');

Non-Blocking (Async) Code

const fs = require('fs');

console.log('Hello World');
fs.readFile('./textAsync.txt', 'utf-8', (err, result) => {
    err ? console.log('Error', err) : console.log(result);
})
console.log('Bye World');

Note: The default thread pool size is 4. The maximum size depends on the machine; for example, a machine with an 8-core CPU can have a maximum thread pool size of 8.

HTTP Server in NodeJS

Build a new project

  • Run the following command in your terminal: npm init -y will simply generate an empty npm project and create the package.json file. -y stands for yes, -y flag when passed to NPM commands tells the generator to use the defaults.

  • Creating an index.js file is a good practice for setting up your entry point.

      const http = require('http');
    
      const myServer = http.createServer((req, res) => {
          console.log("New Request Received");
          console.log(req); // returns big object which contain the information of user.
          console.log(req.headers); // gives extra information about user.
          res.end("Hello From Server");
      })
    
      // To run this server we need a PORT.
      myServer.listen(8000, () => console.log('Server Started!'))
    
  • Go to the package.json and change the scripts command.

      {
        "name": "myserver",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
          "start": "node index"
        },
        "author": "",
        "license": "ISC",
        "dependencies": {
          "url": "^0.11.3"
        }
      }
    
  • To run the index.js file, we should execute the command npm start.

Assignment

  • Write a code which store users log inside log.txt.

      const http = require('http');
      const fs = require('fs');
    
      const myServer = http.createServer((req, res) => {
          const log = `${Date.now()}: ${req.url}: New Req Received\n`;
          fs.appendFile('./log.txt', log, (err, data) => {
              switch(req.url) {
                  case '/': res.end("HomePage");
                  break;
                  case '/about': res.end("I am Mark");
                  break;
                  default: res.end("404 Not Found");
              }
          });
      });
    
      myServer.listen(8000, () => console.log("Server Started!"))
    
  • log.txt file looks like:

      1711101736526: /: New Req Received
      1711101736811: /favicon.ico: New Req Received
      1711102053265: /about: New Req Received
      1711102053375: /favicon.ico: New Req Received
      1711102062264: /contact: New Req Received
      1711102062358: /favicon.ico: New Req Received
    

URL's in NodeJS

💡
URL stands for Uniform Resource Locator

Example URL: https://www.heymark.vercel.app/

  • Protocol: https:// (Hypertext Transfer Protocol Secure)

  • Domain: www.heymark.vercel.app (User Friendly Name of IP Address of My Server)

  • Path: / (Root Path)

  • Paths: /about, /contact-us

  • Nested Path: /project/github-profile-search

  • Query Parameters: https://www.heymark.vercel.app/?urerId=1&a=2 (Query parameter is key-value pair start with ?) ?urerId=1&a=2

    Note:q means query, + is used for spaces, & is used as a delimiter to separate multiple query parameters.

  • Examples: https://www.google.com/search?q=javascript&oq=javascript&gs_lcrp=EgZjaHJvbWUyDwgAEEUYORiDARixAxiABDIGCAEQRRg8MgYIAhBFGD0yBggDEEUYPDIGCAQQRRg8MgYIBRBFGEEyBggGEEUYQTIGCAcQRRhB0gEIMzUwM2owajeoAgCwAgA&sourceid=chrome&ie=UTF-8

    • Protocol: https://

    • Domain: www.google.com

    • Path: /search

    • Query Parameters: q=javascript&oq=javascript&gs_lcrp=EgZjaHJvbWUyDwgAEEUYORiDARixAxiABDIGCAEQRRg8MgYIAhBFGD0yBggDEEUYPDIGCAQQRRg8MgYIBRBFGEEyBggGEEUYQTIGCAcQRRhB0gEIMzUwM2owajeoAgCwAgA&sourceid=chrome&ie=UTF-8

The res.url property provides the path of the URL using the http module. The http module does not understand query parameters. To handle query parameters, install the url module using npm install url.

index.js file:

const http = require('http');
const fs = require('fs');
const url = require('url');

const myServer = http.createServer((req, res) => {
  if(req.url === '/favicon.ico') return res.end();
  const log = `${Date.now()}: ${req.url} New Req Received\n`;
  const myUrl = url.parse(req.url);
  console.log(myUrl);
  fs.appendFile('log.txt', log, (err, data) => {
    switch (req.url) {
      case '/':
        res.end('Home Page');
        break;
      case '/about':
        res.end('I am Mark');
        break;
      default:
        res.end('404 Not Found');
    }
  });
});

myServer.listen(8000, () => console.log('Server Started!'))

URL:http://localhost:8000/about?myname=John&userid=1&search=javascript

const http = require('http');
const fs = require('fs');
const url = require('url');

const myServer = http.createServer((req, res) => {
  if(req.url === '/favicon.ico') return res.end();
  const log = `${Date.now()}: ${req.url} New Req Received\n`;
  const myUrl = url.parse(req.url, true);
  console.log(myUrl);
  fs.appendFile('log.txt', log, (err, data) => {
    switch (myUrl.pathname) {
      case '/':
        res.end('Home Page');
        break;
      case '/about':
        const username = myUrl.query.myname
        res.end(`Hey ${username}`);
        break;
      case '/search':
        const search = myUrl.query.search_query;
        res.end('Here are your results for ' + search)
      default:
        res.end('404 Not Found');
    }
  });
});

myServer.listen(8000, () => console.log('Server Started!'))

URL:http://localhost:8000/about?myname=John&userid=1&search=javascript

URL:http://localhost:8000/search?search_query=javascript+amazon+clone

HTTP Methods in NodeJS

  • GET: Used to request data from a specified resource. It retrieves data from the server based on the parameters sent in the URL.

  • POST: Used to submit data to be processed to a specified resource. It sends data to the server in the body of the HTTP request to create or update a resource.

  • PUT: Used to update existing data on the server. It sends data to a specific resource to update it or create it if it doesn't exist.

  • PATCH: Used to partially update existing data on the server. It sends data to modify specific fields of an existing resource.

  • DELETE: Used to delete existing data from the server. It sends a request to remove a specific resource.

index.js file.

const http = require('http');
const fs = require('fs');
const url = require('url');

const myServer = http.createServer((req, res) => {
  if(req.url === '/favicon.ico') return res.end();
  const log = `${Date.now()}: ${req.method} ${req.url} New Req Received\n`;
  const myUrl = url.parse(req.url, true);
  fs.appendFile('log.txt', log, (err, data) => {
    switch (myUrl.pathname) {
      case '/':
        if(req.method === 'GET') res.end("Home Page")
        break;
      case '/about':
        const username = myUrl.query.myname
        res.end(`Hey ${username}`);
        break;
      case '/search':
        const search = myUrl.query.search_query;
        res.end('Here are your results for ' + search)
      case '/signup':
        if(req.method === 'GET') res.end("This is a signup form");
        else if(req.method === 'POST') {
          // DB Query
          res.end("Success");
        }
      default:
        res.end('404 Not Found');
    }
  });
});

myServer.listen(8000, () => console.log('Server Started!'))

URL's: htts://localhost:8000/, http:/localhost:8000/about, http://localhost:8000/about?myname=Mark

log.txt file.

1711105800452: GET / New Req Received
1711105827923: GET /about New Req Received
1711105859340: GET /about?myname=Mark New Req Received

Master NodeJS withPiyush Garg