Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

Node.js Backend Developer Top 100 Interview Questions

100 Node.js backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers the event loop, async patterns, streams, HTTP servers, databases, caching, testing, security, performance and system design, from the basics to production scenarios.

100 questions

Questions 1-20 of 100

  1. Q1.What does a Node.js backend developer own beyond writing routes?

    A Node.js backend developer owns the service end to end: how requests are handled, how data is stored, and how the process behaves under load, failure and change. Routes are just the entry point.

    • Asynchronous design: structuring code so the event loop never blocks, and handling errors in every promise and callback.
    • Data and integrations: queries, transactions, caching and calls to other services with timeouts and retries.
    • Performance: knowing when work belongs in the event loop, the thread pool, a worker thread or a separate service.
    • Security: validating input, protecting secrets, managing a large dependency tree and avoiding JavaScript-specific risks such as prototype pollution.
    • Operations: structured logs, metrics such as event loop delay and heap use, graceful shutdown and safe deployments.
    • Code health: clear module boundaries, tests, static type checking and dependency upgrades.

    A strong Node.js engineer understands the runtime model deeply, because most production problems, from latency spikes to memory leaks, come from misunderstanding how the event loop and asynchronous code really work.

  2. Q2.What is Node.js, and why is it well suited to I/O-heavy services?

    Node.js is a runtime that executes JavaScript outside the browser, built on a high-performance JavaScript engine and an event loop with non-blocking I/O. Application code runs on a single main thread, while network and file operations complete in the background.

    • Non-blocking I/O: starting a database query or HTTP call does not wait; the code continues, and a callback or promise resumes when the result arrives.
    • Cheap concurrency: one process can hold thousands of open connections, because waiting connections cost memory, not threads.
    • One language across the stack: teams share code, types and validation logic between browser and server.
    • Large package ecosystem: libraries exist for most protocols and services.

    Where it is a weaker fit:

    • CPU-heavy work such as image processing or large computations blocks the main thread and every request with it, unless moved to worker threads or other services.
    • Numerical precision: JavaScript numbers are floating point, so money and large integers need care.

    Node.js shines for APIs, real-time services, proxies and integration layers, where most time is spent waiting on other systems.

  3. Q3.How does the Node.js event loop work, and what are its phases?

    The event loop repeatedly checks for completed work and runs the matching callbacks, in a fixed order of phases. Between every callback, Node.js also drains its process.nextTick queue and the promise microtask queue.

    flowchart TD
      A["Timers: expired setTimeout and setInterval callbacks"] --> B["Pending callbacks: deferred system errors"]
      B --> C["Poll: wait for and run I/O callbacks"]
      C --> D["Check: setImmediate callbacks"]
      D --> E["Close callbacks: closed sockets and handles"]
      E --> F{"Any work left?"}
      F -->|yes| A
      F -->|no| G["Process exits"]
    1. Timers: runs callbacks whose delay has passed; a delay is a minimum, not a guarantee.
    2. Poll: retrieves completed I/O events and runs their callbacks, and waits here when nothing else is ready.
    3. Check: runs setImmediate callbacks right after the poll phase.
    4. Close: handles close events such as a socket ending.

    What it means in practice:

    • Only one callback runs at a time, so a long synchronous task delays everything queued behind it.
    • The process stays alive while timers, sockets or servers remain active, which matters for clean shutdown.
  4. Q4.How do `process.nextTick`, promise callbacks, `setImmediate` and `setTimeout` differ in ordering?

    They go into different queues, which run at different moments. Knowing the order explains many "why did this run first" bugs.

    process.nextTickPromise callbackssetImmediatesetTimeout(fn, 0)
    Runs as soon as the current operation finishesRuns once the nextTick queue is empty, before the loop moves onRuns in the check phase, right after I/O pollingRuns in the next timers phase, after at least 1 ms
    Runs before any promise callbackAlso called microtasksRuns before timers when called inside an I/O callbackOrder against setImmediate varies when called from the main script

    Practical consequences:

    • Recursive process.nextTick or endless promise chains can starve I/O, because the loop never gets to poll.
    • Use setImmediate to break up long work, so I/O callbacks and timers can run between chunks instead of waiting for the whole job.
    • Prefer promises for ordinary async flow, and keep process.nextTick for rare cases such as emitting an event after a constructor returns.

    Example

    Inside an I/O callback, the order is fully predictable.

    const fs = require('node:fs');
    
    fs.readFile(__filename, () => {
      setTimeout(() => console.log('timeout'), 0);
      setImmediate(() => console.log('immediate'));
      Promise.resolve().then(() => console.log('promise'));
      process.nextTick(() => console.log('nextTick'));
      console.log('sync');
    });
    // Output:
    // sync
    // nextTick
    // promise
    // immediate
    // timeout
  5. Q5.What does it mean to block the event loop, and how do you detect it?

    Blocking the event loop means running synchronous code long enough that no other callback can run. Since all requests share one main thread, a 200-millisecond loop in one request adds 200 milliseconds of waiting to every other request in flight.

    Common causes:

    • Synchronous APIs such as readFileSync, pbkdf2Sync or execSync on the request path.
    • Parsing or stringifying very large JSON documents.
    • CPU-heavy loops: sorting huge arrays, building reports, image work.
    • Regular expressions with catastrophic backtracking on hostile input.
    flowchart LR
      A["Request A runs a long synchronous task"] --> B["Event loop cannot run other callbacks"]
      B --> C["Timers fire late, sockets wait"]
      C --> D["Latency rises for every request"]

    How to detect it:

    1. Measure event loop delay with perf_hooks.monitorEventLoopDelay and alert when its 99th percentile climbs.
    2. Watch the symptoms: latency rising across all endpoints while one CPU core sits at 100%.
    3. Profile CPU to see which function holds the main thread.
    4. Ban synchronous APIs in request code with a lint rule.
  6. Q6.Which Node.js operations use the thread pool, and why can that become a bottleneck?

    Some operations cannot be done with non-blocking operating system calls, so Node.js runs them on a small internal thread pool. It has four threads by default, shared by every such operation in the process.

    Runs on the thread poolUses non-blocking operating system I/O
    Most file system operationsNetwork sockets: TCP, HTTP and TLS connections
    dns.lookup, used by default for host namesDNS queries made with the resolver functions
    Asynchronous crypto work such as pbkdf2, scrypt and randomBytesTimers and child process pipes
    Asynchronous compression in zlibMost database drivers' network traffic

    Why it becomes a bottleneck:

    • Four slow password hashes occupy all four threads, so file reads and host name lookups queue behind them.
    • Slow file systems or network storage can tie up threads for long periods.

    How to manage it:

    • Raise the pool size with the UV_THREADPOOL_SIZE environment variable, set before the process starts, usually to around the number of cores.
    • Move heavy hashing or compression to worker threads or a separate service.
    • Cache DNS results, or connect by address where appropriate.
  7. Q7.How do callbacks, promises and async/await relate in Node.js?

    They are three generations of the same idea: run code when an asynchronous operation finishes. Modern code uses async/await, which is syntax built on promises.

    • Error-first callbacks: the original Node.js style, where the callback receives (error, result). Deep nesting and easy-to-miss error handling earned the name "callback hell".
    • Promises: objects representing a future result, chainable with .then and .catch, with errors flowing to the nearest handler.
    • async/await: an async function always returns a promise, and await pauses that function until a promise settles, so asynchronous code reads like synchronous code and works with try/catch.

    Bridging the styles:

    • util.promisify turns an error-first callback function into a promise-returning one.
    • Most core modules also offer promise versions, such as fs/promises and timers/promises.

    Common mistakes:

    • Forgetting await, so errors escape as unhandled rejections and work continues out of order.
    • Awaiting independent calls one after another instead of starting them together.
    • Mixing callbacks and promises in one function, which can call back twice.

    Example

    util.promisify wraps a callback API so both success and failure work with await.

    const util = require('node:util');
    
    function lookupUser(id, callback) {
      setImmediate(() => {
        if (id <= 0) return callback(new Error('invalid id'));
        callback(null, { id, name: 'Asha' });
      });
    }
    
    const lookupUserAsync = util.promisify(lookupUser);
    
    async function main() {
      const user = await lookupUserAsync(7);
      console.log(user.name); // Asha
      try {
        await lookupUserAsync(0);
      } catch (error) {
        console.log(error.message); // invalid id
      }
    }
    
    main();
  8. Q8.How do you handle errors correctly in asynchronous Node.js code?

    Make sure every asynchronous failure reaches a handler: await promises inside try/catch, attach .catch to promises you do not await, check the error argument in callbacks, and listen for error events on emitters and streams.

    • Unhandled rejections crash the process by default in modern Node.js, which is safer than silently continuing in an unknown state.
    • Throwing inside a callback that runs later, such as in a timer, cannot be caught by a try/catch around the code that scheduled it.
    • Streams and emitters report failures through error events; an error event with no listener throws and usually crashes the process.
    • Fire-and-forget work still needs a .catch that logs the failure.

    At the service level:

    1. Use an error-handling middleware that turns thrown errors into consistent responses.
    2. Use Error objects, never thrown strings, so stack traces exist.
    3. Add context with the cause option when wrapping errors.
    4. Log and exit on uncaughtException, letting the process manager restart a clean process, instead of trying to carry on.
  9. Q9.How do `Promise.all`, `Promise.allSettled`, `Promise.race` and `Promise.any` differ?

    All four take several promises and return one, but they differ in when they finish and how they treat failures. Choosing the right one avoids both lost errors and needless waiting.

    Promise.allPromise.allSettledPromise.racePromise.any
    Waits for every promise to succeedWaits for every promise to finish, either waySettles as soon as the first promise settlesResolves with the first promise that succeeds
    Rejects on the first failureNever rejects; reports each outcomeCopies the first result or failureRejects only if every promise fails
    Use when you need all resultsUse when partial results are usefulUse for a deadline against a timerUse for redundant sources, such as replicas

    Important details:

    • None of them cancel the remaining work; a rejected Promise.all leaves the other operations running. Pass an AbortSignal if they should stop.
    • Promise.any rejects with an AggregateError that lists every failure.
    • For many items, combine these with a concurrency limit instead of starting everything at once.

    Example

    Each combinator handles the same mix of fast, slow and failing calls differently.

    const ok = (value, ms) => new Promise((resolve) => setTimeout(() => resolve(value), ms));
    const fail = (message, ms) => new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms));
    
    async function main() {
      console.log((await Promise.all([ok('a', 20), ok('b', 10)])).join(',')); // a,b
      const settled = await Promise.allSettled([ok('a', 10), fail('down', 5)]);
      console.log(settled.map((result) => result.status).join(',')); // fulfilled,rejected
      console.log(await Promise.race([ok('slow', 30), ok('fast', 5)])); // fast
      console.log(await Promise.any([fail('x', 5), ok('backup', 15)])); // backup
      try {
        await Promise.all([ok('a', 20), fail('down', 5)]);
      } catch (error) {
        console.log(error.message); // down
      }
    }
    
    main();
  10. Q10.How do closures work in JavaScript, and how can they cause memory leaks?

    A closure is a function together with the variables from the scope where it was created. The function can use those variables later, even after the outer function has returned, because the closure keeps them alive.

    Why closures are useful in backend code:

    • Factories that capture configuration, such as a logger bound to a module name.
    • Middleware and handlers that capture shared dependencies.
    • Private state, such as a counter that only one function can change.

    How they leak memory:

    • Long-lived callbacks: an event listener or timer registered for each request captures that request's data, and if it is never removed, the data is never freed.
    • Large captured objects: a small closure that captures a huge buffer or result set keeps all of it alive.
    • Caches of functions: storing closures in a module-level map without eviction grows memory forever.

    How to avoid it:

    1. Remove listeners and clear timers when their purpose ends.
    2. Capture only the values you need, not whole request or response objects.
    3. Bound every cache, and compare heap snapshots when memory keeps growing.
  11. Q11.How does prototypal inheritance work, and how do classes relate to it?

    Every JavaScript object has a hidden link to another object, its prototype. When you read a property the object does not have, the engine follows the prototype chain until it finds the property or reaches the end.

    • Methods live on prototypes: all instances of a class share one copy of each method through the class's prototype object, rather than each instance holding its own.
    • class syntax is mostly a cleaner way to set up constructor functions and prototype chains; extends links one prototype to another.
    • super calls the parent's constructor or methods along that chain.
    • Own versus inherited properties: Object.hasOwn(obj, key) checks only the object itself, which matters when processing untrusted input.

    Practical points for backend code:

    • Prefer composition, passing collaborators in, over deep inheritance hierarchies.
    • Do not modify built-in prototypes such as Object.prototype; every object in the process would change.
    • Objects created with Object.create(null) have no prototype, which makes them safe dictionaries for keys supplied by users.

    Private fields declared with # are truly private to the class, unlike properties starting with an underscore by convention.

  12. Q12.What is the difference between `==` and `===`, and how does type coercion cause bugs?

    === compares value and type without conversion. == converts the operands to a common type first, following rules that produce surprising results, so backend code should use === almost everywhere.

    =====Object.is
    Converts operands to a common type, then comparesCompares value and type with no conversionLike ===, but NaN equals itself and 0 differs from -0
    0 == '' is true0 === '' is falseObject.is(NaN, NaN) is true
    Use only as the x == null shorthand, if the team agreesDefault for every comparisonRarely needed outside exact value checks

    Coercion surprises that cause real bugs:

    • 0 == '' and '0' == false are both true, so a quantity of 0 can look like a missing value.
    • Comparing strings with < or > compares character by character, so '10' < '9' is true when numbers arrive from query strings.
    • typeof null is 'object', a long-standing quirk that breaks naive checks for objects.
    • NaN is not equal to itself; use Number.isNaN.
    • + concatenates if either side is a string, so '5' + 1 is '51'.

    Defensive habits:

    1. Convert input explicitly at the boundary, such as Number(value) followed by a validity check.
    2. Use === and !==, and turn on a lint rule that enforces them.
    3. Use x == null only as a deliberate shorthand for "null or undefined", if your team agrees on it.
    4. Validate request data with a schema, so types are known before business logic runs.

    Example

    Each line shows a coercion rule that has caused production bugs.

    console.log(0 == ''); // true
    console.log('0' == false); // true
    console.log(null == undefined); // true
    console.log(null === undefined); // false
    console.log('10' > '9'); // false
    console.log('5' + 1); // 51
    console.log(typeof null); // object
    console.log(Number.isNaN(Number('12px'))); // true
  13. Q13.How do CommonJS modules and ES modules differ in Node.js?

    Node.js supports two module systems. CommonJS is the original one, using require and module.exports; ES modules are the JavaScript standard, using import and export.

    CommonJSES modules
    Loads with require, a normal function callLoads with import statements, resolved before code runs
    Loads synchronouslyLoads asynchronously, allowing top-level await
    Exports are a plain object assigned at run timeExports are live bindings that static tools can analyze
    Default for .js files without a module type settingUsed for .mjs files, or .js when the package declares module type
    Provides __dirname and __filenameUses import.meta.dirname and import.meta.url instead

    Interoperability:

    • ES modules can import CommonJS packages.
    • Requiring an ES module from CommonJS was historically impossible; recent versions support it for modules without top-level await.

    When to use which: choose ES modules for new projects, because they are the standard and enable better tooling, such as unused code removal. Keep a package consistent, since mixing systems causes confusing loading and default-export issues.

  14. Q14.How does module loading and caching work in Node.js?

    The first time a module is loaded, Node.js resolves its path, runs its top-level code once, and caches the result. Every later import of the same resolved file returns the cached exports.

    flowchart TD
      A["import or require"] --> B["Resolve to a file path"]
      B --> C{"Path already in module cache?"}
      C -->|Yes| D["Return cached exports"]
      C -->|No| E["Run top-level code once"]
      E --> F["Store exports under the resolved path"]
      F --> D

    Relative paths point to files, while bare names such as date-utils are looked up in the nearest node_modules folders, honoring the package's declared entry points.

    Consequences for backend code:

    • Modules act as singletons: a connection pool created at a module's top level is shared by every importer in that process.
    • Cache keys are paths: two copies of the same package in different folders are two separate modules, so instanceof checks and shared state can break.
    • Top-level side effects run at import time, so slow work there delays startup and makes tests harder.
    • Circular imports can return partially initialized exports.

    Good practice: keep modules free of heavy side effects, create shared resources in an explicit startup function, and pass them in where they are needed.

  15. Q15.How do you manage dependencies and lock files in a Node.js project?

    Declare direct dependencies in package.json, commit the lock file, and install exactly what the lock file records in every build and deployment. That makes builds reproducible even though version ranges allow newer releases.

    • Version ranges: a caret, such as ^2.4.0, accepts newer minor and patch versions; a tilde accepts only patch versions. Ranges are resolved when the lock file is created.
    • Lock file: records the exact version, source and integrity hash of every package in the tree, direct and indirect.
    • Clean installs in builds: use the package manager's clean-install mode, which installs exactly the lock file and fails if it does not match package.json.
    • Dev versus production: keep test and build tools in development dependencies, and omit them from production images.
    • Updates: update deliberately on a schedule, with automated pull requests and tests, rather than letting each fresh install pick new versions.

    Watch out for:

    • Packages that run install scripts, which execute code on your build machines.
    • Duplicate versions of the same package bloating the bundle and breaking shared state.
    • Unused dependencies, which still add attack surface.
  16. Q16.What are streams in Node.js, and what are the four stream types?

    Streams process data piece by piece as it arrives, instead of loading it all into memory first. They power files, HTTP requests and responses, sockets and compression in Node.js.

    ReadableWritableDuplexTransform
    A source you read data fromA destination you write data toBoth readable and writable, independentlyA duplex stream whose output is computed from its input
    An incoming HTTP request or a file being readAn HTTP response or a file being writtenA network socketCompression, encryption or parsing

    Why streams matter:

    • Constant memory: a 5 GB file can be copied using only a few kilobytes of buffer.
    • Lower latency: the first bytes reach the client before the whole result exists.
    • Backpressure: a slow consumer automatically slows the producer, instead of letting data pile up in memory.

    Modern usage:

    • Connect streams with stream.pipeline, which handles errors and cleanup on every stage.
    • Readable streams are async iterable, so for await (const chunk of stream) works.
    • Object mode lets streams carry JavaScript objects, such as parsed rows, instead of bytes.
  17. Q17.What is backpressure in Node.js streams, and how does `pipeline` handle it?

    Backpressure is the signal a slow consumer sends to a fast producer: "stop, I am full". Without it, a fast source such as a file read would push data into memory faster than a slow destination such as a network client can take it.

    • How it works: writable.write() returns false when the writable's internal buffer passes its high-water mark. The producer should pause until the drain event fires.
    • pipeline and pipe handle this automatically, pausing and resuming the source.
    • Manual code that ignores the return value of write and keeps writing is a classic cause of memory blowups.

    Why pipeline over pipe:

    • It forwards errors from any stage and destroys every stream in the chain, so failures do not leak file handles or sockets.
    • The promise version in stream/promises works naturally with await.
    • Async generator functions can be used as transform stages, which keeps parsing code simple.
    flowchart LR
      A["Fast readable source"] --> B["Transform stage"]
      B --> C["Slow writable destination"]
      C -->|buffer full: pause| A
      C -->|drained: resume| A

    Example

    An async generator parses CSV lines inside a pipeline, and the last stage sums the amounts.

    const { Readable } = require('node:stream');
    const { pipeline } = require('node:stream/promises');
    
    async function* parseAmounts(source) {
      for await (const line of source) {
        const [id, amount] = line.trim().split(',');
        if (id !== 'id') yield Number(amount);
      }
    }
    
    async function main() {
      let total = 0;
      await pipeline(
        Readable.from(['id,amount\n', '1,250\n', '2,100\n', '3,75\n']),
        parseAmounts,
        async function (source) {
          for await (const amount of source) total += amount;
        },
      );
      console.log(total); // 425
    }
    
    main();
  18. Q18.How do Buffers work in Node.js, and how do they differ from strings?

    A Buffer is a fixed-size chunk of raw bytes, stored outside the JavaScript heap. Strings are sequences of characters; converting between the two always needs an encoding such as UTF-8.

    BufferString
    Fixed-size raw bytes, stored outside the JavaScript heapImmutable sequence of characters, stored on the heap
    length counts bytes: Buffer.byteLength('₹') is 3length counts UTF-16 code units: '₹'.length is 1
    Best for files, sockets, hashes and imagesBest for text, after decoding the bytes with an encoding
    • Encodings: buffer.toString('utf8'), 'base64' or 'hex' decode bytes; Buffer.from(text, encoding) encodes them.
    • Splitting multi-byte characters: decoding chunk by chunk can cut a character in half; set an encoding on the stream, or use a string decoder, which handles boundaries.

    Safety points:

    • Buffer.alloc(size) returns zero-filled memory; Buffer.allocUnsafe is faster but may contain old data, so fill it completely before exposing it.
    • Large Buffers count toward process memory but not the JavaScript heap limit, so watch the external memory figure.
    • Compare secrets with crypto.timingSafeEqual on equal-length Buffers, not === on strings.
  19. Q19.How do EventEmitters work, and what are the common pitfalls?

    An EventEmitter lets objects publish named events that any number of listeners subscribe to. Servers, streams and sockets in Node.js are all emitters.

    • on and once register listeners; off removes one.
    • emit is synchronous: all listeners run immediately, in registration order, before emit returns. Slow listeners block the code that emitted the event.
    • The error event is special: emitting it with no listener throws the error, which usually crashes the process.

    Common pitfalls:

    • Listener leaks: adding a listener per request to a long-lived emitter, without removing it, grows memory. Node.js warns when more than ten listeners are added for one event.
    • Errors in listeners: an exception thrown in a listener propagates out of emit, into the code that emitted.
    • Async listeners: an async listener's rejected promise is not caught by emit; handle errors inside it.
    • Assuming delivery order across emitters, which is not guaranteed.

    For waiting on a single event with await, events.once(emitter, name) returns a promise. Each for await over events.on turns an event stream into an async iterator.

    Example

    Listeners run synchronously during emit, once fires only the first time, and an unhandled error event throws.

    const { EventEmitter } = require('node:events');
    
    const orders = new EventEmitter();
    orders.on('created', (order) => console.log(`email for ${order.id}`));
    orders.once('created', () => console.log('first order bonus'));
    
    console.log('before emit');
    orders.emit('created', { id: 1 });
    orders.emit('created', { id: 2 });
    console.log('after emit');
    
    try {
      orders.emit('error', new Error('payment provider down'));
    } catch (error) {
      console.log(`unhandled error event: ${error.message}`);
    }
    // Output:
    // before emit
    // email for 1
    // first order bonus
    // email for 2
    // after emit
    // unhandled error event: payment provider down
  20. Q20.How does JSON handling affect a Node.js service's performance and safety?

    JSON.parse and JSON.stringify are synchronous and run on the main thread, so very large payloads block the event loop. JSON also has gaps that cause subtle bugs with numbers, dates and special keys.

    Performance:

    • Parsing or stringifying a multi-megabyte document can take tens of milliseconds, during which no other request progresses.
    • Set a request body size limit, paginate large responses and stream big exports instead of building one giant string.
    • For truly large documents, use a streaming parser or move the work to a worker thread.

    Correctness and safety:

    • Large integers: numbers above 2 to the power of 53 lose precision, so send big identifiers as strings.
    • Dates become strings and do not come back as dates; parse them explicitly.
    • Unsupported values: BigInt makes JSON.stringify throw, and undefined properties are dropped silently.
    • Special keys: a parsed object can contain a __proto__ key, which is harmless on its own but dangerous when naively merged into other objects.
    • Circular references throw; log objects with a safe serializer.

    A toJSON method on a class controls exactly what is serialized, which also prevents accidentally leaking internal fields.