Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Node.js Event Loop & Core Concepts
This guide provides a foundational understanding of how Node.js works, its architecture, and how it handles asynchronous operations.1. Introduction to Node.js
Node.js is a JavaScript Runtime Environment that allows JavaScript to run outside the browser. It is built by wrapping the Google V8 engine and the Libuv library to enable server-side development. Core Principle:Node.js is a SINGLE-THREADED environment While JavaScript execution happens on a single thread, Node.js offloads complex tasks to the operating system or its internal thread pool.Primary Use Cases:
- I/O Operations: Reading/Writing files, Database calls.
- Real-time Applications: Chat apps, Game servers.
- REST APIs: Fast, scalable web services.
2. Event Loop Architecture
The Event Loop is the “heart” of Node.js. It monitors for pending tasks and manages asynchronous operations, enabling non-blocking I/O.How Node.js Reads Code
- Top-to-bottom: Executes synchronous code immediately.
- Delegation: Asynchronous tasks (like timers or network calls) are sent to the Event Loop.
- Callback Queue: Once a task is done, its callback is queued and processed when the main thread is free.
3. Blocking vs Non-Blocking
Blocking Code Example
Blocking Code Example
Non-Blocking Code Example
Non-Blocking Code Example
4. Event Loop Phases & Priority
The Event Loop executes tasks in a specific hierarchical order:| Phase | Priority | Tasks |
|---|---|---|
| Microtask Queue | Highest | process.nextTick(), Promise.resolve() |
| Timers | 1st Macrotask | setTimeout(), setInterval() |
| I/O Callbacks | 2nd Macrotask | Network / File System results |
| Poll | 3rd Macrotask | New I/O events |
| Check | 4th Macrotask | setImmediate() |
| Close | 5th Macrotask | Socket closures, cleanup |
5. CPU Intensive Tasks & Multi-Threading
Because of its single-threaded nature, CPU-intensive tasks (like heavy loops) can block the entire server, making it unresponsive to other requests.Solution 1: Child Processes (fork)
Creates a separate instance of Node.js with its own memory. Good for isolated scripts.
Solution 2: Worker Threads
Threads that share memory with the main process. More lightweight and efficient for mathematical calculations.Thread Pool
Node.js uses Libuv to maintain a thread pool (default size: 4) for operations like file I/O, hash generation (crypto), and compression (zlib).
6. Streams & Buffers
Processing large data efficiently.| Feature | Stream | Buffer |
|---|---|---|
| Mechanism | Processes data in chunks | Loads everything into memory |
| Memory | Efficient (low footprint) | High (can crash on large files) |
| Use Case | Video, big logs, file uploads | Small, fixed-size data |
7. Rate Limiting
Crucial for protecting your server from DDoS attacks and Resource Exhaustion. Implementation: Use libraries likeexpress-rate-limit to restrict requests per IP address.
8. Event-Driven Architecture
Using theEventEmitter to create decoupled, scalable applications.
9. Common Interview Questions
Q1: Difference between setImmediate() and process.nextTick()?
Q1: Difference between setImmediate() and process.nextTick()?
process.nextTick() is the “cut the line” pass, while setImmediate() waits its turn in the queue.process.nextTick()is a Microtask. It fires immediately after the currently executing operation completes, before the event loop moves to any other phase. Internally, Node maintains a separatenextTickQueuethat gets drained completely between every phase transition of the event loop. This means 1,000 recursivenextTickcalls will all execute before a single timer or I/O callback fires.setImmediate()is a Macrotask that runs in the Check phase of the event loop. It executes after the Poll phase, which means I/O callbacks that are already queued will fire before yoursetImmediatecallback.- The starvation problem is real: If you recursively call
process.nextTick(), you starve the event loop. I/O callbacks, timers, and everything else will never fire. This is not theoretical — a team at one company discovered their WebSocket heartbeats were timing out because a data-transformation pipeline was chainingnextTickrecursively. Switching tosetImmediatefixed 100% of their connection drop issues overnight. - When to use which: Use
nextTickwhen you need something to happen before any I/O (e.g., ensuring an event handler is registered before an event fires). UsesetImmediatewhen you want to yield to the event loop and let pending I/O drain first.
- What happens if you call
process.nextTick()inside a Promise.then()handler? Which executes first — the nextnextTickor the next chained Promise? - Can you describe a production scenario where choosing
setImmediateovernextTickwould prevent a bug? - How does
queueMicrotask()relate toprocess.nextTick()and which takes priority?
Q2: What is the Thread Pool in Node.js?
Q2: What is the Thread Pool in Node.js?
- Default size is 4 threads. You can increase it by setting
UV_THREADPOOL_SIZE(max 1024). In production, teams running crypto-heavy workloads (like bcrypt password hashing) often bump this to 8-16. The setting must be applied before the thread pool initializes — set it at the very top of your entry file or via environment variable, not inside a route handler. - Operations that use the thread pool: File System operations (
fs.readFile,fs.writeFile), cryptographic functions (crypto.pbkdf2,crypto.randomBytes), DNS lookups viadns.lookup()(but NOTdns.resolve()— this is a classic gotcha), and compression (zlib.gzip,zlib.deflate). - Operations that do NOT use the thread pool: Network I/O (TCP/UDP sockets, HTTP requests). These use OS-level async primitives (epoll on Linux, kqueue on macOS, IOCP on Windows) which are far more scalable.
- Real-world impact: If you have 4 thread pool threads and 4 concurrent
fs.readFilecalls, a 5th call will queue and wait. A company discovered their API latency spiked 3x during peak hours because bcrypt hashing (which uses the thread pool) was competing with file uploads for the same 4 threads. IncreasingUV_THREADPOOL_SIZEto 12 and moving file uploads to streaming (which bypasses the pool) brought p99 latency from 800ms back to 200ms.
- How would you diagnose thread pool exhaustion in production? What metrics or tools would you use?
- Why does
dns.lookup()use the thread pool butdns.resolve()does not? What practical difference does this make? - If you set
UV_THREADPOOL_SIZEto 128 on a 4-core machine, what happens? Is bigger always better?
Q3: fork() vs spawn()?
Q3: fork() vs spawn()?
child_process module, but they serve fundamentally different purposes.fork()is specialized for creating child Node.js processes. It automatically sets up an IPC (Inter-Process Communication) channel between parent and child, so you can send messages viachild.send()andprocess.on('message'). Each forked process gets its own V8 instance and its own memory space (typically 30-50 MB baseline). Use it when you need to offload CPU-heavy JavaScript work (like report generation or data transformation) to keep your main server responsive.spawn()is general-purpose. It launches any executable as a child process (Python scripts, shell commands, ffmpeg, ImageMagick). Communication happens through stdio streams (stdout,stderr), not IPC. It does NOT create a new V8 instance, so memory overhead depends entirely on the spawned process.- Key difference in data handling:
spawnstreams data back via stdout, making it suitable for large outputs (e.g., piping a 2 GB database dump).forksends structured JS objects over IPC, which must be serialized/deserialized — sending a 500 MB JSON object over IPC will serialize the entire thing and can cause out-of-memory errors. exec()vsspawn():execbuffers the entire output in memory (default 1 MB limit, configurable viamaxBuffer) then passes it to the callback.spawnstreams it. For a command that outputs 10 bytes, useexec. For a command that outputs 10 GB, usespawn.
- When would you choose Worker Threads over
fork()? What is the key architectural difference? - You need to run 50 concurrent
fork()processes for a batch job. What happens to your server? How do you manage it? - How does
execFile()differ fromexec(), and why does it matter for security?
Q4: Why is Node.js not ideal for heavy computation?
Q4: Why is Node.js not ideal for heavy computation?
- Quantifying “heavy”: A computation that takes more than 50-100ms on the main thread is already dangerous in a production server handling concurrent requests. At 200ms of blocking, you will see noticeable latency spikes. At 1+ second, your server is effectively unresponsive during that window.
- Mitigation strategies (in order of preference):
- Worker Threads (Node 12+): Share memory with the main process via
SharedArrayBuffer, minimal startup overhead (~5ms), ideal for CPU-bound JS work like JSON parsing of massive payloads, bcrypt hashing, or data aggregation. Use a worker pool (like thepiscinaorworkerpoollibraries) to avoid the cost of spinning up new threads per request. - Child Processes (
fork/spawn): Full process isolation, own V8 heap. Higher memory cost (~30-50 MB each). Use when you need crash isolation (a segfault in the child does not kill the parent). - Offload to specialized services: For image/video processing, use a dedicated service (Sharp + a queue, or a Python microservice). For ML inference, use a Python/Go service behind gRPC. A company I know moved PDF generation from their Node API to a separate Go microservice and reduced their p95 latency from 4.2s to 180ms.
- Native addons (N-API/NAPI): Write the hot path in C++ and call it from Node. Libraries like Sharp (image processing) and bcrypt do exactly this.
- Worker Threads (Node 12+): Share memory with the main process via
- What Node IS great for: I/O-bound workloads where you are mostly waiting on network, disk, or database responses. A single Node process can handle 10,000+ concurrent connections because waiting on I/O does not block the thread.
- How would you detect if the event loop is being blocked in production? Name specific tools or metrics.
- What is the
--max-old-space-sizeflag, and when would you tune it? - You have a Node.js API that needs to resize images on upload. Walk me through three different architectures, with trade-offs for each.
Q5: What are Streams in Node.js and what types exist?
Q5: What are Streams in Node.js and what types exist?
- Four types of streams:
- Readable: Source of data. Examples:
fs.createReadStream(), HTTP request body (req),process.stdin. - Writable: Destination for data. Examples:
fs.createWriteStream(), HTTP response (res),process.stdout. - Duplex: Both readable and writable, independently. Example: TCP sockets (
net.Socket). - Transform: A duplex stream where the output is a computed transformation of the input. Examples:
zlib.createGzip(),crypto.createCipheriv().
- Readable: Source of data. Examples:
- Why streams matter in production: Without streams, serving a 2 GB file means loading 2 GB into memory per request. With 100 concurrent users, that is 200 GB of RAM. With streams, each request uses only the
highWaterMarkbuffer size (default 64 KB for file streams), so 100 concurrent users need roughly 6.4 MB total. - Backpressure is the critical concept most developers miss. If a writable stream is slower than a readable stream (e.g., writing to a slow disk while reading from fast RAM), data piles up in memory. The
.pipe()method handles backpressure automatically by pausing the readable stream when the writable stream’s internal buffer is full. If you manually consume streams with.on('data'), you must handle backpressure yourself or risk memory exhaustion. pipeline()vs.pipe(): Always preferstream.pipeline()(Node 10+) over.pipe(). Thepipelinefunction properly handles error propagation and cleanup. With.pipe(), if the writable stream errors, the readable stream is NOT automatically destroyed, leading to resource leaks.
.pipe() and pipeline(). Senior candidates should mention highWaterMark and real memory impact numbers.Red flag answer: “Streams are for reading files” or inability to name all four stream types. If a candidate does not mention backpressure, they have likely never dealt with streams under real load.Follow-up:- What is
highWaterMarkand how would you tune it for different workloads? - Explain backpressure. What happens if you consume a readable stream with
.on('data')and your writable destination is slow? - How would you implement a custom Transform stream for, say, CSV-to-JSON conversion on a 10 GB file?
Q6: How does Node.js handle errors in async code?
Q6: How does Node.js handle errors in async code?
- Callback pattern: Errors are the first argument of the callback (the “error-first callback” convention). If you forget to check
err, the error is silently swallowed and your app continues in a corrupted state. This is arguably the #1 source of hard-to-debug Node.js bugs. - Promise pattern: Errors propagate through the
.catch()chain. An unhandled rejection (a rejected Promise with no.catch()) used to silently disappear. Since Node 15, unhandled rejections crash the process by default (--unhandled-rejections=throw). This is the correct behavior — an unhandled error means your app is in an unknown state. - async/await pattern: Use
try/catchblocks. The gotcha: if you fire off multiple async operations withoutawaitand one throws, the error becomes an unhandled rejection that escapes yourtry/catch. - EventEmitter pattern: Emitters crash the process if an
'error'event fires and no listener is registered. Always attach.on('error')to streams, sockets, and custom emitters. - The
processsafety nets:process.on('uncaughtException')catches synchronous errors that escaped all try/catch blocks.process.on('unhandledRejection')catches Promises that rejected without a handler. In production, use these to log the error and gracefully shut down (finish in-flight requests, close DB connections, thenprocess.exit(1)). Never use them to “recover” and continue — your app state is corrupted.
uncaughtException should NOT be used for recovery? Do you know about the Node 15 behavior change for unhandled rejections?Red flag answer: “I just use try/catch everywhere” without discussing callbacks, EventEmitters, or the process safety nets. Another red flag: using uncaughtException to “keep the server running.”Follow-up:- What changed in Node 15 regarding unhandled Promise rejections, and why was that change made?
- How do you handle errors in a stream pipeline? What happens if a Transform stream throws mid-way?
- Your production Node.js app is silently failing — requests return empty responses but no errors in logs. How do you diagnose this?
Q7: Explain the cluster module and how Node.js achieves horizontal scaling.
Q7: Explain the cluster module and how Node.js achieves horizontal scaling.
- How it works: A master process calls
cluster.fork()to create worker processes. Each worker is a full Node.js instance with its own event loop and memory. The master distributes incoming connections to workers using a round-robin algorithm (default on Linux) or lets the OS handle it (default on Windows). Workers share the same port viaSO_REUSEPORTor the master accepting connections and handing them off. - Memory implications: Each worker is a full process, so a 16-worker cluster on a server where each process uses 150 MB means ~2.4 GB just for Node. This is why you do not blindly set workers to CPU count — profile your memory first.
- PM2 in practice: Most production Node deployments use PM2 instead of writing cluster code manually.
pm2 start app.js -i maxauto-forks to the number of CPUs, handles restarts on crash, provides zero-downtime reloads viapm2 reload, and centralizes logging. Under the hood, PM2 uses the same cluster module. - The sticky session problem: If you use cluster with WebSockets or session-based auth (in-memory sessions), you have a problem. A user’s first request might go to Worker 1 (which stores the session), but the next request goes to Worker 3 (which has no session). Solutions: externalize sessions to Redis, use JWT tokens (stateless), or enable sticky sessions in your load balancer.
- Cluster vs. Docker/K8s: In containerized environments, the common pattern is to run ONE Node process per container and scale at the container/pod level. This gives you better isolation, resource limits, and rolling deployments. Running cluster inside a container is double-scaling and wastes resources.
- How does zero-downtime deployment work with the cluster module (or PM2)?
- Why would you choose running one Node process per Docker container over using the cluster module inside a container?
- How does the master process distribute incoming connections to workers? What is the difference between round-robin and OS-level scheduling?
Q8: What is the difference between CommonJS (require) and ES Modules (import)?
Q8: What is the difference between CommonJS (require) and ES Modules (import)?
- CommonJS (
require): Synchronous loading. When Node hits arequire(), it stops execution, reads the file, executes it, caches the result inrequire.cache, and returnsmodule.exports. Because it is synchronous, you canrequire()conditionally inside anifblock or dynamically based on runtime values. This is why CommonJS was originally the only option for Node — synchronous loading is fine on a server where files are local. - ES Modules (
import/export): Asynchronous, statically analyzable. Imports are hoisted and resolved before any code executes. This means you cannot conditionally import (useimport()for dynamic imports). The static nature enables tree-shaking (dead code elimination) by bundlers. In Node, you enable ESM by setting"type": "module"inpackage.jsonor using the.mjsextension. - Loading differences that bite you: CommonJS
requirereturns a copy of primitive exports (numbers, strings). ES Modules export live bindings — if the exporting module changes a variable, the importing module sees the new value. This difference causes subtle bugs when migrating. - Interop hell: You can
importa CommonJS module (Node wraps itsmodule.exportsas the default export). You CANNOTrequire()an ES Module — you must useawait import()instead. This asymmetry means if a popular library switches to ESM-only, every CommonJS consumer must refactor their import code. This has caused significant community friction (the “pure ESM package” debate). __dirnameand__filename: These are CommonJS globals. They do not exist in ES Modules. Useimport.meta.urlwithfileURLToPath()instead.
- What is
require.cacheand how can it cause issues with hot-reloading or test isolation? - Can you explain the “dual package hazard” and why it matters for library authors?
- Why are ES Modules faster for frontend bundlers but the loading difference is mostly irrelevant for backend Node.js?
Q9: How does Node.js garbage collection work and how do you debug memory leaks?
Q9: How does Node.js garbage collection work and how do you debug memory leaks?
- V8 memory structure: The heap is divided into New Space (young generation, ~1-8 MB) and Old Space (old generation, up to 1.5 GB by default on 64-bit). New objects are allocated in New Space. Objects that survive two GC cycles get promoted to Old Space.
- Scavenge (Minor GC): Cleans New Space using a semi-space algorithm. Very fast (1-5ms). Happens frequently. This is why short-lived objects (request/response data) are essentially “free” in terms of GC pressure.
- Mark-Sweep-Compact (Major GC): Cleans Old Space. Slower (50-100ms+ depending on heap size). Can cause noticeable latency spikes. This is why long-lived objects (caches, connection pools) need careful management.
- Common memory leak sources in production:
- Global caches without eviction: Storing data in a plain object or Map that grows forever. Fix: use an LRU cache with a max size (e.g.,
lru-cachepackage). - Event listener accumulation: Adding
.on()listeners in a request handler without removing them. Node warns at 11 listeners (MaxListenersExceededWarning), but many teams suppress this warning instead of fixing the leak. - Closures capturing large scopes: An event handler closure that accidentally captures a reference to a large object, preventing it from being GC’d.
- Unreferenced timers:
setIntervalcallbacks that are never cleared.
- Global caches without eviction: Storing data in a plain object or Map that grows forever. Fix: use an LRU cache with a max size (e.g.,
- Debugging toolkit:
process.memoryUsage()for quick heap snapshots (rss, heapUsed, heapTotal, external).--inspectflag + Chrome DevTools for heap snapshots and allocation timelines.node --max-old-space-size=4096to increase the heap limit (but this just delays the OOM, it does not fix the leak).clinic.js(specificallyclinic doctorandclinic heapprofile) for automated diagnostics.- In production, expose
heapUsedas a Prometheus metric and alert when it consistently trends upward.
- Your Node.js service RSS grows from 200 MB to 1.2 GB over 48 hours, then OOMs. Walk me through your debugging process step by step.
- What is the difference between
heapUsed,heapTotal,rss, andexternalinprocess.memoryUsage()? - How does
WeakRefandFinalizationRegistry(Node 14+) help prevent certain categories of memory leaks?
Q10: Explain the EventEmitter pattern and when you would build on it vs. use alternatives.
Q10: Explain the EventEmitter pattern and when you would build on it vs. use alternatives.
EventEmitter is the backbone of Node.js. Nearly every core module inherits from it — Streams, HTTP servers, child processes, the fs watcher. Understanding it is understanding Node’s architecture.- How it works internally: An EventEmitter maintains a hash map where keys are event names and values are arrays of listener functions.
emit('event')iterates over the array and calls each listener synchronously, in registration order. This is the critical detail most people miss: emitting is synchronous. If you have 5 listeners and the 3rd one takes 200ms, the 4th and 5th listeners wait, and so does all code after theemit()call. onvsonce:onregisters a persistent listener.onceregisters a listener that auto-removes itself after the first invocation. Internally,oncewraps the listener in a function that callsremoveListenerafter firing. Useoncefor initialization events (e.g., database connected) andonfor recurring events (e.g., incoming data).- Error handling contract: If you emit an
'error'event and no listener is registered, Node throws the error and crashes the process. This is by design — unhandled errors should crash loudly, not fail silently. - Memory leak potential: Every
.on()call adds a listener to the array. If you register listeners inside a request handler (a common mistake), the array grows with every request. Node’s default warning threshold is 10 listeners per event name (configurable viasetMaxListeners()). - When to use EventEmitter vs. alternatives:
- Use EventEmitter for intra-process, pub/sub-style decoupling (e.g., “when a user signs up, send email AND create audit log AND provision resources” — each in its own listener).
- Use message queues (RabbitMQ, SQS, BullMQ) for inter-process or inter-service communication, retry logic, and persistence.
- Use RxJS Observables when you need operators for filtering, debouncing, combining, or transforming event streams.
- You emit an event with 10 listeners attached. The 5th listener throws an error. What happens to listeners 6-10?
- How would you make EventEmitter listeners execute asynchronously so they do not block the emit call?
- When would you choose a message queue (like BullMQ or RabbitMQ) over EventEmitter for event-driven architecture?
Q11: What happens when you type 'node app.js' and press Enter?
Q11: What happens when you type 'node app.js' and press Enter?
- Step 1 — OS process creation: The OS creates a new process, loads the Node.js binary (which includes V8 and Libuv), allocates memory for the heap and stack, and begins executing the Node bootstrap code in C++.
- Step 2 — V8 initialization: V8 creates an Isolate (an independent instance of the engine with its own heap), creates a Context (the global scope where your code runs), and compiles the built-in JavaScript modules (like
console,process,Buffer). - Step 3 — Libuv event loop creation: The event loop is initialized but NOT yet running. The thread pool is created (default 4 threads). OS-level async handles (epoll/kqueue/IOCP) are set up.
- Step 4 — Module loading: Node resolves
app.jsto an absolute path, reads the file, wraps it in the module wrapper function(function(exports, require, module, __filename, __dirname) { ... }), and passes it to V8 for compilation and execution. This is whyrequire,module,__filename, and__dirnameare available — they are injected function parameters, not true globals. - Step 5 — Synchronous execution: V8 executes your top-level code synchronously, top to bottom. Any
require()calls trigger the same wrap-and-execute process recursively. Async operations (timers, I/O) register callbacks but do not execute yet. - Step 6 — Event loop starts: Once all synchronous code finishes, Node enters the event loop. If there are pending async operations (timers, I/O, listeners), the loop keeps running. If there is nothing left to process (no open handles or pending callbacks), the loop exits and the process terminates with code 0.
- Why does a Node.js process exit after running a simple script with no timers or servers, but stays alive when you call
http.createServer().listen()? - What is the module wrapper function and why does Node use it instead of running your code directly in the global scope?
- What is a V8 Isolate and why is it relevant to Worker Threads?
Q12: How do you handle graceful shutdown in a Node.js production server?
Q12: How do you handle graceful shutdown in a Node.js production server?
app.listen(3000) and stop there. In production, you need to handle SIGTERM, drain connections, close database pools, and exit cleanly — or your users see 502 errors and your data gets corrupted.- Why it matters: When Kubernetes sends SIGTERM to scale down a pod, or PM2 restarts a process, you have a window (typically 30 seconds) to finish in-flight requests before SIGKILL forcefully terminates you. If you do not handle SIGTERM, in-flight requests get dropped, database transactions are left incomplete, and message queue messages are acknowledged but never processed.
- The shutdown sequence:
- Stop accepting new connections: Call
server.close()— this stops the server from accepting new TCP connections but lets existing connections finish. - Wait for in-flight requests: Set a timeout (10-15 seconds) for existing requests to complete.
- Close external connections: Close database pools, Redis connections, message queue consumers.
- Exit: Call
process.exit(0)for clean exit orprocess.exit(1)if something went wrong during shutdown. - Force kill safety net: Set a hard timeout (e.g., 25 seconds if K8s gives you 30) that calls
process.exit(1)in case graceful shutdown gets stuck.
- Stop accepting new connections: Call
- Keep-alive connections: HTTP/1.1 keep-alive connections stay open even after
server.close(). You need to track active connections and destroy idle ones, or setConnection: closeheader on responses during the shutdown window.
node app.js in their terminal from those who have operated services at scale.Red flag answer: “I just restart the process” or “PM2 handles that for me” without being able to explain what PM2 is actually doing under the hood.Follow-up:- What is the difference between SIGTERM and SIGKILL, and why can you not handle SIGKILL?
- How does Kubernetes interact with your graceful shutdown code? What is the
terminationGracePeriodSecondssetting? - What happens to WebSocket connections during a graceful shutdown? How do you handle them differently from HTTP?
Q13: Explain middleware in the context of Node.js HTTP servers.
Q13: Explain middleware in the context of Node.js HTTP servers.
http.createServer.- What it is: A middleware is a function that sits between the incoming request and the final response. It receives
(req, res, next), can modify the request/response objects, execute any code, end the request-response cycle, or callnext()to pass control to the next middleware. The order ofapp.use()calls defines the execution order, and this order matters more than most developers realize. - The middleware chain is a pipeline: Think of it as a stack of functions. The request flows down through each middleware until one sends a response. If no middleware sends a response and
next()is called past the last one, Express sends a default 404. - Types of middleware (in typical order):
- Request parsing:
express.json(),express.urlencoded(),cookie-parser. - Security:
helmet(sets security headers),cors(cross-origin handling). - Authentication: Verify JWT tokens, session cookies.
- Authorization: Check user roles/permissions for the specific route.
- Business logic: Your route handlers.
- Error handling: The
(err, req, res, next)four-argument middleware at the END of the chain.
- Request parsing:
- Performance trap: Middleware runs on EVERY request that matches its mount path. A company added a logging middleware that performed a synchronous
JSON.stringifyon the full request body. At 5,000 req/s with 50 KB average body size, this added 40ms per request and saturated a CPU core. Moving to async logging (writing to a stream) reduced overhead to under 1ms. - Error middleware is special: Express identifies error handlers by their four-parameter signature
(err, req, res, next). If you accidentally omit thenextparameter (even if you do not use it), Express treats it as a regular middleware and your error handler never fires. This is the single most common Express debugging headache.
- What happens if a middleware calls
next()but also sends a response withres.json()? What error do you get? - How does Koa’s middleware model (async/await with
next()returning a Promise) differ from Express’s callback model? - How would you implement a rate-limiting middleware that shares state across a cluster of Node.js processes?