Skip to content

7. Node.js & Express Interview Questions

Grouped Basic → Intermediate → Advanced.


Basic


1. What is Node.js?

A JavaScript runtime built on V8 that runs JavaScript outside the browser. It bundles V8, libuv (the event loop and async I/O), and Node's own APIs (fs, http, crypto).

Not a language, not a framework — a runtime.


2. Is Node single-threaded?

Your JavaScript runs on one thread. Node itself is not single-threaded — libuv maintains a thread pool (4 by default) for filesystem, DNS and some crypto work, and the OS handles network I/O asynchronously.

So: single-threaded execution, multi-threaded I/O.


3. Explain the event loop.

  1. Your code runs on the main thread
  2. Async I/O is handed to libuv
  3. Your code continues — it does not wait
  4. When the I/O completes, its callback is queued
  5. The event loop picks up queued callbacks and runs them

Phases, in order: timers → pending callbacks → poll → check → close callbacks.

process.nextTick and promise microtasks are not phases — they drain after every phase and after every callback, with nextTick first.


4. What does this print?

js
console.log("1");
setTimeout(() => console.log("2"), 0);
setImmediate(() => console.log("3"));
Promise.resolve().then(() => console.log("4"));
process.nextTick(() => console.log("5"));
console.log("6");
1, 6, 5, 4, 2, 3

Synchronous first, then nextTick, then promise microtasks, then the timers phase, then check.

Bonus: at the top level, setTimeout(fn, 0) vs setImmediate is actually non-deterministic — it depends on process startup timing. Inside an I/O callback, setImmediate always wins.


5. Node vs browser JavaScript?

BrowserNode
Globalwindowglobal / globalThis
DOMYesNo
File systemNoYes
ModulesESMCommonJS + ESM
Extra APIslocalStorage, fetchfs, process, Buffer

6. CommonJS vs ESM?

CommonJSESM
Syntaxrequire / module.exportsimport / export
LoadingSynchronousAsynchronous
Tree shakingNoYes
Top-level awaitNoYes
__dirnameAvailableMust derive from import.meta.url
File extensionsOptionalRequired

Key asymmetry: ESM can import CommonJS, but CommonJS cannot require an ESM module — only await import(). That's why migrating a big codebase to ESM is painful.


7. What is Express?

A minimal web framework over Node's http module. It gives you routing, a middleware pipeline, body parsing and response helpers — and deliberately nothing else.


8. What is middleware?

A function with access to req, res and next that runs between the request and the response. It can run code, modify req/res, and either end the cycle or call next().

The rule: every middleware must either send a response or call next(). Neither and the request hangs; both and you get "Cannot set headers after they are sent".


9. Why does my Express request hang?

Almost always a middleware that neither responded nor called next(). The classic form:

js
if (!req.user) return;   // no response, no next()

Or in Express 4, an async handler that threw — the rejection is never caught, so nothing responds.


10. Explain REST.

Resource-based URLs (/users/1, not /getUser?id=1), HTTP verbs carrying the action, stateless requests, and correct status codes. Plural nouns for collections.

GET /users  ·  POST /users  ·  GET /users/:id  ·  PATCH /users/:id  ·  DELETE /users/:id

11. PUT vs PATCH?

PUT replaces the entire resource — fields you omit are cleared. PATCH updates only the fields you send.

PUT is idempotent by definition. PATCH usually is, but isn't required to be.


12. 401 vs 403?

401 Unauthorized — you are not authenticated. Missing or invalid credentials. "Who are you?"

403 Forbidden — you are authenticated but not permitted. "I know who you are, and no."


13. Name the status codes you use.

200 OK · 201 Created · 204 No Content · 400 Bad Request · 401 Unauthorized · 403 Forbidden · 404 Not Found · 409 Conflict · 422 Unprocessable · 429 Too Many Requests · 500 Internal Server Error · 503 Service Unavailable.

