Skip to content

Understanding and Using Node.js

Checking Node.js and NPM Versions

To check the installed versions of Node.js and npm (Node Package Manager), open your terminal or command prompt and use the following commands:

  • To check Node.js version:
bash
node -v
node --version
  • To check npm version:
bash
npm -v
npm --version

NVM (Node Version Manager)

NVM (Node Version Manager) is a command-line tool (typically a bash script) used to manage multiple active Node.js versions on the same machine. This is extremely useful because different projects might require different Node.js versions.

Benefits of using NVM:

  • Install Multiple Versions: Easily install various versions of Node.js.
  • Switch Between Versions: Quickly switch the active Node.js version globally or per shell session.
  • Set Default Version: Define a default Node.js version to use.
  • Project-Specific Versions: Often used with .nvmrc files in project roots to automatically switch to the project's required Node.js version.

Installation: The installation method varies slightly by operating system. For Linux and macOS, you typically use a curl or wget script:

bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

# Or using wget:
# wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

(Always check the official NVM GitHub repository for the latest version and installation instructions: https://github.com/nvm-sh/nvm)

After installation, you'll likely need to close and reopen your terminal or source your shell's configuration file (e.g., .bashrc, .zshrc).

Common NVM Commands:

  • Install a specific Node.js version:
bash
nvm install 18.17.0
nvm install 20
nvm install node # Installs the latest version
nvm install lts/* # Installs the latest LTS (Long Term Support) version
  • List installed versions:
bash
nvm ls
  • List available remote versions:
bash
nvm ls-remote
  • Use a specific installed version in the current shell:
bash
nvm use 18.17.0
nvm use 20
  • Set a default Node.js version:
bash
nvm alias default 18.17.0
  • Run a command with a specific version without switching the shell's version:
bash
nvm exec 20 node my_script.js

NVM simplifies managing Node.js environments, especially when working on multiple projects or testing compatibility across different Node versions.

Node.js

Node.js is a powerful JavaScript runtime built on Chrome's V8 JavaScript engine. It allows you to execute JavaScript code outside of a web browser, making it suitable for server-side programming, command-line tools, and more.

You can interact with Node.js in two main ways:

1. Interactive Mode (REPL):

Running the node command without any arguments launches the Node.js REPL (Read-Eval-Print Loop). This is an interactive shell where you can type and execute JavaScript code line by line, and see immediate results. It's excellent for quick experiments, learning, and debugging.

bash
$ node

This will open the REPL prompt:

>

Now you can execute JavaScript:

javascript
> console.log('Hello World');
Hello World
undefined
> 2 + 2
4
  • Hello World is the output of the console.log() function.
  • undefined is printed because console.log() doesn't explicitly return a value.
  • 4 is the result of the expression 2 + 2.

To exit the REPL, type .exit or press Ctrl+C twice.

2. Executing a Script File:

The more common way to use Node.js is to write your JavaScript code in a .js file and then execute that file using the node command.

For example, create a file named hello.js:

javascript
// hello.js
console.log('Hello World');

Then, run it from your terminal:

bash
$ node hello.js
Hello World

Node.js will execute the JavaScript code within hello.js, and you'll see the output:

I/O Operations with Node.js: The Filesystem (fs) Module

Node.js excels at input/output (I/O) operations, such as reading from and writing to files. The built-in fs (filesystem) module provides a rich API for interacting with the file system.

One common function is fs.readFile(), which asynchronously reads the entire contents of a file.

  • path: The path to the file you want to read.
  • options (optional): Can include encoding (e.g., 'utf8'). If not specified, the raw buffer is returned.
  • callback: A function that Node.js will call once the file reading is complete (or if an error occurs). This callback receives two arguments:
    • err: An error object if an error occurred, otherwise null.
    • data: The contents of the file. If no encoding is specified, this is a Buffer object.

A Buffer is Node.js's way of representing raw binary data. You can convert a Buffer to a string using its toString() method, often specifying an encoding like 'utf8'.

Example: Reading and Parsing a Log File

txt
// example_log.txt

2021-09-09T10:15:33.166Z A 8
2021-09-09T10:16:33.166Z B 8
2021-09-09T10:17:33.166Z C 7
2021-09-09T10:18:33.166Z C 7
2021-09-09T10:19:33.166Z A 8

Here's how you might read and process it using my_parser.js:

javascript
// my_parser.js

// Import the built-in 'fs' module
const fs = require('fs'); 

// Asynchronously read the contents of 'example_log.txt'
// We specify 'utf8' encoding so 'data' will be a string directly.
fs.readFile('example_log.txt', 'utf8', function (err, fileContents) {
    // Always handle potential errors first
    if (err) {
        console.error('Error reading the file:', err);
        // Optionally, re-throw or handle more gracefully
        return; // Exit the callback if an error occurs
        // throw err; // This would stop the program
    }

    // 'fileContents' is now a string because we specified 'utf8' encoding.
    // If encoding wasn't specified, 'fileContents' would be a Buffer,
    // and you'd convert it like this: 
    //const text = fileContents.toString();

    console.log('File Contents:\n', fileContents);

    // Example: Process the data (e.g., split into lines)
    const lines = fileContents.split('\n');
    console.log('\nProcessing lines:');
    lines.forEach((line, index) => {
        if (line.trim() !== '') { // Avoid processing empty lines
            console.log(`Line ${index + 1}: ${line}`);
        }
    });
});

console.log('Reading file... (This message will likely appear before file contents)');

Note on Synchronous Operations: The fs module also provides synchronous versions of its functions (e.g., readFileSync). These block the entire Node.js process until the operation completes. While sometimes useful for simple scripts or startup tasks, they should generally be avoided in server applications or any code where responsiveness is critical, as they prevent Node.js from handling other operations.

Asynchronous Operations in Node.js

A core characteristic of Node.js is its asynchronous, non-blocking I/O model. Node.js operates on a single main thread and uses an event loop to manage concurrent operations.

  • Single-Threaded: Node.js executes your JavaScript code on a single main thread.

  • Event Loop: When an asynchronous operation (like reading a file, making a network request, or querying a database) is initiated, Node.js doesn't wait for it to complete. Instead, it registers a callback function and delegates the actual I/O work to the system's kernel (which often uses multiple threads behind the scenes).

  • Non-Blocking: While the I/O operation is in progress, the Node.js main thread is free to continue executing other code, like handling other incoming requests or running other tasks.

  • Callbacks: Once the I/O operation finishes (successfully or with an error), the event loop picks up the completed task and executes the associated callback function with the result or error.

This non-blocking nature is crucial for building scalable applications, especially web servers, because it allows a single Node.js process to handle many concurrent connections efficiently without getting bogged down waiting for slow I/O operations.

Key Asynchronous Patterns to Master: To effectively work with Node.js, you'll need a solid understanding of these patterns for managing asynchronous operations:

  • Callbacks: The traditional way of handling asynchronous results. Can lead to "callback hell" (deeply nested callbacks) if not managed carefully.

  • Promises: Objects representing the eventual completion (or failure) of an asynchronous operation and its resulting value. They provide better composability and error handling than raw callbacks (e.g., .then(), .catch(), .finally()).

  • Async/Await: Syntactic sugar built on top of Promises, allowing you to write asynchronous code that looks and behaves a bit more like synchronous code, making it easier to read and reason about. async functions return Promises, and await pauses execution within an async function until a Promise settles.

  • Synchronous and Asynchronous Generators & Iterators: Advanced concepts for producing sequences of values over time, useful for handling streams of data or complex asynchronous workflows.

Creating a Simple Web Server with Node.js

Node.js includes a built-in http module for creating HTTP servers.

js
// mywebserver.js

const http = require('http');

http.createServer(function(req, res) {
	res.writeHead(200, { 'Content-Type': 'text/plain' });
	res.end('Hello World\n');
}).listen(8080);

// http.createServer(handler).listen(8080);

console.log('Server running at http://localhost:8080/');

This server:

  • Listens on port 8080
  • Responds with "Hello World" to every request

Here's a basic example with explanation:

javascript
// my_webserver.js

// Import the built-in 'http' module
const http = require('http');

// Define the port the server will listen on
const port = 8080;

// Create the server
// The function passed to createServer is a request handler,
// executed for each incoming HTTP request.
const server = http.createServer(function (request, response) {
    // 'request' (req) object contains information about the incoming request
    // 'response' (res) object is used to send data back to the client

    // Set response HTTP header with HTTP status and Content type
    response.writeHead(200, { 'Content-Type': 'text/plain' });

    // Send the response body "Hello World"
    response.end('Hello World\n');
});

// Start the server and have it listen on the specified port
server.listen(port, function () {
    // This callback is executed once the server starts listening
    console.log(`Server running at http://localhost:${port}/`);
    console.log('Press Ctrl+C to stop the server.');
});

Alternative Structure (Named Handler Function):

You can also define the request handler function separately, which can be useful for more complex servers or better organization.

javascript
// my_webserver_alt.js

const http = require('http');
const port = 8080;

// Define the request handler function
const requestHandler = function (request, response) {
    console.log(`Received request for: ${request.url}`); 
    // Log the requested URL

    response.writeHead(200, { 'Content-Type': 'text/plain' });
    response.end('Hello World from the alternative server!\n');
};

// Create the server using the named handler
const server = http.createServer(requestHandler);

// Start listening
server.listen(port, () => {
    console.log(`Alternative server running at http://localhost:${port}/`);
    console.log('Press Ctrl+C to stop the server.');
});

This "longer format" is simply about modularity and readability. For very simple servers, the anonymous function is fine. For more complex logic, separating the handler is a good practice.

Running the Web Server:

  1. Save one of the above examples (e.g., my_webserver.js).
  2. Run it from your terminal: node my_webserver.js
  3. Open a web browser and navigate to http://localhost:8080.
  4. You will see "Hello World" (or the respective message) displayed.

Important Note on Server Behavior: A Node.js web server application, like the ones above, will not stop automatically after starting. It continuously listens for incoming HTTP requests. To stop the server, you need to go back to the terminal where it's running and press Ctrl+C.

Server-Side Programming

Server-side programming (often called backend development) involves writing code that runs on a web server. This code is responsible for handling client requests, processing data, interacting with databases, enforcing business logic, and ultimately determining the content that is sent back to the client's browser (or other client applications).

Key Responsibilities of Server-Side Code:

  1. Handling Client Requests: When a user interacts with a website (e.g., clicks a link, submits a form), their browser sends an HTTP request to the server. Server-side code intercepts and processes these requests.

  2. Data Validation and Processing: It validates data sent from the client (e.g., form submissions) to ensure it's in the correct format and meets business rules. It then processes this data according to the application's logic.

  3. Database Interaction: Most dynamic websites rely on databases to store and retrieve information (user accounts, product details, content, etc.). Server-side code manages these interactions – querying data, inserting new records, updating existing ones, and deleting data.

  4. Authentication and Authorization: It verifies user identities (authentication) and checks if authenticated users have permission to access specific resources or perform certain actions (authorization).

  5. Dynamic Content Generation: Instead of serving static HTML files for every page, server-side code can dynamically generate HTML, JSON, XML, or other content based on the request, user data, or information from a database. This allows for personalized and up-to-date content.

  6. API Development: Server-side code often exposes Application Programming Interfaces (APIs) that client applications (web browsers, mobile apps, other services) can use to request and send data.

  7. Business Logic Implementation: Enforces the core rules and processes that define how the application works.

Benefits of Server-Side Programming:

  • Dynamic and Personalized Content: The most significant benefit is the ability to tailor website content to individual users. This can be based on user preferences, past behavior, location, or other criteria, leading to a much richer user experience.

    • Example (Amazon): Amazon uses server-side programming to show personalized product recommendations, remember previous purchases, manage wish lists, and process orders securely.
    • Example (Banks): Banks use it to securely store account information, process transactions, and ensure only authorized users can access sensitive financial data.
  • Centralized Logic and Data Management: Business rules and data are managed in one place (the server), ensuring consistency and making updates easier.

  • Enhanced Security: Sensitive operations and data access can be controlled and protected on the server, away from direct client-side manipulation.

  • Access to Server Resources: Server-side code can access resources not available to client-side code, such as databases, file systems, and external services.

  • Scalability and Performance: Well-designed server-side architectures can handle a large number of users and requests efficiently.

Node.js for Server-Side Programming

Node.js allows developers to use JavaScript for server-side programming, which offers several advantages, especially for those already familiar with JavaScript for front-end development.

Performance and V8 Engine: Node.js is built on Google's V8 JavaScript engine, the same engine that powers Chrome. V8 is highly optimized for performance.

  • Just-In-Time (JIT) Compilation: V8 compiles JavaScript directly into native machine code, rather than interpreting it or compiling it to an intermediate bytecode. This direct compilation significantly boosts execution speed, making Node.js performant for many types of applications, especially I/O-bound ones.

Client-Side (Browser) vs. Server-Side (Node.js) JavaScript Environments

While both environments use the JavaScript language, their execution contexts, available APIs, and primary concerns are quite different.

FeatureClient-Side (Browser) JavaScriptServer-Side (Node.js) JavaScript
Primary GoalUser interface interaction, DOM manipulation, dynamic content updates in the browser.Handle requests, process data, interact with databases, manage server resources, build APIs.
Execution EnvironmentWeb browser (Chrome, Firefox, Safari, etc.)Server, local machine (via Node.js runtime)
Global Objectwindowglobal
Core APIsWeb APIs:Node.js APIs:
- DOM (Document Object Model) for HTML/XML manipulation- fs (File System) for file operations
- BOM (Browser Object Model) e.g., navigator, location- http/https for creating servers/clients
- fetch / XMLHttpRequest for HTTP requests- path for path manipulation
- localStorage / sessionStorage for client storage- os for operating system info
- Event handling (clicks, mouse movements, etc.)- process for current process info/control
- setTimeout, setInterval- child_process for running external commands
- WebSockets, WebRTC, Canvas, WebGL, etc.- Streams for efficient data handling
Module System (Historically)No built-in module system (relied on <script> tags, IIFEs, or libraries like RequireJS). ES Modules (import/export) now widely supported.CommonJS (require() / module.exports) is the native, traditional system. ES Modules are also supported in newer Node.js versions (often requiring specific configuration or .mjs files).
Control over EnvironmentLimited. Developer adapts to various browsers and user settings.Full control. Developer chooses Node.js version, OS, installed packages, and server configuration.
Access to ResourcesRestricted by browser security sandbox (e.g., limited file system access, strict cross-origin policies).Full access to the server's file system, network interfaces, databases, and other system resources (within OS permissions).
Concurrency ModelSingle-threaded event loop (similar to Node.js). Web Workers allow for some background threading.Single main thread with an event loop for non-blocking I/O. worker_threads module allows for CPU-bound tasks to be run on separate threads.

In essence:

  • Browser JS focuses on interacting with the visual web document and browser features.
  • Node.js JS focuses on system-level operations, networking, and data processing on a server.

The shift towards ES Modules (import/export) is notable. While Node.js traditionally used CommonJS (require()), modern Node.js versions increasingly support ES Modules, aiming for a more unified module system across both browser and server environments. However, CommonJS remains prevalent in many existing Node.js projects and libraries.

Made with ❤️ for students, by a fellow learner.