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):
- If `node:` prefix or core module — built-in.
- Walk `node_modules` upward from importing file.
- Read `package.json` `exports` / `main` / `module`.
- 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.