intermediate

Module resolution

Resolve relative paths, bare specifiers, node_modules traversal, built-ins, conditions, TypeScript paths, and bundler gaps.

Node resolves specifiers differently for ESM and CommonJS, but the mental model is consistent: relative paths (`./`), bare specifiers (`lodash`), and built-ins (`node:fs`).

Bare specifier algorithm (simplified):

  1. If `node:` prefix or core module — built-in.
  2. Walk `node_modules` upward from importing file.
  3. Read `package.json` `exports` / `main` / `module`.
  4. File extensions: `.js`, `.json`, `.node`; ESM may require full extensions.
					import fs from 'node:fs';           // explicit built-in
import { z } from 'zod';            // node_modules/zod
import helper from './helper.js';   // ESM often needs extension
				

TypeScript `paths` aliases work at compile time — Node does not understand them unless a loader or bundler rewrites imports. Same gap exists between Jest/Vitest resolution and production runtime.

On interviews: trace how `import 'express'` finds a file; explain `node_modules` hoisting; name bundler vs Node differences.

Common pitfalls: missing `.js` extension in ESM source; relying on TS paths in Node without tsx/ts-node; deep imports into package internals that break on upgrade.

The trade-off is balancing simplicity, performance, safety, and operability — name which axis you optimized and what cost you accepted.

Checklist:

  • Prefer `node:` prefix for built-ins.
  • Understand upward `node_modules` walk.
  • Align runtime, bundler, and test resolver.
  • Avoid unsupported deep imports.