Node.js Interview Questions: The Core Developer Round
50 Intermediate Practical Node.js Questions asked in Top MNCs focusing on Architecture, Performance, and Microservices.
Intermediate Level (Q1 – Q50)
`libuv` is a multi-platform C library that provides support for asynchronous I/O based on event loops. It abstracts non-blocking I/O operations (file system, networking) to provide a consistent interface across different operating systems, and it manages the internal thread pool.
The Event Loop runs in a specific order of phases: 1) Timers (`setTimeout`, `setInterval`), 2) Pending Callbacks (I/O callbacks deferred to the next iteration), 3) Idle/Prepare (internal use), 4) Poll (retrieve new I/O events), 5) Check (`setImmediate`), and 6) Close Callbacks (e.g., `socket.on(‘close’)`).
By default, `libuv` creates a thread pool of 4 threads to handle heavy tasks like DNS lookups, crypto operations, and file I/O. You can alter it by setting `process.env.UV_THREADPOOL_SIZE = ‘x’` (up to 1024) before your app boots.
Backpressure occurs when data is being read from a source faster than it can be written to a destination. The writable stream’s internal buffer fills up, and it must signal the readable stream to pause until the buffer drains to prevent memory exhaustion.
`.pipe()` automatically manages backpressure, but it does not automatically destroy connected streams if an error occurs in the middle of the chain, leading to memory leaks. `pipeline()` (from the `stream` module) safely handles errors and properly cleans up all streams involved.
A graceful shutdown involves listening to OS signals (`SIGINT`, `SIGTERM`), stopping the server from accepting new connections (`server.close()`), completing existing active requests, closing database connections, and finally calling `process.exit(0)`.
The `cluster` module forks the entire Node.js process. Each fork has its own V8 instance, memory, and Event Loop, but shares server ports (great for horizontal scaling). `worker_threads` share the same process memory (via ArrayBuffers) and are designed to offload CPU-heavy mathematical tasks within a single app instance.
You start the Node process with the `–inspect` flag and connect Chrome DevTools. You take a Heap Snapshot, run a load test against your API, and take another snapshot. By comparing the two (Heap Allocation Timeline), you can identify objects (like closures or arrays) that are not being garbage collected.
Callbacks registered with `process.nextTick()` are executed immediately after the current operation, before the Event Loop continues. If you recursively call `nextTick()`, you can cause “I/O Starvation,” where the Event Loop is locked and never reaches the Poll phase to handle incoming requests.
If you repeatedly attach listeners to an EventEmitter (e.g., inside a middleware or request handler) without removing them via `emitter.removeListener()`, the emitter retains a reference to the callback closures, preventing them from being garbage collected.
MaxListenersExceededWarning: Possible EventEmitter memory leak detected.Session cookies are stateful; the server stores a Session ID in memory/Redis and sends a cookie to the client. The server must check the DB on every request. JWT is stateless; the payload (e.g., user role) is cryptographically signed and stored on the client. The server mathematically verifies the signature without querying the database.
Storing JWTs in LocalStorage makes them vulnerable to Cross-Site Scripting (XSS). If an attacker injects malicious JavaScript into your site, that script can easily read `localStorage.getItem(‘token’)` and steal the user’s identity.
Cross-Site Request Forgery (CSRF) is when a malicious site tricks a user’s browser into making an authenticated request to your site (using their active cookies). You protect against it by implementing Anti-CSRF tokens (using packages like `csurf`) or setting the `SameSite` attribute on your cookies.
Rate limiting restricts the number of requests a client (usually identified by IP address) can make in a given timeframe. It is typically implemented using middleware like `express-rate-limit` backed by an in-memory store or Redis for distributed systems.
Redis is an in-memory key-value data store. Because Node is single-threaded, querying a traditional DB (like Postgres) for frequently accessed, unchanging data is slow. Redis acts as a caching layer, returning data in sub-milliseconds.
Opening a new database connection for every HTTP request is incredibly resource-intensive (TCP handshakes, authentication). A connection pool creates a set number of DB connections on startup and reuses them across requests.
You listen to `process.on(‘uncaughtException’)` and `process.on(‘unhandledRejection’)`. However, it is dangerous to keep the server running after an uncaught exception due to unpredictable state. You should log the error to a service (like Sentry), and then synchronously shut down the process (`process.exit(1)`), letting a process manager (PM2/Kubernetes) restart it cleanly.
The built-in `crypto` module provides cryptographic functionality that includes a set of wrappers for OpenSSL’s hash, HMAC, cipher, decipher, sign, and verify functions.
In Express, an error-handling middleware is distinct because it takes exactly four arguments: `(err, req, res, next)`. If you pass an argument to `next(err)` in any normal route, Express will bypass all standard middleware and jump straight to the nearest error handler.
Buffers allocate raw memory *outside* the V8 JavaScript engine heap, meaning they aren’t subject to V8’s memory limits (usually ~1.5GB). `Buffer.alloc()` creates a zero-filled, safe buffer. `Buffer.allocUnsafe()` skips zero-filling, which is faster but may contain old, sensitive data from RAM.
A reverse proxy (like NGINX or HAProxy) sits in front of your Node server. Node is excellent at running application logic but mediocre at handling raw network tasks. The proxy handles SSL termination, gzip compression, serving static assets, and load balancing, freeing Node to focus strictly on API logic.
PM2 is a production process manager for Node.js. It ensures your application stays online 24/7 by automatically restarting it if it crashes. It also provides built-in load balancing (Cluster mode), log management, and zero-downtime reloads.
Microservices often use an Event-Driven Architecture via Message Brokers (like RabbitMQ or Apache Kafka). Instead of Service A making a synchronous HTTP request to Service B, Service A publishes an event to a queue, and Service B consumes it asynchronously.
gRPC is a high-performance RPC framework developed by Google. Unlike REST, which uses JSON over HTTP/1.1, gRPC uses Protocol Buffers (binary data) over HTTP/2. It is strictly typed and vastly faster, making it ideal for internal microservice-to-microservice communication.
An API Gateway is a server that acts as a single entry point into a system of multiple backend microservices. It handles cross-cutting concerns like authentication, routing, rate limiting, and analytics.
Use environment variables. You rely on the `NODE_ENV` variable to dictate the environment. Libraries like `dotenv` load local `.env` files, but in production, variables should be injected by the host (AWS Parameter Store, Kubernetes Secrets, or Docker Compose).
Dependency Injection (DI) is a design pattern where an object receives its dependencies from the outside rather than creating them internally (e.g., using `require`). This decouples code and makes unit testing incredibly easy.
Test-Driven Development (TDD) is writing tests before writing the actual code. In Node, standard tools include Jest or Mocha/Chai for unit and integration testing, and Supertest for testing Express HTTP endpoints.
Jest provides `jest.mock()` to replace the actual implementation of a module with a dummy version. This isolates the function you are testing by preventing side effects like actual HTTP calls or database writes.
By default, `express-session` uses a MemoryStore, which stores session data in the Node process RAM. If the server restarts, all users are logged out. A Persistent Store (like `connect-redis` or `connect-pg-simple`) saves session data externally.
An index is a data structure (usually a B-Tree) that improves the speed of data retrieval operations on a database table. If your Express API queries a table by `email`, applying an index to the `email` column prevents a slow “full table scan”.
A transaction is a sequence of DB operations that must all succeed, or all fail completely (ACID properties). You start a transaction, run queries, and then either `COMMIT` or `ROLLBACK` on error.
Never trust client data. You should validate payloads at the route level before they reach your controllers. Popular libraries include Joi, Zod, or express-validator to enforce data types, lengths, and strict schemas.
Before sending a complex HTTP request (like PUT/DELETE, or containing custom headers), the browser automatically sends an `OPTIONS` request called a preflight. It asks the Node server, “Are you okay with me sending this request?”
SSE is a standard allowing a client to receive automatic updates from a server via an HTTP connection. Unlike WebSockets (which are bi-directional), SSE is uni-directional (Server to Client) and operates over standard HTTP.
HTTP is stateless and strictly Request-Response (client must ask, server answers). WebSockets create a persistent, full-duplex TCP connection. Both the client and server can push messages to each other at any time.
The N+1 problem occurs when you query a list of items (1 query), and then iterate over that list, executing an additional query for each item (N queries). This crushes performance. It is solved using tools like DataLoader (batching) or DB joins.
Bull is a Redis-based queue for Node.js used to handle background jobs. You use it to offload tasks that take a long time and don’t require an immediate HTTP response to the user.
Health monitoring involves exposing a `/health` endpoint, collecting metrics (CPU, Memory, Request latency) using APM tools like Datadog, New Relic, or Prometheus, and setting up alerts for anomaly detection.
When a frontend app needs data from a 3rd party API that does not support CORS, the browser will block the request. You can create a proxy route on your Node backend to fetch the data on behalf of the client (servers don’t enforce CORS on outbound requests) and return it to the frontend.
When you `require()` a module in Node, it is evaluated once, and the result is cached in `require.cache`. Subsequent calls to `require` for the same file will return the exact same cached object reference without re-executing the code.
Content negotiation is an HTTP mechanism where the client tells the server what format of data it expects (using the `Accept` header), and the server (e.g., Express) formats the response accordingly (JSON, XML, HTML).
File uploads are sent using `multipart/form-data`. Express cannot parse this out of the box. You must use middleware like Multer or Busboy to intercept the binary stream, parse the file, and save it to disk or a cloud bucket.
Both methods are highly synchronous and CPU-intensive. Parsing massive JSON payloads (megabytes in size) will block the Event Loop entirely while V8 evaluates the string.
An API endpoint is idempotent if making multiple identical requests has the same effect as making a single request. `GET`, `PUT`, and `DELETE` should be idempotent. `POST` is typically not (it creates a new resource each time).
You use a Node Version Manager like NVM (or Volta) and include a `.nvmrc` file at the root of your project containing the specific Node version (e.g., `v18.17.0`). Additionally, you lock engines in `package.json`.
An ETag (Entity Tag) is an HTTP response header that provides a unique hash representing the requested resource. Express generates this automatically for responses.
The `os` module provides operating system-related utility methods. It allows a Node app to query the underlying server for metrics like total memory, free memory, CPU architecture, and network interfaces.
Prisma is a Next-Generation ORM that replaces traditional class-based models with a custom schema file. It auto-generates a fully type-safe query builder customized exactly for your database, offering vastly superior TypeScript integration and auto-completion compared to older ORMs.
The Thundering Herd problem occurs when a highly trafficked cache key expires, and suddenly hundreds of concurrent requests all hit the primary database simultaneously to rebuild the cache, causing the DB to crash.