Curated questions covering event loop, streams, modules, Express.js, async/await, npm, REST APIs, clustering, and Node.js architecture.
Node.js is a JavaScript runtime built on Chrome V8 engine that runs JavaScript on the server. Differences: Node has no DOM/window/document; it has access to the file system, network, and OS via built-in modules (fs, http, path, os); it uses CommonJS/ESM modules; it is single-threaded with non-blocking I/O.
The event loop allows Node.js to perform non-blocking I/O despite being single-threaded. It processes callbacks in phases: timers (setTimeout/setInterval), pending callbacks, idle/prepare, poll (I/O), check (setImmediate), close callbacks. Each phase has a FIFO queue of callbacks.
console.log("1");
setTimeout(() => console.log("2"), 0); // timers phase
setImmediate(() => console.log("3")); // check phase
Promise.resolve().then(() => console.log("4")); // microtask
console.log("5");
// Output: 1, 5, 4, 2, 3
setImmediate(() => console.log("setImmediate"));
process.nextTick(() => console.log("nextTick"));
// Output: nextTick, setImmediate
// CommonJS
const fs = require("fs");
module.exports = { readFile };
// ESM
import fs from "fs";
export { readFile };
Synchronous operations (fs.readFileSync) block the event loop until complete. Asynchronous operations (fs.readFile, fs.promises.readFile) are non-blocking. Always use async operations in production to avoid blocking other requests.
// Async (preferred)
const data = await fs.promises.readFile("file.txt", "utf8");
// Sync (blocks event loop - avoid in servers)
const data = fs.readFileSync("file.txt", "utf8");
Streams process data in chunks rather than loading everything into memory. Four types:
const readable = fs.createReadStream("large.csv");
const writable = fs.createWriteStream("output.csv");
readable.pipe(writable); // memory-efficient piping
const { pipeline } = require("stream/promises");
await pipeline(
fs.createReadStream("input.txt"),
zlib.createGzip(),
fs.createWriteStream("output.gz")
); // auto-cleans up on error
The cluster module allows Node.js to create child processes (workers) that share the same server port, utilizing multiple CPU cores. The master process manages workers and distributes incoming connections.
const cluster = require("cluster");
const os = require("os");
if (cluster.isPrimary) {
for (let i = 0; i < os.cpus().length; i++) {
cluster.fork();
}
} else {
require("./server"); // each worker runs the server
}
const { spawn, exec, fork } = require("child_process");
spawn("ls", ["-la"]); // streams output
exec("ls -la", (err, stdout) => console.log(stdout)); // buffered
fork("./worker.js"); // Node.js child with IPC
worker_threads enables true multi-threading in Node.js for CPU-intensive tasks. Unlike cluster (multiple processes), workers share memory via SharedArrayBuffer and communicate via message passing. Use for heavy computation to avoid blocking the event loop.
const { Worker, isMainThread, parentPort } = require("worker_threads");
if (isMainThread) {
const worker = new Worker(__filename);
worker.on("message", result => console.log(result));
worker.postMessage({ data: [1,2,3] });
} else {
parentPort.on("message", ({ data }) => {
parentPort.postMessage(data.reduce((a,b) => a+b, 0));
});
}
Express.js is a minimal, unopinionated web framework for Node.js. Core concepts: routing (app.get/post/put/delete), middleware (functions that process req/res), request/response objects, error handling middleware, and template engines.
const express = require("express");
const app = express();
app.use(express.json()); // middleware
app.get("/users", async (req, res) => {
const users = await User.findAll();
res.json(users);
});
app.listen(3000);
Middleware functions have access to req, res, and next. They execute in order and can modify req/res, end the request, or call next() to pass control. Types: application-level, router-level, error-handling (4 params), built-in, and third-party.
// Logger middleware
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next(); // pass to next middleware
});
// Error handling middleware (4 params)
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
Buffer represents fixed-length raw binary data. Used for working with binary data from files, network streams, and cryptography. Buffers are allocated outside the V8 heap.
const buf = Buffer.from("Hello", "utf8");
console.log(buf); // <Buffer 48 65 6c 6c 6f>
console.log(buf.toString()); // "Hello"
const buf2 = Buffer.alloc(10); // zero-filled 10-byte buffer
EventEmitter is the foundation of Node.js event-driven architecture. Objects emit named events and listeners respond. Most Node.js core modules (streams, http, fs) extend EventEmitter.
const { EventEmitter } = require("events");
class MyEmitter extends EventEmitter {}
const emitter = new MyEmitter();
emitter.on("data", (chunk) => console.log("Received:", chunk));
emitter.emit("data", "hello"); // "Received: hello"
package-lock.json locks the exact version of every installed package and its dependencies, ensuring reproducible installs across environments. It should be committed to version control. npm ci uses it for deterministic installs.
// In /home/user/app/src/server.js
console.log(__dirname); // /home/user/app/src
console.log(process.cwd()); // /home/user/app (where node was run)
The path module provides utilities for working with file and directory paths in a cross-platform way. Always use path.join() instead of string concatenation to handle OS differences (/ vs \).
const path = require("path");
path.join(__dirname, "public", "index.html"); // cross-platform
path.resolve("./config.json"); // absolute path
path.extname("file.txt"); // ".txt"
path.basename("/foo/bar.js"); // "bar.js"
http creates an unencrypted HTTP server. https creates a TLS/SSL encrypted server requiring a certificate and private key. In production, use a reverse proxy (Nginx) for HTTPS termination rather than handling it in Node.js directly.
const https = require("https");
const fs = require("fs");
const server = https.createServer({
key: fs.readFileSync("key.pem"),
cert: fs.readFileSync("cert.pem")
}, app);
server.listen(443);
The crypto module provides cryptographic functionality: hashing (SHA-256, MD5), HMAC, encryption/decryption (AES), key generation, and random bytes. Use bcrypt or argon2 for password hashing (not crypto.createHash).
const crypto = require("crypto");
// Hash
const hash = crypto.createHash("sha256").update("data").digest("hex");
// Random token
const token = crypto.randomBytes(32).toString("hex");
// HMAC
const hmac = crypto.createHmac("sha256", "secret").update("data").digest("hex");
const jwt = require("jsonwebtoken");
const token = jwt.sign({ userId: 1 }, process.env.JWT_SECRET, { expiresIn: "1h" });
const decoded = jwt.verify(token, process.env.JWT_SECRET);
Mongoose is an ODM (Object Document Mapper) for MongoDB. It adds: schema definition with validation, middleware (pre/post hooks), virtuals, population (joins), query building, and TypeScript support. The native driver is lower-level with no schema enforcement.
const userSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true }
});
userSchema.pre("save", async function() {
this.password = await bcrypt.hash(this.password, 10);
});
Rate limiting restricts the number of requests a client can make in a time window, protecting against brute force and DDoS attacks. Use express-rate-limit middleware.
const rateLimit = require("express-rate-limit");
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requests per window
message: "Too many requests"
});
app.use("/api/", limiter);
process.env provides access to environment variables set in the OS or shell. dotenv loads variables from a .env file into process.env during development. Never commit .env files. In production, set environment variables directly in the hosting platform.
require("dotenv").config();
const dbUrl = process.env.DATABASE_URL;
const port = process.env.PORT || 3000;
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that blocks requests from different origins. Configure it in Express using the cors middleware.
const cors = require("cors");
app.use(cors({
origin: ["https://myapp.com", "https://staging.myapp.com"],
methods: ["GET", "POST", "PUT", "DELETE"],
credentials: true
}));
PM2 is a production process manager for Node.js. Features: automatic restart on crash, cluster mode (multi-core), log management, monitoring, zero-downtime reloads, and startup scripts. Essential for production deployments.
pm2 start app.js --name "api" -i max # cluster mode, all CPUs
pm2 restart api
pm2 logs api
pm2 monit
// Express async error handling
app.get("/users", async (req, res, next) => {
try {
const users = await User.find();
res.json(users);
} catch (err) {
next(err); // passes to error middleware
}
});
The os module provides operating system information: CPU count (os.cpus().length), total/free memory, hostname, platform, network interfaces, and temp directory. Useful for cluster setup and system monitoring.
const os = require("os");
console.log(os.cpus().length); // number of CPU cores
console.log(os.totalmem()); // total RAM in bytes
console.log(os.platform()); // "linux", "win32", "darwin"
The net module provides TCP/IPC networking. Used to create raw TCP servers and clients. The http module is built on top of net. Useful for custom protocols, game servers, and inter-process communication.
const net = require("net");
const server = net.createServer((socket) => {
socket.write("Hello\n");
socket.on("data", data => console.log(data.toString()));
});
server.listen(8080);
Synchronous middleware calls next() directly. Asynchronous middleware uses async/await or Promises. Unhandled Promise rejections in async middleware will not be caught by Express error handlers unless you explicitly call next(err) or use an async wrapper.
// Async middleware - must catch errors
app.use(async (req, res, next) => {
try {
req.user = await getUser(req.headers.authorization);
next();
} catch (err) {
next(err);
}
});
const server = http.createServer(app);
const io = require("socket.io")(server);
server.listen(3000); // needed for WebSocket support
The util module provides utility functions: util.promisify() (converts callback-based functions to Promises), util.inspect() (deep object inspection), util.format() (string formatting), and util.inherits() (prototype inheritance).
const util = require("util");
const readFile = util.promisify(fs.readFile);
// Now use with async/await
const data = await readFile("file.txt", "utf8");
const { Server } = require("socket.io");
const io = new Server(server);
io.on("connection", (socket) => {
socket.on("message", data => io.emit("message", data));
});
const bcrypt = require("bcrypt");
// Hash
const hash = await bcrypt.hash(password, 12); // cost factor 12
// Verify
const match = await bcrypt.compare(password, hash);
// Prisma
const users = await prisma.user.findMany({
where: { active: true },
include: { posts: true }
});
const redis = require("ioredis");
const client = new redis();
await client.set("user:1", JSON.stringify(user), "EX", 3600); // 1hr TTL
const cached = await client.get("user:1");
Graceful shutdown allows in-flight requests to complete before the process exits. Abrupt termination (SIGKILL) immediately kills the process, potentially corrupting data or dropping requests.
process.on("SIGTERM", async () => {
console.log("Shutting down gracefully...");
server.close(async () => {
await db.disconnect();
process.exit(0);
});
});
Node.js caches modules after the first require() call. Subsequent require() calls return the cached exports without re-executing the module. This means modules are singletons. Delete require.cache[key] to force re-evaluation (rarely needed).
// Both return the same instance
const a = require("./config");
const b = require("./config");
console.log(a === b); // true - same cached object
Node.js 18+ includes a built-in fetch API (same as browser fetch), eliminating the need for node-fetch or axios for simple HTTP requests. For advanced features (interceptors, automatic JSON, retries), axios or got are still useful.
// Node.js 18+ built-in fetch
const res = await fetch("https://api.example.com/users");
const users = await res.json();
const pino = require("pino");
const logger = pino({ level: "info" });
logger.info({ userId: 1, action: "login" }, "User logged in");
Explore 500+ free tutorials across 20+ languages and frameworks.