Menu
CoddyTech

Advanced Node.js Interview Questions

Production behavior and design: shutdown, request context, leaks, security, testing and scaling out.

10 questions
  1. How do you shut down a Node.js server gracefully?

    On SIGTERM, stop accepting connections, let in-flight requests finish, close database pools, then exit, with a hard timeout in case something hangs.

    const server = app.listen(3000);
    
    function shutdown(signal) {
      console.log(`${signal} received, closing`);
      server.close(async () => {   // stop accepting, wait for open requests
        await db.end();            // close the database pool
        process.exit(0);
      });
      setTimeout(() => process.exit(1), 10_000).unref(); // give up after 10 s
    }
    
    process.on('SIGTERM', () => shutdown('SIGTERM'));
    process.on('SIGINT', () => shutdown('SIGINT'));
    

    Fail the readiness probe first so the load balancer stops sending traffic. On Node 18 and earlier, idle keep-alive connections can hold server.close() open; call server.closeIdleConnections() (18.2 and later) there.

  2. What is AsyncLocalStorage and what problem does it solve?

    AsyncLocalStorage keeps a value that follows one async call chain, through await and timers, without passing it as an argument. The standard use is a request id in logs.

    JavaScript
    const { AsyncLocalStorage } = require('node:async_hooks');
    const context = new AsyncLocalStorage();
    
    function log(message) {
      const store = context.getStore();
      console.log(`[${store ? store.requestId : 'no request'}] ${message}`);
    }
    
    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    
    async function handleRequest(delay) {
      await sleep(delay);
      log('loaded user');
      await sleep(delay);
      log('sent response');
    }
    
    context.run({ requestId: 'req-1' }, () => handleRequest(20));
    context.run({ requestId: 'req-2' }, () => handleRequest(5));
    log('server started');

    The requests interleave, yet each line carries the right id. OpenTelemetry uses it for context propagation.

  3. How do you find and fix a memory leak in Node.js?

    Confirm it: heap usage that keeps rising across garbage collections under steady load. Then take two heap snapshots minutes apart (--inspect or v8.writeHeapSnapshot()), compare them, and follow the retainers. Usual culprits: an unbounded Map cache, listeners added per request to a long-lived emitter (MaxListenersExceededWarning), setInterval timers never cleared, and closures that capture a whole request.

  4. What are the most common security issues in Node.js applications?

    The common ones: injection (use parameterized SQL, and execFile with an argument array for shell commands), prototype pollution from deep-merging user JSON (below), ReDoS from a regex such as /(a+)+$/ that blocks the event loop on a crafted string, path traversal through names like ../../etc/passwd, vulnerable dependencies (npm audit, a committed lock file) and secrets in code or logs. Add helmet, rate limiting, schema validation and error responses without stack traces.

  5. How do you prevent path traversal when serving files in Node.js?

    Resolve the name against the base directory, then check the result is still inside it.

    JavaScript
    const path = require('node:path');
    
    const UPLOADS = '/srv/uploads';
    
    function safePath(name) {
      const full = path.resolve(UPLOADS, name);
      if (!full.startsWith(UPLOADS + path.sep)) {
        throw new Error(`blocked: ${name}`);
      }
      return full;
    }
    
    for (const name of ['report.pdf', '../../etc/passwd', 'a/../b.txt', '/etc/hosts']) {
      try {
        console.log(safePath(name));
      } catch (err) {
        console.log(err.message);
      }
    }

    path.resolve collapses .. and lets an absolute name replace the base, so check the resolved path, against UPLOADS + path.sep: a plain startsWith(UPLOADS) would accept /srv/uploads-old/secret.

  6. What is prototype pollution, and how does it happen in Node.js?

    Prototype pollution is user input writing properties onto Object.prototype, so every object inherits them, usually through a hand-written deep merge.

    function merge(target, source) {
      for (const key in source) {
        if (typeof source[key] === 'object' && source[key] !== null) {
          target[key] = merge(target[key] ?? {}, source[key]);
        } else {
          target[key] = source[key];
        }
      }
      return target;
    }
    
    const payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
    merge({}, payload);
    
    const user = {};
    console.log(user.isAdmin); // Node.js prints: true
    

    JSON.parse makes __proto__ an ordinary key, but target['__proto__'] reads the real prototype. Fix: skip __proto__, constructor and prototype, validate with a schema, or use a Map for user-keyed data.

  7. How do you test a Node.js application?

    Unit tests for pure logic, integration tests for routes against a real test database, a few end-to-end checks. Node has a built-in runner, node:test (stable since Node 20); Jest and Vitest are common.

    // sum.test.js, run with: node --test
    const test = require('node:test');
    const assert = require('node:assert/strict');
    const { sum } = require('./sum');
    
    test('adds two numbers', () => {
      assert.equal(sum(2, 3), 5);
    });
    
    test('rejects bad input', async () => {
      await assert.rejects(() => Promise.reject(new TypeError('bad')), TypeError);
    });
    

    For routes, export the app without listen and drive it with supertest.

  8. What changed in recent Node.js versions that you should know for an interview?

    FeatureSince
    Global fetchNode 18 (stable in 21)
    Test runner node:testNode 18 (stable in 20)
    --env-file=.envNode 20.6
    import.meta.dirnameNode 20.11
    require() of ES modulesNode 22.12
    Running .ts by stripping typesNode 23.6 (22.18 on the 22 line)

    Type stripping checks nothing and rejects syntax that needs rewriting, such as enum (Node 22 and 24 accept it with --experimental-transform-types, a flag Node 26 removed). Through Node 26, even versions become LTS; from Node 27 there is one major a year and every one becomes LTS.

  9. How do you scale a Node.js application horizontally?

    Run several identical, stateless instances behind a load balancer, with shared state outside the process: sessions and rate limits in Redis, uploads in object storage, cron on a queue or one elected leader, WebSocket broadcasts through pub/sub. Use every core first, then add machines. The test: you can kill any instance and users see at most one retried request.

  10. A payment provider sends the same webhook twice. How do you make the handler safe?

    Make it idempotent: webhooks are delivered at least once, so record each event id and skip ids already seen.

    JavaScript
    const processed = new Set();
    
    function handlePaymentWebhook(event) {
      if (processed.has(event.id)) return `ignored duplicate ${event.id}`;
      processed.add(event.id);
      return `marked order ${event.orderId} as paid`;
    }
    
    const deliveries = [
      { id: 'evt_1', orderId: 101 },
      { id: 'evt_2', orderId: 102 },
      { id: 'evt_1', orderId: 101 }, // the provider retried the first one
    ];
    for (const event of deliveries) console.log(handlePaymentWebhook(event));

    In production the Set is a unique constraint in the database, written in the same transaction as the side effect. Answer a duplicate with 200, or the provider keeps retrying.

    Practice it: Contains Duplicate

Coddy programming languages illustration

Learn to code with Coddy

Get started