All posts

Node.js Interview Questions: 25 Questions on the Event Loop, Streams and APIs

Node.js Interview Questions: 25 Questions on the Event Loop, Streams and APIs

You have built three Express APIs, one with login and a MongoDB database. The interviewer glances at your resume and asks: "If Node is single-threaded, what happens when two hundred requests arrive at once?" You say "it's asynchronous", and the follow-up comes: "Asynchronous how? Who does the waiting?" That is where most Node.js interviews are decided.

Backend interviewers rarely test whether you can write a route. They test whether you understand the runtime underneath it, because that is what breaks in production. Here are the 25 questions that come up most, grouped the way interviewers tend to move through them.

The short answer

Node.js developer interview questions cluster around five areas: the runtime (V8, libuv, the event loop and what blocks it), data handling (streams, backpressure, buffers), error handling (callbacks, promises, unhandled rejections), building APIs with Express (middleware order, REST design, authentication, config), and running Node in production (modules, npm, security, scaling, database pooling, logging and testing). For each one, interviewers want the mechanism, not the definition: say what happens, then why it matters for a real server.

The runtime and the event loop

1. What is Node.js?

Node.js is a JavaScript runtime, not a language or a framework. It combines Google's V8 engine, which compiles and runs your JavaScript, with libuv, a C library that provides the event loop, asynchronous I/O and a thread pool. On top, Node adds what a browser lacks: fs, net, http, process. It suits servers because I/O is non-blocking by default: while one request waits on a database, the same thread serves others.

2. Node is single-threaded. How does it handle thousands of connections?

Your JavaScript runs on one thread, but the waiting happens elsewhere. Network I/O is handed to the operating system's async mechanisms (epoll on Linux, kqueue on macOS, IOCP on Windows), and some work goes to libuv's thread pool. When a result is ready, a callback is queued, and the event loop runs it once the call stack is empty. A server spends most of its time waiting on networks and disks, so one thread doing only the quick JavaScript parts can serve many connections, as long as every callback finishes quickly.

3. Walk me through the phases of the event loop.

