Menu
CoddyTech

Node.js Coding Interview Questions

Small utilities and log processing problems from Node.js rounds; every solution runs here.

10 questions
  1. Implement a simple EventEmitter with on, off, once and emit.

    Keep a Map from event name to an array of listeners.

    JavaScript
    class MiniEmitter {
      #listeners = new Map();
    
      on(event, fn) {
        if (!this.#listeners.has(event)) this.#listeners.set(event, []);
        this.#listeners.get(event).push(fn);
        return this;
      }
    
      off(event, fn) {
        const list = this.#listeners.get(event) ?? [];
        this.#listeners.set(event, list.filter((l) => l !== fn && l.original !== fn));
        return this;
      }
    
      once(event, fn) {
        const wrapper = (...args) => {
          this.off(event, wrapper);
          fn.apply(this, args);
        };
        wrapper.original = fn;
        return this.on(event, wrapper);
      }
    
      emit(event, ...args) {
        const list = this.#listeners.get(event);
        if (!list || list.length === 0) {
          if (event === 'error') throw args[0];
          return false;
        }
        for (const fn of [...list]) fn.apply(this, args);
        return true;
      }
    }
    
    const e = new MiniEmitter();
    const greet = (name) => console.log('hello', name);
    e.on('join', greet);
    e.once('join', (name) => console.log('first join was', name));
    e.emit('join', 'Ada');
    e.emit('join', 'Linus');
    e.off('join', greet);
    console.log(e.emit('join', 'nobody'));

    Details that get checked: emit loops over a copy, so a once listener removing itself does not skip the next one, and off finds once wrappers through original.

  2. Write your own promisify.

    Return a function that creates a promise and calls the original with an extra error-first callback that settles it.

    JavaScript
    function promisify(fn) {
      return function (...args) {
        return new Promise((resolve, reject) => {
          fn.call(this, ...args, (err, value) => {
            if (err) reject(err);
            else resolve(value);
          });
        });
      };
    }
    
    function readConfig(name, callback) {
      setTimeout(() => {
        if (name !== 'app') return callback(new Error(`no config named ${name}`));
        callback(null, { port: 8080 });
      }, 1);
    }
    
    const readConfigAsync = promisify(readConfig);
    readConfigAsync('app').then((cfg) => console.log('port', cfg.port));
    readConfigAsync('db').catch((err) => console.log(err.message));

    A regular function, not an arrow, keeps this for methods.

  3. Run async tasks with at most N in flight at once (a promise pool).

    Start limit runners; each takes the next index from a shared counter until none are left.

    JavaScript
    async function mapLimit(items, limit, worker) {
      const results = new Array(items.length);
      let next = 0;
    
      async function runner() {
        while (next < items.length) {
          const i = next++;
          results[i] = await worker(items[i], i);
        }
      }
    
      const runners = Array.from({ length: Math.min(limit, items.length) }, runner);
      await Promise.all(runners);
      return results;
    }
    
    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    let active = 0;
    let peak = 0;
    
    async function fetchPage(id) {
      active++;
      peak = Math.max(peak, active);
      await sleep(10 + (id % 3) * 5);
      active--;
      return `page ${id}`;
    }
    
    mapLimit([1, 2, 3, 4, 5, 6, 7], 3, fetchPage).then((pages) => {
      console.log(pages.join(', '));
      console.log('max at once:', peak);
    });

    next++ needs no lock, because JavaScript runs one callback at a time, and storing results by index keeps the input order.

  4. Write a function that retries a failing async call with exponential backoff.

    Loop over attempts, await the call inside try, and on failure wait base * 2^attempt.

    JavaScript
    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    
    async function retry(fn, { retries = 3, baseMs = 10 } = {}) {
      for (let attempt = 0; ; attempt++) {
        try {
          return await fn(attempt);
        } catch (err) {
          if (attempt >= retries) throw err;
          const delay = baseMs * 2 ** attempt;
          console.log(`attempt ${attempt + 1} failed (${err.message}), retrying in ${delay} ms`);
          await sleep(delay);
        }
      }
    }
    
    let calls = 0;
    async function flakyRequest() {
      calls++;
      if (calls < 3) throw new Error('ECONNRESET');
      return `ok after ${calls} calls`;
    }
    
    retry(flakyRequest).then(console.log);

    Add random jitter, retry only transient errors (ECONNRESET, 503, 429), and never retry a payment without an idempotency key.

  5. Implement an LRU cache in JavaScript.

    A Map keeps insertion order, so if every read moves its key to the end, the first key is the least recently used.

    JavaScript
    class LRUCache {
      constructor(capacity) {
        this.capacity = capacity;
        this.map = new Map();
      }
    
      get(key) {
        if (!this.map.has(key)) return undefined;
        const value = this.map.get(key);
        this.map.delete(key); // move to the newest position
        this.map.set(key, value);
        return value;
      }
    
      set(key, value) {
        if (this.map.has(key)) this.map.delete(key);
        this.map.set(key, value);
        if (this.map.size > this.capacity) {
          const oldest = this.map.keys().next().value;
          this.map.delete(oldest);
        }
      }
    }
    
    const cache = new LRUCache(2);
    cache.set('a', 1);
    cache.set('b', 2);
    cache.get('a');    // a is now the most recent
    cache.set('c', 3); // evicts b, the least recently used
    console.log([...cache.map.keys()].join(','), cache.get('b'));

    Both operations are O(1). The classic version is a hash map plus a doubly linked list.

  6. Find the most frequent words in a large text stream.

    Count with a Map while streaming, and carry over a word cut in half at a chunk boundary.

    JavaScript
    const { Readable } = require('node:stream');
    
    async function topWords(stream, k) {
      const counts = new Map();
      const add = (w) => counts.set(w, (counts.get(w) ?? 0) + 1);
      let leftover = '';
      for await (const chunk of stream) {
        const words = (leftover + chunk.toString()).toLowerCase().split(/\s+/);
        leftover = words.pop(); // the last word may continue in the next chunk
        for (const w of words) if (w) add(w);
      }
      if (leftover) add(leftover);
      return [...counts]
        .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
        .slice(0, k);
    }
    
    // the chunks split "dog" on purpose, like a real stream can
    const source = Readable.from(['the cat and the d', 'og and the bird ', 'saw the cat']);
    topWords(source, 3).then((top) => {
      for (const [word, n] of top) console.log(word, n);
    });

    Without leftover, d and og count as two words. For a large vocabulary and small k, use a min-heap of size k: O(n log k).

    Practice it: Top K Frequent Elements, Kth Largest Element in an Array

  7. Given packages and their dependencies, print an install order and detect cycles.

    A topological sort. Kahn's algorithm: start with packages that have no unmet dependencies and release each package once everything it needs is installed.

    JavaScript
    function installOrder(deps) {
      const pending = new Map();    // package -> number of unmet dependencies
      const dependents = new Map(); // package -> packages that need it
      for (const [pkg, needs] of Object.entries(deps)) {
        pending.set(pkg, (pending.get(pkg) ?? 0) + needs.length);
        for (const d of needs) {
          if (!pending.has(d)) pending.set(d, 0);
          if (!dependents.has(d)) dependents.set(d, []);
          dependents.get(d).push(pkg);
        }
      }
      const ready = [...pending].filter(([, n]) => n === 0).map(([pkg]) => pkg);
      const order = [];
      while (ready.length) {
        const pkg = ready.shift();
        order.push(pkg);
        for (const next of dependents.get(pkg) ?? []) {
          pending.set(next, pending.get(next) - 1);
          if (pending.get(next) === 0) ready.push(next);
        }
      }
      if (order.length !== pending.size) throw new Error('dependency cycle');
      return order;
    }
    
    console.log(installOrder({
      app: ['express', 'pg'],
      express: ['body-parser'],
      'body-parser': [],
      pg: [],
    }).join(' > '));
    
    try {
      installOrder({ a: ['b'], b: ['a'] });
    } catch (err) {
      console.log(err.message);
    }

    O(V + E); leftover packages mean a cycle. See the topological sort visualization.

    Practice it: Course Schedule, Alien Dictionary

  8. From request logs with start and end times, find the peak number of concurrent requests.

    Sweep the timeline: +1 at each start, -1 at each end, sorted, with a running total.

    JavaScript
    function peakConcurrency(requests) {
      const events = [];
      for (const { start, end } of requests) {
        events.push([start, 1]);
        events.push([end, -1]);
      }
      // at equal times, ends come before starts: a request ending at 30
      // does not overlap one starting at 30
      events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
      let current = 0;
      let peak = 0;
      for (const [, change] of events) {
        current += change;
        peak = Math.max(peak, current);
      }
      return peak;
    }
    
    const log = [
      { start: 0, end: 30 },
      { start: 5, end: 10 },
      { start: 15, end: 20 },
      { start: 8, end: 18 },
      { start: 30, end: 40 },
    ];
    console.log(peakConcurrency(log));

    O(n log n). It is the meeting rooms problem, and it sizes a connection pool from real traffic.

    Practice it: Meeting Rooms II, Merge Intervals

  9. Merge two log files that are each sorted by timestamp into one sorted log.

    Two pointers: always take the earlier of the two current lines.

    JavaScript
    function mergeLogs(a, b) {
      const out = [];
      let i = 0;
      let j = 0;
      while (i < a.length && j < b.length) {
        out.push(a[i].ts <= b[j].ts ? a[i++] : b[j++]);
      }
      while (i < a.length) out.push(a[i++]);
      while (j < b.length) out.push(b[j++]);
      return out;
    }
    
    const api = [
      { ts: 1, msg: 'api start' },
      { ts: 4, msg: 'api request' },
      { ts: 9, msg: 'api stop' },
    ];
    const db = [
      { ts: 2, msg: 'db connect' },
      { ts: 4, msg: 'db query' },
      { ts: 7, msg: 'db slow query' },
    ];
    for (const line of mergeLogs(api, db)) console.log(line.ts, line.msg);

    <= keeps the merge stable at ties; O(n + m). For k files, keep each file's current line in a min-heap.

    Practice it: Merge Sorted Array, Merge k Sorted Lists

  10. Implement a rate limiter that allows N requests per time window for each client.

    A sliding window log: keep each client's recent timestamps, drop those older than the window, and allow the request only if fewer than N remain.

    JavaScript
    class RateLimiter {
      constructor(limit, windowMs) {
        this.limit = limit;
        this.windowMs = windowMs;
        this.hits = new Map(); // key -> timestamps inside the window
      }
    
      allow(key, now = Date.now()) {
        const recent = (this.hits.get(key) ?? []).filter((t) => now - t < this.windowMs);
        const allowed = recent.length < this.limit;
        if (allowed) recent.push(now);
        this.hits.set(key, recent);
        return allowed;
      }
    }
    
    const limiter = new RateLimiter(3, 1000); // 3 requests per second per client
    const times = [0, 100, 200, 300, 1050, 1150];
    console.log(times.map((t) => limiter.allow('1.2.3.4', t)).join(' '));
    console.log(limiter.allow('5.6.7.8', 300));

    The fourth request inside one second is refused; by 1050 ms the first has left the window. The clock is a parameter, so tests need no waiting. A token bucket uses O(1) memory per client, and with several instances the counts move to Redis. Reply 429 with Retry-After.

Coddy programming languages illustration

Learn to code with Coddy

Get started