Node.js is a JavaScript runtime built on the V8 engine — the same engine used in Chromium-based browsers. It runs JavaScript outside the browser, making it possible to use the same language for both front-end and server-side code. This article covers the architecture, core modules, and practical patterns for writing server-side Node.js applications.

Node.js logo

The event loop

Node.js is single-threaded but non-blocking. It handles concurrency through the event loop — a mechanism that continuously checks for pending callbacks and executes them when their associated I/O operations complete. This model allows a single process to handle thousands of concurrent connections without spawning threads for each one.

The phases of the event loop are documented in the official Node.js event loop guide:

  • timers — executes setTimeout and setInterval callbacks
  • pending callbacks — I/O callbacks deferred from the previous iteration
  • idle, prepare — internal use
  • poll — retrieves new I/O events; blocks here if the queue is empty
  • checksetImmediate callbacks
  • close callbacks — close event handlers

Installing Node.js

The recommended way to manage Node.js versions is through a version manager. On Linux and macOS, nvm is widely used. On Windows, nvm-windows provides equivalent functionality.

nvm install 20
nvm use 20
node --version

Node.js LTS releases are supported for approximately three years. The release schedule is published on the official site.

Core modules

Node.js ships with a standard library of built-in modules. No external installation is required to use them:

http — creating an HTTP server

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello from Node.js\n');
});

server.listen(3000, () => {
  console.log('Server running on port 3000');
});

fs — file system operations

The fs module provides both synchronous and asynchronous methods. For server code, the async versions are preferred to avoid blocking the event loop:

const fs = require('fs');

fs.readFile('./data.json', 'utf8', (err, content) => {
  if (err) { console.error(err); return; }
  const data = JSON.parse(content);
  console.log(data);
});

fs.promises.readFile('./data.json', 'utf8')
  .then(content => JSON.parse(content))
  .catch(console.error);

path — cross-platform path handling

const path = require('path');

const filePath = path.join(__dirname, 'public', 'index.html');
const ext = path.extname(filePath);
console.log(ext);

npm and package management

Node.js uses npm (Node Package Manager) to manage third-party dependencies. A project is initialised with npm init, which creates package.json. Dependencies are listed there and installed into node_modules:

npm init -y
npm install express
npm install --save-dev nodemon

The node_modules directory should not be committed to version control. A .gitignore entry covering it is standard practice.

A minimal Express application

Express is a minimal web framework for Node.js. It provides routing, middleware support, and request/response helpers without imposing a specific project structure:

const express = require('express');
const app = express();

app.use(express.json());

app.get('/', (req, res) => {
  res.json({ status: 'ok' });
});

app.get('/articles/:slug', (req, res) => {
  const { slug } = req.params;
  res.json({ slug });
});

app.listen(3000, () => {
  console.log('Listening on http://localhost:3000');
});

Environment variables

Configuration values such as port numbers, API keys, and database connection strings should not be hardcoded. Node.js reads environment variables through process.env:

const PORT = process.env.PORT || 3000;
const DB_URL = process.env.DATABASE_URL;

app.listen(PORT);

The dotenv package is commonly used to load a .env file into process.env during development. The .env file itself should be listed in .gitignore.

Streams

For large files or network responses, reading everything into memory before processing is inefficient. Node.js streams process data in chunks. The fs.createReadStream method and HTTP request/response objects both implement the stream interface:

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

http.createServer((req, res) => {
  const readStream = fs.createReadStream('./large-file.txt');
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  readStream.pipe(res);
}).listen(3000);

Further reading