Returning 200 { success: false } for an error is the anti-pattern — HTTP already has a status field.


Intermediate


14. What blocks the event loop and how do you fix it?

Anything synchronous and slow: fs.readFileSync, crypto.pbkdf2Sync, JSON.parse on a huge payload, a tight loop over a million items, or catastrophic regex backtracking (ReDoS).

While it runs, every other request is stalled.

Fixes: use the async API, worker_threads for CPU work in a request, a queue plus a separate worker for background jobs, cluster/PM2 for multi-core, or chunking with setImmediate.


15. Promise.all vs allSettled vs race vs any?

ResolvesRejects
allAll succeedAny rejects — you lose the other results
allSettledAll settleNever
raceFirst to settleIf the first settles as a rejection
anyFirst to succeedAll reject

Use allSettled for a dashboard where one failing widget shouldn't kill the page.


16. What's wrong with users.forEach(async u => await send(u))?

forEach ignores the returned promise. All the calls fire at once, nothing is awaited, and rejections become unhandled.

js
for (const u of users) await send(u);              // sequential
await Promise.all(users.map((u) => send(u)));      // parallel

const limit = pLimit(5);                            // parallel, bounded
await Promise.all(users.map((u) => limit(() => send(u))));

Say the last one. Unbounded Promise.all over 10,000 items opens 10,000 connections and takes down your database. The concurrency limit is what makes it a production answer.


17. What are streams and why use them?

Process data in chunks instead of loading it into memory. fs.createReadStream("2gb.csv").pipe(res) uses constant memory; readFile uses 2 GB.

Four types: Readable, Writable, Duplex, Transform.

Use pipeline() rather than .pipe() — it propagates errors and cleans up every stream. A plain .pipe() chain leaks file descriptors on error.

Backpressure: if the consumer is slower than the producer, pipe/pipeline pause the source automatically. Manual .on("data") handlers do not — that's how memory blows up.


18. How do you handle async errors in Express?

Express 4 does not catch rejected promises from async handlers — the request hangs.

js
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

Or import "express-async-errors". Express 5 fixed this and forwards rejections automatically.


19. How does the error-handling middleware work?

Four parameters — Express detects it by arity. Drop the unused next and it silently becomes normal middleware.

js
app.use((err, req, res, next) => {
  if (res.headersSent) return next(err);
  res.status(err.statusCode ?? 500).json({
    error: err.isOperational ? err.message : "Internal server error",
  });
});

Register it last. Reach it by throwing (sync) or calling next(err).


20. Operational vs programmer errors?

Operational — expected failures: 404, validation, DB timeout, upstream 503. Handle them, return a clear 4xx.

Programmer — bugs: undefined is not a function. Don't try to recover; log with full context, return a generic 500, and fix the code.

This distinction drives everything else in your error strategy.


21. What do you do on uncaughtException?

Log and exit. The process state is unknown after an uncaught exception — a connection may be half-written, a lock may be held. Let the process manager restart you clean.

Swallowing it and continuing is the wrong answer.

Since Node 15, an unhandled rejection terminates the process by default.


22. What is graceful shutdown and why does it matter?

js
process.on("SIGTERM", () => {
  server.close(async () => {
    await db.disconnect();
    process.exit(0);
  });
  setTimeout(() => process.exit(1), 10_000).unref();
});

server.close() stops accepting new connections while letting in-flight requests finish. Docker and Kubernetes send SIGTERM before SIGKILL. Without this, every rolling deploy drops requests.


23. How do you prevent SQL injection?

Parameterised queries. The SQL text and the values travel to the database separately, so the value is never parsed as SQL.

js
await pool.query("SELECT * FROM users WHERE email = $1", [email]);

ORMs are safe by default, but $queryRawUnsafe with string interpolation is not — the name is the warning. Prisma's tagged template $queryRaw\… ${email}`` is parameterised.


24. What is NoSQL injection?