Each turn goes through phases in order: timers (expired setTimeout/setInterval callbacks), pending callbacks (some deferred system errors), idle/prepare (internal), poll (fetch new I/O events and run their callbacks, waiting here if nothing else is scheduled), check (setImmediate callbacks) and close callbacks (such as a socket's close). Between callbacks, Node drains the process.nextTick queue, then the promise microtask queue. Since Node 11 this happens after each individual callback, not only between phases, matching browser behaviour.

4. What is the difference between process.nextTick, setImmediate and setTimeout(fn, 0)?

process.nextTick runs as soon as the current operation finishes, before promise callbacks and before the loop moves on (in CommonJS; in an ES module's top-level code, promise callbacks can run first). setImmediate runs in the check phase. setTimeout(fn, 0) is really a minimum of 1ms and runs in the timers phase. From the main module, the order of setTimeout(0) and setImmediate is not guaranteed. Inside an I/O callback, setImmediate always runs first, because check comes straight after poll.

Likely follow-up: "Why is nextTick risky?" A recursive nextTick keeps refilling a queue that is drained before I/O, so it can starve the loop. Prefer setImmediate for deferring work.

5. What does blocking the event loop mean, and how do you avoid it?

A callback runs long enough that nothing else can: no new requests accepted, no timers fired. Common causes are fs.readFileSync in a request handler, JSON.parse on a huge payload, CPU-heavy loops, and regular expressions that backtrack catastrophically on user input. Use async APIs on the request path, cap body sizes, chunk large jobs, and move real CPU work to worker threads or a job queue.

Likely follow-up: "How would you detect it?" Measure event loop delay with perf_hooks.monitorEventLoopDelay(), or profile with node --prof or the DevTools inspector.

6. What is the libuv thread pool, and what uses it?

Some operations have no truly asynchronous OS interface, so libuv runs them on a pool of threads: most fs operations, dns.lookup, and CPU-heavy crypto (pbkdf2, scrypt) and zlib work. The default size is 4, set by UV_THREADPOOL_SIZE. Network sockets do not use it. So five concurrent password hashes on a default pool means one waits.

7. Worker threads, cluster or child processes: when do you use each?

Worker threads (stable since Node 12) run JavaScript in parallel inside one process and can share memory through SharedArrayBuffer, so they suit CPU-bound tasks like image processing. Cluster forks several Node processes sharing one port, so request handling uses every core. Child processes (spawn, exec, fork) run a separate program, such as a shell command or a Python script.

Streams, buffers and errors

8. What are streams, and why use them?

A stream processes data piece by piece instead of loading it all into memory. There are four types: Readable, Writable, Duplex (both, like a TCP socket) and Transform (a Duplex that changes data, like gzip). Serving a 2GB file with readFile holds 2GB in memory per request; fs.createReadStream(path).pipe(res) holds a small buffer and the client gets the first bytes immediately.

9. What is backpressure?

It is what happens when the consumer is slower than the producer, say reading from a fast disk and writing to a slow client. Without handling it, data piles up in memory. A writable signals it by returning false from write(); you stop writing until the drain event fires. pipe() and stream.pipeline() do this for you. Prefer pipeline (promise version in stream/promises), because it also destroys every stream and reports the error if any one fails, which pipe does not.

10. What is a Buffer?

A fixed-length chunk of raw binary memory, allocated outside the V8 heap and a subclass of Uint8Array. Node needs it because sockets, files and crypto output are bytes, not strings. Create one with Buffer.from() or Buffer.alloc(); the old new Buffer() constructor is deprecated. Buffer.allocUnsafe() is faster but may contain old memory, so overwrite it fully before exposing it.

11. How do you handle errors with callbacks, promises and async/await?

Callbacks use the error-first convention (err, result): check err first. A try/catch around a callback API does not help, because the error arrives later on a different stack. Promises take .catch(); async/await takes try/catch around the await. The classic bug is a missing await, which lets the rejection escape the try. Also separate operational errors you handle (a timeout, bad input) from programmer errors (an undefined access) that should crash and be fixed.

12. What happens when a promise rejection is unhandled?

Since Node 15, an unhandled rejection crashes the process by default, like an uncaught exception. Before that Node only printed a warning, so a server could run for weeks silently dropping errors. You can listen on process.on('unhandledRejection') and process.on('uncaughtException'), but use them to log and exit, not to carry on: the process may be in an unknown state. Let a process manager restart it.

Express and API design

13. How does Express middleware work, and why does order matter?

Middleware is a function (req, res, next) run in registration order. Each one either ends the response or calls next(). Order matters because each step depends on earlier ones: express.json() must run before a handler reads req.body, authentication before protected routes, and a rate limiter registered after a route never runs for it. Middleware that neither responds nor calls next() leaves the request hanging until it times out.

14. How do you handle errors in Express?

Express recognises error middleware by its four arguments, (err, req, res, next), registered after all routes, and you reach it with next(err). In Express 4, a rejected promise in an async handler was not caught, so you needed a wrapper or express-async-errors. Express 5 (released in late 2024) forwards rejected promises to the error middleware automatically.

15. What makes a good REST API design?

Resources are nouns in the URL (/users/42/orders) and HTTP methods are the verbs. Use correct status codes: 201 created, 400 invalid input, 401 not authenticated, 403 not allowed, 404 not found, 409 conflict. GET, PUT and DELETE should be idempotent; POST is not assumed to be. Paginate lists and version the API (/api/v1) so you can change it without breaking clients.

Likely follow-up: "How do you make a payment endpoint safe to retry?" Accept an idempotency key from the client and return the stored result when the same key arrives again.

16. JWT or sessions for authentication?

With sessions, the server stores session data (often in Redis) and the client holds only an ID in a cookie; revoking access means deleting the session. A JWT carries signed claims itself, so any server can verify it without a lookup, which suits multiple services. The cost is revocation: a stolen token stays valid until it expires. The usual answer is short-lived access tokens plus a revocable refresh token. An httpOnly cookie keeps the token away from JavaScript, but then you need CSRF protection.

17. How do you manage configuration and environment variables?

Anything that changes between environments (database URLs, API keys, ports) comes from environment variables, never committed code. Read process.env once in a config module and validate at startup, so a missing key fails the boot rather than the first request that needs it. Node 20.6 added a built-in --env-file flag, so local development no longer needs dotenv. Keep .env out of version control and commit a .env.example.

Modules and tooling

18. What is the difference between CommonJS and ES modules?

CommonJS uses require and module.exports and loads synchronously at runtime, so you can require inside an if. ES modules use import/export, are statically analysable (enabling tree-shaking), support top-level await, and have no __dirname (use import.meta.dirname or import.meta.url). A file is ESM if it ends in .mjs or the nearest package.json has "type": "module". ESM can import CommonJS; require() of an ES module works without a flag since Node 22.12 and 20.19, provided the module has no top-level await.

19. What is the difference between package.json and package-lock.json?

package.json lists dependency ranges: ^4.18.2 allows any 4.x from 4.18.2 up, ~4.18.2 only 4.18.x patches. package-lock.json records the exact version of every installed package, including nested ones, so every machine gets the same tree. Commit it. In CI use npm ci: it installs exactly what the lockfile says, removes node_modules first, and fails if the lockfile and package.json disagree.

20. What is the difference between npm and npx?

npm manages packages: installing, updating and running package.json scripts. npx runs a package's executable, using the local copy if installed and otherwise downloading it temporarily, so npx create-vite needs no global install.

Production: security, scaling, data, logging, tests

21. What are the security basics for a Node API?

Start with injection. For SQL, use parameterised queries or an ORM, never string concatenation. For MongoDB, watch for operator injection: a body like {"password": {"$ne": null}} passed straight into a query can bypass a login, so validate types with Zod or Joi. Then rate-limit login and expensive routes, hash passwords with bcrypt, scrypt or argon2, set security headers (helmet), limit body size, and run npm audit, because most of your code is dependencies.

Likely follow-up: "Does an in-memory rate limiter work across four servers?" No, each counts separately. Use a shared store like Redis.

22. How do you scale a Node application?

First use every core on one machine with the cluster module or PM2 in cluster mode, which also restarts crashed processes. Then scale horizontally behind a load balancer. That only works if the app is stateless: no sessions, uploads or caches in process memory, because the next request may hit another instance. Move that state to Redis, a database or object storage.

23. How do you connect to a database, and what is connection pooling?

Opening a connection is expensive: a TCP handshake, often TLS, then authentication. A pool keeps connections open and lends them to queries. Create it once at startup, never per request. Size it carefully: too small and requests queue, too large and you exhaust the database's limit, since the real total is instances multiplied by pool size. Release connections on error paths too, or the pool slowly runs dry. For the database side, see our SQL interview questions and answers.

24. How should you log in a Node application?

Use structured JSON logs from a library such as Pino or Winston rather than console.log, so logs can be filtered by field. Use levels set per environment, attach a request ID to every line so you can follow one request across services, and never log passwords, tokens or card details.

25. How do you test a Node.js API?

Unit-test business logic with dependencies mocked, and write integration tests that send real HTTP requests (Supertest is common) against a test database. Node 20 made the built-in node:test runner stable, though Jest and Vitest remain common. Export the Express app separately from listen() so tests can import it without opening a port.

How to practise these

Reading 25 answers is not the same as giving them. In a real interview, question 2 leads straight to question 3, then to "where does a setImmediate inside this readFile callback run?" The follow-up shows whether you understood or memorised, as we explain in why knowing the answer isn't enough. When you hit one you cannot answer, read how to handle a question you don't know. Revise the language underneath too: closures, promises and this come up in every Node round, and our JavaScript interview questions cover them.

Then practise out loud. On 99interview, Sia, our AI interviewer, asks one question at a time and follows up on what you actually said. Pick Node.js as your stack, choose a difficulty (Beginner, Standard or Senior) and a round length, and get a scored written breakdown when you finish; the backend practice track is the place to start. For a human view, a live 40-minute Zoom interview with a working engineer is scored on technical depth, problem solving, communication, behaviour and code quality, with written feedback.

If you are still building the basics, the fresher preparation hub shows what to cover first.