What to Expect in a Node.js Interview
Node.js interviews test three layers: how the runtime works (event loop, streams, clustering), whether you write correct async code, and whether you can build and secure a real API. This guide covers 50+ questions you are most likely to face, organized by topic, with real code examples and what the interviewer is actually listening for.
Bookmark the sections that feel shaky and run through the answers out loud — Node.js interviewers listen for fluency, not just accuracy.
1. The Event Loop and How Node.js Works
These questions almost always open the interview. Get them right and you signal you understand the runtime, not just the syntax.
Q1. Explain the Node.js event loop in plain terms.
Node.js runs on a single thread and uses an event loop to handle concurrency. When your code calls something async (a file read, a database query, a timer), Node delegates it to the underlying OS or libuv thread pool and moves on. When the operation finishes, a callback is queued. The event loop picks callbacks off the queue and executes them one at a time.
The loop has phases: timers (setTimeout/setInterval), pending callbacks, idle/prepare, poll (wait for I/O), check (setImmediate), and close callbacks. Understanding the order matters when debugging tricky timing bugs.
Q2. What is the difference between process.nextTick and setImmediate?
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
console.log('sync');
// Output: sync → nextTick → setImmediateprocess.nextTick fires before the event loop moves to the next phase — it drains completely before any I/O or timer callbacks run. setImmediate fires in the check phase, after I/O events. Overusing nextTick can starve I/O callbacks.
Q3. What is libuv and why does Node.js need it?
libuv is the C library that provides Node's event loop, async I/O, thread pool, and cross-platform abstractions (file system, networking, DNS). The thread pool (default 4 threads, configurable via UV_THREADPOOL_SIZE) handles operations the OS cannot do asynchronously natively — like fs.readFile or crypto.pbkdf2.
Q4. What does "non-blocking I/O" actually mean?
When Node calls fs.readFile, it hands the work to the OS and returns immediately. Your JavaScript thread does not wait. This is the opposite of synchronous code like fs.readFileSync, which blocks the thread until the read completes. For a server handling many concurrent requests, blocking is catastrophic — one slow file read stalls every other request.
Q5. What is the difference between concurrency and parallelism in Node.js?
Node achieves concurrency (many things in progress at the same time) on a single thread via the event loop. It does not achieve true parallelism (many things running at the exact same instant) unless you use worker_threads or cluster. A CPU-heavy task blocks the event loop unless offloaded.
Q6. How does cluster work and when should you use it?
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');
}Cluster forks the process so you get one worker per CPU core. The master distributes incoming connections. Use it to saturate multiple cores on a multi-CPU machine. In production, PM2 handles this for you.
Q7. What are worker_threads and how do they differ from cluster?
worker_threads run JavaScript in separate threads within the same process, sharing memory via SharedArrayBuffer. cluster forks entire OS processes. Workers are better for parallelizing CPU tasks within one server instance; cluster is for scaling across cores to handle more network connections.
Q8. What happens if you throw a synchronous error inside an async callback?
fs.readFile('file.txt', (err, data) => {
throw new Error('oops'); // uncaught — process may crash
});The throw unwinds the current call stack, not the original one. Nobody catches it. Listen to process.on('uncaughtException') or, better, use promises and .catch().
Q9. What is backpressure in Node.js streams?
When a readable stream produces data faster than the writable stream can consume it, the buffer overflows. writable.write() returns false when the buffer is full, and emits drain when it empties. Piping handles backpressure automatically; manual stream code must respect the return value of write.
Q10. What is the purpose of Buffer and why can't you use strings?
Strings in JavaScript are UTF-16. Raw binary data — TCP packets, file bytes, images — does not fit into strings without corruption. Buffer is a fixed-size chunk of memory outside the V8 heap, used for binary protocols, streaming file data, and crypto operations.
2. Async JavaScript — Callbacks, Promises, Async/Await
Q11. Explain callback hell and how to avoid it.
Callback hell is deeply nested callbacks that make code hard to read and maintain:
getUser(id, (err, user) => {
getPosts(user.id, (err, posts) => {
getComments(posts[0].id, (err, comments) => { /* ... */ });
});
});Escape routes: named functions instead of inline callbacks, util.promisify, or async/await.
Q12. What is the difference between Promise.all, Promise.allSettled, Promise.race, and Promise.any?
await Promise.all([p1, p2]); // rejects if ANY rejects
await Promise.allSettled([p1, p2]); // waits for ALL, returns {status, value|reason}
await Promise.race([p1, p2]); // resolves/rejects with the FIRST to settle
await Promise.any([p1, p2]); // resolves with FIRST to fulfillPromise.allSettled is the right choice when you want all results even if some requests fail — interviewers often ask specifically about this one.
Q13. What does async/await actually compile down to?
Async functions are syntactic sugar over generators and promises. await suspends the async function (not the thread) and resumes it when the promise resolves. Under the hood it is equivalent to .then() chaining but reads synchronously.
Q14. How do you handle errors with async/await?
async function fetchUser(id) {
try {
const user = await db.findById(id);
return user;
} catch (err) {
logger.error(err);
throw err;
}
}For cleaner error handling in complex flows, a small utility like const [err, result] = await to(promise) avoids deeply nested try/catch blocks.
Q15. What is the danger of await inside a loop?
// SLOW — sequential, one at a time
for (const id of ids) {
const user = await db.findById(id);
}
// FAST — parallel
const users = await Promise.all(ids.map(id => db.findById(id)));Awaiting in a loop serializes requests. Always consider Promise.all when iterations are independent.
Q16. How does EventEmitter work?
const { EventEmitter } = require('events');
const emitter = new EventEmitter();
emitter.on('data', (chunk) => console.log(chunk));
emitter.emit('data', 'hello');Node streams, http.Server, and many built-in objects extend EventEmitter. It is the observer pattern: listeners register, the emitter fires. Max listeners default to 10 — set higher with setMaxListeners to avoid memory leak warnings.
Q17. What is the order of microtasks and macrotasks?
The order within a single event loop tick: synchronous code → nextTick queue → promise microtasks → macrotasks (timers, I/O). nextTick callbacks run before promise .then() callbacks, which run before setTimeout.
Q18. How do you make a CPU-bound task non-blocking?
Options in order of complexity: (1) break the work into chunks and yield with setImmediate between them; (2) offload to a worker thread via worker_threads; (3) offload to a separate microservice. Never run synchronous crypto, image resizing, or large JSON parsing on the main thread of a web server.
3. Express.js and REST API Design
Q19. What is middleware in Express and how does the pipeline work?
app.use((req, res, next) => {
console.log(req.method, req.path);
next(); // must call next or the chain stops
});Middleware functions receive (req, res, next). Calling next() passes control forward; calling next(err) jumps to error-handling middleware. The order you register middleware is the order it runs.
Q20. How do you write centralized error handling in Express?
// Error middleware has 4 parameters — Express identifies it by arity
app.use((err, req, res, next) => {
const status = err.status || 500;
res.status(status).json({ error: err.message });
});All thrown errors and next(err) calls funnel here. Keep it at the end of your middleware stack, after all routes.
Q21. How do you validate request input?
Use a library like zod, joi, or express-validator. Never trust req.body directly.
import { z } from 'zod';
const schema = z.object({ email: z.string().email(), age: z.number().min(18) });
const result = schema.safeParse(req.body);
if (!result.success) return res.status(400).json(result.error);Q22. What is the difference between app.use and app.get?
app.use matches any HTTP method and any path that starts with the specified prefix. app.get only matches GET requests to the exact path. Use app.use for middleware; use app.get/post/put/delete for routes.
Q23. How would you structure a large Express app?
Split by domain: routes/, controllers/, services/, repositories/, middleware/. Controllers handle HTTP concerns; services hold business logic; repositories talk to the database. This separation makes unit testing possible — you can test a service without spinning up HTTP.
Q24. What is CORS and how do you enable it?
const cors = require('cors');
app.use(cors({ origin: 'https://yourapp.com', credentials: true }));CORS is the browser mechanism that restricts cross-domain requests. The server must send Access-Control-Allow-Origin headers. Use the cors package rather than building header logic by hand.
Q25. How do you implement rate limiting?
const rateLimit = require('express-rate-limit');
app.use('/api/', rateLimit({ windowMs: 60_000, max: 100 }));Store the counter in Redis (via rate-limit-redis) when you have multiple server instances — in-memory counters reset on each pod restart.
4. Databases and Performance
Q26. How do you prevent N+1 queries?
An N+1 problem: fetch 10 orders, then fetch the customer for each — that is 11 queries. Fix with eager loading via JOIN, or with a DataLoader batching pattern. In Prisma: include: { customer: true }. In raw SQL: LEFT JOIN.
Q27. What is connection pooling and why does it matter?
Opening a new database connection takes 20-100ms. A pool keeps N connections alive and reuses them. In Node.js: pg uses Pool, Prisma manages it automatically. Set max based on your database's connection limit divided by the number of app instances.
Q28. How do you implement caching in Node.js?
Three levels: (1) in-process with a Map or lru-cache (fast, not shared across instances); (2) Redis (shared, supports TTL and pub/sub); (3) CDN for public responses. Use Redis for anything shared across instances; in-process only for static lookup data.
Q29. How do you stream a large file download without loading it into memory?
app.get('/download', (req, res) => {
res.setHeader('Content-Disposition', 'attachment; filename="data.csv"');
fs.createReadStream('/data/large.csv').pipe(res);
});pipe handles backpressure automatically. Never load the whole file with readFile before sending — for large files it runs out of memory and blocks the event loop.
Q30. How do you run database migrations safely?
Use a migration tool (Prisma Migrate, db-migrate, node-pg-migrate). Never run raw ALTER TABLE by hand in production. Migrations should be version-controlled, ideally idempotent, and always tested on staging first.
5. Authentication and Security
Q31. How do JWTs work and what are their weaknesses?
A JWT is a signed JSON payload encoded as header.payload.signature. The server signs it with a secret or private key; any service with the key can verify it without a DB lookup. Weaknesses: (1) stateless — you cannot invalidate a token before expiry without a blocklist; (2) the alg: none attack — always verify the algorithm server-side; (3) short expiry plus refresh token is the standard pattern.
Q32. What is the difference between authentication and authorization?
Authentication proves who you are (login, JWT verification). Authorization proves what you are allowed to do (RBAC, permission checks). A common bug: checking that req.user exists does not mean that user can access that specific resource.
Q33. How do you prevent SQL injection in Node.js?
// VULNERABLE
db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
// SAFE — parameterized
db.query('SELECT * FROM users WHERE id = $1', [req.params.id]);ORMs parameterize automatically. Never interpolate user input into SQL strings.
Q34. What HTTP security headers should every Node.js API set?
Use helmet:
const helmet = require('helmet');
app.use(helmet());Key headers it sets: Strict-Transport-Security, X-Content-Type-Options, X-Frame-Options, Content-Security-Policy, Referrer-Policy.
Q35. How do you store passwords securely?
Hash with bcrypt (cost factor 12+) or argon2. Never use MD5, SHA-1, or plain SHA-256 — they are fast and crackable. bcrypt adds a unique salt per user automatically. Never store the plaintext password anywhere, including logs.
6. Testing
Q36. What is the difference between unit, integration, and end-to-end tests?
Unit: test a function in isolation, mock all dependencies. Integration: test a service with a real database but mocked external HTTP calls. End-to-end: spin up the full server and test via HTTP like a real client. Most Node.js codebases need mostly unit + integration; E2E tests are slow and brittle.
Q37. How do you mock modules in Jest?
jest.mock('../db', () => ({ findUser: jest.fn().mockResolvedValue({ id: 1 }) }));Or use jest.spyOn to mock a method while keeping the rest of the module real. Reset mocks in afterEach to prevent state from leaking between tests.
Q38. How do you test an Express route without starting a server?
Use supertest:
const request = require('supertest');
const app = require('../app');
test('GET /users returns 200', async () => {
const res = await request(app).get('/users').expect(200);
expect(res.body).toHaveProperty('users');
});Supertest creates an in-process HTTP server — no port binding, no cleanup required.
Q39. What is test coverage and what percentage should you target?
Coverage measures which lines and branches your tests execute. 100% is counterproductive to chase — critical paths (auth, billing, data mutations) should be at 100% and overall 70-80% is healthy. Coverage does not measure quality — you can have 100% coverage with tests that never assert anything meaningful.
Q40. How do you test code that depends on Date.now() or Math.random()?
Inject the dependency or use jest.useFakeTimers() and jest.spyOn(global.Date, 'now'). Code that calls Date.now() directly inside functions is harder to test than code that accepts a clock as a parameter.
7. Production and Observability
Q41. How do you handle uncaught exceptions and unhandled rejections?
process.on('uncaughtException', (err) => {
logger.fatal(err);
process.exit(1); // must exit — state is unpredictable after this
});
process.on('unhandledRejection', (reason) => {
logger.error(reason);
});Since Node 15, unhandled rejections crash the process by default. Always .catch() your promise chains.
Q42. How do you implement structured logging?
Use pino or winston and output JSON. JSON logs are parseable by aggregators (Datadog, Loki, CloudWatch). Include a requestId on every log line so you can trace a single request across hundreds of entries. Never use console.log in production.
Q43. What is graceful shutdown?
process.on('SIGTERM', () => {
server.close(() => {
db.end();
process.exit(0);
});
});When Kubernetes sends SIGTERM before killing a pod, you have a few seconds to stop accepting new requests and drain in-flight ones. Without graceful shutdown, active requests are dropped mid-response.
Q44. How do you profile a slow Node.js application?
Start with --inspect and Chrome DevTools CPU profiler, or use clinic.js doctor for automated diagnostics. First ask: CPU-bound or I/O-bound? CPU-bound means offload or cache. I/O-bound means check for N+1 queries, missing database indexes, or slow external calls.
Q45. What environment variables should a Node.js app use?
All secrets (DB URLs, API keys, JWT secrets) must be environment variables, never hardcoded. Use dotenv for local dev, never commit .env files. Validate all env vars at startup with a schema — a clear crash at boot is better than a cryptic crash at 3am.
8. Common Coding Questions
Q46. Implement a debounce function.
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}Q47. Implement a retry wrapper with exponential backoff.
async function retry(fn, { attempts = 3, base = 200 } = {}) {
for (let i = 0; i < attempts; i++) {
try { return await fn(); }
catch (err) {
if (i === attempts - 1) throw err;
await new Promise(r => setTimeout(r, base * 2 ** i));
}
}
}Q48. What is a closure? Give a Node.js example.
A closure is a function that retains access to its outer scope after the outer function returns.
function createCounter() {
let count = 0;
return () => ++count;
}
const counter = createCounter();
counter(); // 1
counter(); // 2Middleware factories use this pattern constantly — the outer function configures options, the inner function handles each request.
Q49. Implement a simple in-memory pub/sub.
class PubSub {
#subs = new Map();
subscribe(event, fn) {
if (!this.#subs.has(event)) this.#subs.set(event, []);
this.#subs.get(event).push(fn);
}
publish(event, data) {
(this.#subs.get(event) ?? []).forEach(fn => fn(data));
}
}Q50. How do you stream a large file without loading it into memory?
app.get('/export', (req, res) => {
res.setHeader('Content-Disposition', 'attachment; filename="report.csv"');
const stream = fs.createReadStream('/data/report.csv');
stream.pipe(res);
});pipe handles backpressure automatically. If the client is slow, Node pauses reading. Loading the full file with readFile first kills memory on large exports.
How to Prepare for a Node.js Interview
The most effective prep is not memorizing these answers — it is building a mental model of the event loop, practicing async patterns in code, and being able to explain tradeoffs out loud.
For company-specific prep, paste the job link into [InterviewHack.ai](https://www.interviewhack.ai/dashboard/new): you get a dossier showing who your actual interviewers are, the questions they tend to ask, and answers grounded in your own CV. Free, no card required. Generate it in under a minute and go in knowing exactly what to expect.