js
// Client posts { "email": "a@b.com", "password": { "$gt": "" } }
await User.findOne({ email, password });   // $gt: "" matches any password

Validate that inputs are the expected type, not just that they exist. Zod solves it.


25. How do you hash passwords?

bcrypt (cost ≥ 12) or argon2id. Never plaintext, never encryption (reversible), never MD5 or SHA-256 alone — they are fast, which is exactly wrong.

Slow is the point: a fast hash lets a GPU try billions of guesses per second against a leaked database; bcrypt at cost 12 caps it at thousands.

bcrypt salts automatically and embeds the salt in the hash.


26. Where do you store a JWT and why?

httpOnly + secure + sameSite cookie.

XSS safeCSRF safe
localStorageNoYes
httpOnly cookieYesNeeds sameSite or a CSRF token

httpOnly makes it unreadable to JavaScript, so an XSS cannot steal it. localStorage is the common wrong answer — the follow-up is always "what happens if you have an XSS?" and the answer is the attacker owns every session.

Best pattern: access token in memory, refresh token in an httpOnly cookie scoped to /auth/refresh.


27. Why access tokens plus refresh tokens?

A JWT cannot be revoked — that's inherent to being stateless. A short-lived access token (15 min) limits the damage from a stolen one. The long-lived refresh token is stored server-side, so logout and "revoke all sessions" actually work.

Rotation: issue a new refresh token on every refresh and invalidate the old one. If an old one is reused, it was stolen — revoke the whole family.


28. JWT vs sessions?

JWTSession
StateStatelessServer-side store
RevocationHardInstant
ScalingNo shared storeNeeds Redis
MicroservicesGood fitNeeds shared access

Honest answer: sessions are the better default for a normal web app — instant revocation, small cookies, sub-millisecond Redis lookup. JWTs win for microservices, mobile and third-party API access.

Most teams pick JWTs by reflex, then rebuild revocation with a denylist — at which point they have a session with extra steps.


29. What is CORS? Does it secure your API?

No. CORS is a browser mechanism: the server tells the browser which origins may read its responses.

curl, Postman and any backend ignore CORS entirely. It protects the user's browser from a malicious site reading your API with their cookies. Authorisation protects your API.

Access-Control-Allow-Origin: * with credentials: true is invalid and browsers reject it.

Most "CORS errors" are a failing preflight — the OPTIONS request the browser sends before non-simple requests.


30. How do you rate limit?

express-rate-limit with a Redis store — the default in-memory store gives each process its own counter, useless behind a load balancer or with cluster.

Stricter limits on auth endpoints (5 per 15 min) than on reads (100 per 15 min).

Behind a proxy, set app.set("trust proxy", 1) or req.ip is the proxy's IP and every user shares one bucket.


31. What is the N+1 query problem?

One query fetches N rows, then you run one query per row.

js
const users = await db.user.findMany();                    // 1
for (const u of users) u.posts = await getPosts(u.id);     // N

Fix with an eager include/join: findMany({ include: { posts: true } }).

Spot it by logging queries in development — 200 queries for one HTTP request means an N+1. In GraphQL, DataLoader batches and dedupes within a request tick.


32. Offset vs cursor pagination?

OffsetCursor
Jump to page NYesNo
Total countEasyExpensive
Page 10,000Terrible — the DB scans 200,000 rows and discards themConstant
Items shifting mid-scrollDuplicates and skipsStable

Offset for admin tables with page numbers; cursor for infinite scroll and large datasets.

Always cap the limit?limit=1000000 is a free denial of service.


Advanced


33. How do you scale a Node app?

Vertically first: cluster or PM2, one process per core, since one process uses one core.

Horizontally: multiple instances behind a load balancer.

The prerequisite: the app must be stateless. No in-memory sessions, no in-memory cache assumed shared, no in-memory rate-limit counters — each process has its own. Move that state to Redis.

Then: read replicas, caching, a queue for background work, and a CDN for static assets.

