Node.js & Express - robbiehume/CS-Notes GitHub Wiki

Node.js

Overview

  • In Node.js, there is no window object because it operates outside the browser environment
  • Instead, Node.js uses its own global object, global, to store globally accessible properties and functions
  • While we can reference properties on the global object explicitly, in most cases, we can access global properties directly without needing the global. prefix
  • In a browser, the top-level scope is the global scope, whereas, in Node.js, the top-level scope is not the global scope; it is local to each module
    • A var defined in a browser will be global and accessible everywhere
    • A var defined in a Node.js module will be local to that module alone
      • That being said, using global variables is often not recommended
  • Node is often described as having event-driven architecture

Common global properties and functions

  • The setTimeout function allows us to delay the execution of a function. This can be useful in asynchronous programming when we want to execute code after a certain amount of time
    • setTimeout(() => {
        console.log("This message appears after a 1-second delay");
      }, 1000);
  • The setInterval function is similar but repeatedly executes the function at specified intervals until stopped.
    • const intervalId = setInterval(() => {
        console.log("This message repeats every second");
      }, 1000);
      
      clearInterval(intervalId);

Modules

  • Each separate file is treated as a module in Node.js

Core (built-in) Modules

  • Modules in global scope (don't need to call require()
    • console, process, error, Buffer, timers (setInterval, setTimeout, etc.)

os

  • Gives info about the OS and program

events

  • Need to require it to use it: const events = require('events');
  • EventEmitter class: const myEmitter = new events.EventEmitter()
    • .on(): assigns a listener callback function to a named event
      • It takes the name of the event as a string and the listener callback function
      • Ex: `myEmitter.on('new user', newUserListener)
      • An event can have more than one listener added to it
    • .emit(): execute the named event
      • It takes the name of the event as a string and the data to pass to the callback defined in .on()
      • Ex: `myEmitter.emit('new user', 'Robbie')
  • Listener functions registered using the .on() method are called each time their specific event is ommitted
    • To only have it run once, use .once() instead: myEmitter.once('new user', newUserListener)
  • Handling errors: errors can cause issues when they occur in an EventEmitter
    • If you don't have a listener registered to handle an error event, the process will exit and print a stack trace
    • Therefore, it is good coding practice to have a listener for an error event:
      • myEmitter.on('error', handleError): handleError function will be called if an error occurs

console

  • Examples:
    • console.log('Hello World');
      console.warn('This is a warning!');
      console.error('This is an error');
      console.error(new Error('This is a different error'));
  • console.trace('output message'): used to print a stack trace
  • console.table(<iterable>): outputs an iterable (JSON object, array, etc.) in a nice table format
  • console.time('timer name'): starts a timer with a name that we pass to it as a string
  • console.timeEnd('timer name'): stops the specified timer and outputs the time elapsed since it started
  • Previously, we were using the global console for printing to the console. What if we wanted to output to a file?
    • For that, we can use the Console class:
      const fs = require('fs');
      const { Console } = require('console');
      const output = fs.createWriteStream('./stdout.log');
      const errorOutput = fs.createWriteStream('./stderr.log');
      const logger = new Console({ stdout: output, stderr: errorOutput });
      logger.log('number:', 5);  // In stdout.log: number 5
      logger.error('error code:', 'code');

process

  • Allows us to control and get info about the current process
  • Since it's an instance of the EventEmitter class, it has a few important events that we should know about
    • beforeExit and exit
      • beforeExit event: emitted when the event loop is empty (Node.js has done all its work and is about to exit)
      • exit event: emitted when process.exit() is called, or the event loop has no additional work to perform
        • The exit event executes synchronously, and asynchronous calls may not return properly. This is why the console.log() call in the setTimeout below won't get output
      • You can register the events to take action
        • process.on('beforeExit', (code) => {console.log('Process beforeExit event with code:', code)})
        • process.on('exit', (code) => {
            setTimeout(() => { console.log('This will not work.') }, 0);
            console.log('Process exit event with code:', code);
          });
      • NOTE: if we explicitly call process.exit(), the beforeExit event is not emitted
    • uncaughtException
      • This event is fired when an exception occurs that has not been handled or dealt with in the program
      • By default, Node.js exits and prints the stack trace in case an uncaughtException arises
      • Using the process object, we can cater to these exceptions and deal with them as we wish:
        process.on('uncaughtException', (err, origin) => {
          console.error('This caused a problem:', origin);
          console.error(err.stack);
        });
        
  • Other methods
    • process.cwd(): returns the current working directory
    • process.chdir(directory): change the working directory of the process
    • process.exit(<code>): terminate the process with an exit status of ; the default is 0
  • Streams
    • A stream is an abstract interface for working with streaming data in Node.js
    • Streams can be readable, writable, or both
    • All streams are instances of EventEmitter
    • The streams include:
      • process.stdin
      • process.stdout
      • process.stderr

User input/output (process, readline)

  • process.argv: array of command line arguments passed to the program
    • argv[0]: the path of the node executable (process.execPath)
    • argv[1]: the path of the JavaScript file that is being run
    • argv[2], argv[3], ...: the command line argument(s)
    • There are also packages, such as yargs that make parsing CLI arguments easier
  • process.stdin: get input
    • .stdin.on(): retrieve input from the user
    • process.stdin.on('data', (userInput) => {
        let input = userInput.toString()
        console.log(input)
      });
    • Here, we were able to use .on() because under the hood process.stdin is an instance of EventEmitter
    • When a user enters text into the terminal and hits enter, a 'data' event will be fired and our anonymous listener callback will be invoked
    • The userInput we receive is an instance of the Node Buffer class ([link(https://nodejs.org/api/buffer.html#buffer_buffer)), so we convert it to a string before printing
  • process.stdout: write input
    • .stdout.write(): write to the console
      • The console.log() method is a “thin wrapper” on the process.stdout.write() method
  • readline: get input
    • The readline module needs an interface to work
      • It can be a file, or in the example below, the console
    • Ex:
      const readline = require('readline');
      const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
      });
      let query = 'What is your name?\n'
      rl.question(query, (answer) => {
        console.log(`Hello ${answer}!`);
        rl.close();
      });

error

  • The error module has all the standard JS errors Cush as EvalError, SyntaxError, RangeError, ReferenceError, TypeError, and URIError as well as the JavaScript Error class for creating new error instances
  • Within our own code, we can generate errors and throw them, and, with synchronous code in Node, we can use error handling techniques such as try...catch statements
  • Many asynchronous Node APIs use error-first callback functions—callback functions which have an error as the first expected argument and the data as the second argument
    • If no error was thrown, the first argument will be undefined
    • Ex:
      const errorFirstCallback = (err, data)  => {
        if (err) {
           console.log(`There WAS an error: ${err}`);
        } else { // err was falsy  
          console.log(`There was NO error. Event data: ${data}`);
        }
      }
  • Node uses error-first callbacks in many of its asynchronous APIs because traditional try...catch statements won't work for error thrown during asynchronous operations

Buffer

  • Provides us with a class that stores an array of bytes that is used to represent binary data
  • Buffer objects correspond to fixed-sized blocks of memory, which cannot be changed after they are created
  • Different ways to allocate:
    • const buf1 = Buffer.alloc(10);  // 10 is the length of the Buffer
      const buf2 = Buffer.alloc(10, 15);  // 15 is the value it fills with; defaults to 0 if not provided
      const buf3 = Buffer.allocUnsafe(10);  // unsafe (Buffer may or may not be empty), but faster
      const buf3.fill(1);  // fills the Buffer with the argument passed (1's)
      const buf2.write('abcdef');  // write a string to the Buffer; only writes the amount that will fit
      const buf4 = Buffer.from([265, 6.5, -255, '7']);  // creates a Buffer from the provided data
      const buf5 = Buffer.from('Hello world');  // can create from a string
      console.log(buf5.toString());  // convert back to string ('Hello world')

fs (file system)

  • fs allows us to interact with the file system
  • It must be required before using: const fs = require('fs')
  • It has quite a few methods, some of which have both sync. and async. variants
    • NOTE: the async. forms of the methods always take a completion callback as the last argument
    • For this callback, the first argument is reserved for an exception
  • Reading files: fs.readFile and fs.readFileSync
    • Takes a file path, optional arguments (such as encoding), and a callback function to call with data read from the file (no callback needed for sync. version)
    • Asynchronous way: doesn't wait for reading to finish; outputs 'Hello!' first, then file data
      fs.readFile('test.txt', 'utf-8', (err, data) => {
        if (err) {
          console.error(err)
          return
        }
        console.log(data)
      })
      console.log("Hello!")  // outputs before file data since it's async
    • Synchronous way: waits for reading to finish; outputs file data first, then 'Hello!'
      try {
        const data = fs.readFileSync('test.txt', 'utf8')
        console.log(data)
      } catch (err) {
        console.error(err)
      }
      console.log("Hello!")  // outputs after file data since it's sync and waits for reading to finish
  • Writing to files: fs.writeFile and fs.writeFileSync
    • Takes a file path, the content to write, and a callback function to call when finished (no callback needed for sync. version)
    • Asynchronous way: doesn't wait for reading to finish; outputs 'Hello!' first, then file data
      fs.writeFile('test.txt', 'hello', (err) => {
        if (err) {
          console.error(err)
          return
        }
        console.log('File written!')
      })
    • Synchronous way: waits for reading to finish; outputs file data first, then 'Hello!'
      try {
        const data = fs.writeFileSync('test.txt', 'hello')
        console.log('File written!')
      } catch (err) {
        console.error(err)
      }
  • fs.stat(): provides useful info about files
  • fs.open()/fs.read()/fs.write(): for reading/writing in chunks, fine-grained control; best for streaming or large files
  • fs.readFile()/fs.writeFile(): for reading/writing entire files at once

net

  • net module provides an asynchronous network API for creating stream-based TCP or IPC servers
  • Allows you to create client-server socket connections

http

Express

Overview

  • Express is a framework built on top of Node.js
  • Express offers many benefits over Node.js
    • Reduces boilerplate code
    • Simplified routing
    • Built-in Middleware Support
    • Easy Route Management
    • Database Integration
⚠️ **GitHub.com Fallback** ⚠️