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.
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
setTimeoutandsetIntervalcallbacks - 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
- check —
setImmediatecallbacks - 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);