The stateless point is the follow-up interviewers are fishing for.


34. How do you size a connection pool?

The trap: max: 20 per process × 4 processes × 3 replicas = 240 connections. Postgres defaults to 100 and rejects the rest.

Rule of thumb: (cores × 2) + spindles. Start at 5–10 per process and measure. More connections is not faster — Postgres uses a process per connection.

For serverless, use a pooler (PgBouncer, Prisma Accelerate, Neon) — otherwise each invocation opens its own connection and exhausts the database.


35. When do you need a transaction?

Whenever two or more writes must succeed or fail together — the classic being a money transfer.

js
await prisma.$transaction(async (tx) => {
  await tx.account.update({ where: { id: from }, data: { balance: { decrement: 100 } } });
  await tx.account.update({ where: { id: to },   data: { balance: { increment: 100 } } });
});

Note { decrement: 100 } rather than read-then-write — an atomic update avoids a lost-update race between concurrent requests.


36. Explain ACID and isolation levels.

Atomicity (all or nothing), Consistency (constraints hold), Isolation (concurrent transactions don't corrupt each other), Durability (survives a crash).

Isolation levels: Read Uncommitted → Read Committed (Postgres default) → Repeatable Read → Serializable. Higher means more locking and contention.

Read Committed is right for almost everything. Serializable for money movement and inventory decrements.


37. How do you handle concurrent updates to the same row?

Optimistic locking with a version column:

js
const updated = await prisma.product.updateMany({
  where: { id, version: currentVersion },
  data: { stock: { decrement: 1 }, version: { increment: 1 } },
});
if (updated.count === 0) throw new ConflictError();   // → 409

No locks held between read and write; the version check catches concurrent modification. Return 409 Conflict.


38. How do you cache, and how do you invalidate?

Cache-aside: check Redis, miss → DB → populate with a TTL.

On a write, delete the key rather than updating it — updating opens a race where a stale read repopulates the old value after your update.

Cache stampede: when a hot key expires, hundreds of requests miss at once and all hit the DB. Mitigate with a short lock around repopulation, or refresh slightly before expiry.


39. How do you handle retries to an external service?

Three things:

  1. Only retry retryable errors — 5xx and connection resets. Retrying a 400 wastes time; a 401 never succeeds.
  2. Exponential backoff with jitter — fixed intervals from many clients create a thundering herd that keeps the struggling service down.
  3. Idempotency — retrying a POST that already succeeded can double-charge someone. Use an idempotency key.

Add a circuit breaker so a dead dependency fails fast instead of holding your connections open.


40. Why does every outbound call need a timeout?

Without one, a slow dependency holds your connections open. Requests queue, the pool exhausts, and your service goes down because someone else's did.

A missing timeout turns their outage into your outage.

js
await fetch(url, { signal: AbortSignal.timeout(5000) });

Also set query timeouts and server.requestTimeout.


41. How do you design a background job system?

A queue (BullMQ on Redis, or SQS) plus separate worker processes — never in the API process, where a long job blocks request handling.

Requirements: retries with backoff, a dead-letter queue for permanent failures, idempotent handlers (jobs will be delivered more than once), visibility into queue depth, and a concurrency limit per worker.

The idempotency point is the one interviewers wait for.


42. What do you log, and what do you never log?

Log: structured JSON, a request ID propagated to downstream services, method/path/status/duration, user ID, and full error context.

Never log: passwords, tokens, cookies, authorization headers, card numbers, or PII. Configure redaction at the logger, not at each call site — logs get shipped to third parties and retained for months.

Free-text console.log is unqueryable. That's the practical argument for structured logging.


43. How do you monitor a Node service in production?

  • Errors: Sentry or Datadog, with source maps uploaded
  • Metrics: request rate, error rate, p50/p95/p99 latency, event loop lag, heap usage, DB pool saturation
  • Traces: OpenTelemetry for cross-service request flow
  • Health: /health for liveness (restart me), /ready for readiness (stop routing to me)

Event loop lag is the Node-specific metric worth naming — it directly measures whether something is blocking.

Confusing liveness and readiness causes restart loops.


44. What security issues do you check for in a Node API?

AttackDefence
SQL / NoSQL injectionParameterised queries, type validation
XSSEscape output, CSP via helmet
CSRFsameSite cookies or CSRF tokens
Brute forceRate limiting on auth endpoints
DoS via payloadexpress.json({ limit: "10kb" })
Mass assignmentValidate and allowlist fields
BOLA / IDOROwnership checks in the query
Path traversalNever use a client filename directly
Info disclosureGeneric 500s, app.disable("x-powered-by")
Dependency CVEsnpm audit, Dependabot

45. What is BOLA and why do role checks not catch it?

Broken Object Level Authorization — the most common real-world API vulnerability.

js
// ❌ Any logged-in user reads any order by changing the URL
const order = await db.order.findUnique({ where: { id: req.params.id } });

// ✅ Scope the query to the owner
const order = await db.order.findFirst({
  where: { id: req.params.id, userId: req.user.id },
});

A role check confirms what kind of user you are, not whether this object is yours. Enforce ownership in the query, not with an if after fetching.

Return 404, not 403 — a 403 confirms the resource exists.


46. How do you structure a production Express app?

src/
├── routes/        thin — wiring only
├── controllers/   parse request, call service, format response
├── services/      business logic — NO Express types
├── repositories/  database access
├── middleware/
├── schemas/       Zod schemas + inferred types
└── utils/

The rule: services must not import Express types. A service takes plain arguments and returns plain data, so it can be called from a route, a cron job, a queue worker or a test without faking req/res.

That separation is what interviewers actually look for.


47. Express 4 vs Express 5?

Express 4Express 5
Async errorsNot caught — request hangsForwarded to the error handler
Wildcards/files/*/files/*splat — named required
res.send(status)Deprecated overloadRemoved
app.del()PresentRemoved
Node minimum0.10+18+

The async error change is the one that matters — it's the reason express-async-errors and asyncHandler exist in older codebases.


48. How do you test a Node API?

  • Unit — services in isolation, dependencies mocked. Fast, and possible precisely because services don't import Express.
  • Integration — routes via supertest against a real database in Docker (Testcontainers). Mocking the database tests your mock, not your SQL.
  • Contract — validate responses against the schema clients depend on.

Mock outbound HTTP with nock or MSW. Keep tests independent — no shared mutable state, and each test creates its own fixtures.

js
const res = await request(app)
  .post("/api/users")
  .send({ email: "a@b.com", name: "Test" })
  .expect(201);

49. When would you not use Node?

CPU-bound work — video encoding, large-scale image processing, heavy numerical computation, ML inference. One blocking function stalls every concurrent request.

Also: teams with no JavaScript experience, and domains where a mature ecosystem exists elsewhere (Python for data science, Go for high-throughput proxies).

Being willing to say "Node is the wrong tool here" is a good signal. The workaround — worker_threads or offloading to another service — is worth mentioning too.


50. What would you flag in a Node code review?

  • Sync I/O in a request path (readFileSync, pbkdf2Sync)
  • Unhandled promise rejections; async handlers without a wrapper on Express 4
  • Missing return before res.json()
  • Error handler with three parameters, or not registered last
  • Stack traces or internal messages in a 500 response
  • String-concatenated SQL
  • SELECT * returning passwordHash
  • Unbounded findMany() with no limit
  • Missing ownership check on a resource lookup
  • Secrets in code or committed .env
  • No timeout on an outbound call
  • Unbounded Promise.all over a large array
  • In-memory state (sessions, rate limits) in a multi-process deployment
  • Business logic in a controller or middleware instead of a service

© 2025 DDocs · Dipak's Documentation Guide