Node.js Interview Questions: Fundamentals
Node.js fundamentals come up in almost every backend interview, and the questions tend to probe whether you actually understand the runtime rather than just the framework on top of it. This page collects the most common fundamentals questions—what Node is, how its single-threaded model really works, the roles of V8 and libuv, blocking versus non-blocking calls, the module systems, and the global objects—each with a short, precise answer you can deliver under pressure. The answers target modern Node.js 20/22 LTS.
What is Node.js?
Q: What is Node.js, and what is it used for?
Node.js is a runtime that lets you execute JavaScript outside the browser. It pairs Google’s V8 engine (which compiles JS to machine code) with a C++ layer and the libuv library that provides an event loop and asynchronous I/O. It is not a language or a framework—it is the environment your server-side JS runs in. Node shines for I/O-heavy, concurrent workloads such as HTTP APIs, real-time services, and CLI tools, because it handles many simultaneous connections without spawning a thread per request.
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.end("Hello from Node.js\n");
});
server.listen(3000, () => console.log("Listening on :3000"));
Output:
Listening on :3000
The single-threaded model
Q: Is Node.js single-threaded? How does it handle concurrency?
Your JavaScript runs on a single main thread—there is exactly one call stack executing your code, so two lines of your JS never run literally at the same time. Node achieves concurrency by being asynchronous, not by multithreading your code: when you start an I/O operation (reading a file, querying a database), Node hands it off to libuv and immediately continues running other JS. When the operation finishes, its callback is queued and the event loop runs it. This is why one Node process can serve thousands of concurrent connections.
Single-threaded refers to the JavaScript execution thread. Under the hood libuv maintains a small worker thread pool (default 4) for operations that can’t be done asynchronously at the OS level, such as filesystem and crypto work.
V8 and libuv
Q: What are V8 and libuv, and how do they relate?
They are the two pillars of the runtime:
| Component | Written in | Responsibility |
|---|---|---|
| V8 | C++ | Parses, compiles, and executes JavaScript; manages the heap and garbage collection |
| libuv | C | Provides the event loop, async I/O, timers, and the worker thread pool |
V8 runs your code; libuv lets that code wait on the outside world without blocking. Node glues them together with C++ bindings and a standard library (fs, http, net, and so on) exposed to JavaScript.
Blocking vs non-blocking
Q: What is the difference between blocking and non-blocking calls?
A blocking call stops the single thread until it completes—nothing else can run in the meantime. A non-blocking call starts the work and returns immediately, delivering the result later via a callback or promise. Because Node has one JS thread, a blocking call freezes the whole process, including every other in-flight request. Most core APIs offer both forms; the synchronous variants end in Sync.
import { readFile, readFileSync } from "node:fs";
// Blocking: nothing else runs until the file is read
const data = readFileSync("config.json", "utf8");
console.log("1. read synchronously");
// Non-blocking: returns immediately, callback runs later
readFile("config.json", "utf8", (err, contents) => {
console.log("3. read asynchronously");
});
console.log("2. this line runs before the async callback");
Output:
1. read synchronously
2. this line runs before the async callback
3. read asynchronously
The promise-based API (node:fs/promises) is the modern, preferred way to do non-blocking I/O with async/await:
import { readFile } from "node:fs/promises";
const contents = await readFile("config.json", "utf8");
console.log(JSON.parse(contents).port);
Avoid
SyncAPIs and CPU-heavy loops on the request path of a server. They block the event loop and stall every concurrent client. Reserve synchronous calls for startup/config code that runs once.
CommonJS vs ES modules
Q: What is the difference between CommonJS and ES modules?
CommonJS (CJS) is Node’s original module system; ES modules (ESM) are the standardized JavaScript modules shared with browsers. Node supports both, but they differ in syntax, loading, and timing.
| Aspect | CommonJS | ES Modules |
|---|---|---|
| Syntax | require() / module.exports | import / export |
| Loading | Synchronous, runtime | Asynchronous, statically analyzed |
| File extension | .cjs (or .js by default) | .mjs (or .js with "type": "module") |
Top-level await | Not supported | Supported |
__dirname | Available | Use import.meta.url instead |
// CommonJS
const path = require("node:path");
module.exports = { join: path.join };
// ES module
import path from "node:path";
export const join = path.join;
You opt into ESM by setting "type": "module" in package.json or using the .mjs extension. ESM is the modern default for new projects.
Global objects
Q: What global objects and variables are available in Node?
Node exposes globals you can use without importing anything. The most common are process, console, Buffer, the timer functions, and the global fetch (stable since Node 18). Note that __dirname and __filename are module-scoped globals available only in CommonJS.
| Global | Purpose |
|---|---|
process | Current process: argv, env, pid, exit() |
console | Logging to stdout/stderr |
Buffer | Fixed-length binary data |
globalThis | The standard reference to the global object |
setTimeout / setInterval / setImmediate | Timers and deferral |
fetch | Built-in HTTP client (no library needed) |
console.log(process.version); // Node version
console.log(process.env.NODE_ENV ?? "dev"); // Environment variable
const res = await fetch("https://api.github.com");
console.log(res.status);
Output:
v22.11.0
dev
200
Best Practices
- Describe Node as a runtime (V8 + libuv), not a language or framework—interviewers listen for this distinction.
- Keep the event loop free: prefer async/promise APIs and offload CPU-bound work to worker threads or a separate service.
- Default to ES modules with
"type": "module"for new code, but know how CommonJS interop works. - Use the
node:prefix for core modules to make intent explicit and avoid clashes with npm packages. - Read configuration with
Synccalls at startup only; never use them inside request handlers. - Prefer built-in
fetchover adding an HTTP library when you only need basic requests.