skip to content

Some JavaScript teams commit their entire node_modules folder to git instead of relying on npm install at build time. What does this buy them, and why has it fallen out of favor as a default practice?

level: middleimportance: nice to knowfreq 30%

answer

  1. pre-lockfile era motivation
  2. left-pad 2016
  3. native binary addon platform mismatch
  4. npm ci vs npm install
  5. package-lock.json + private mirror replaced it

basics

~20 s

Committing node_modules means the exact installed dependency files travel with the repo, so a fresh checkout runs immediately without needing to reinstall anything — but it makes the repo huge and hides security problems from normal tooling.

solid answer

~50 s

Committing node_modules is a full-vendoring approach specific to Node/npm: instead of running npm install (which fetches packages from the registry, or a lockfile-pinned equivalent) as a build step, the exact installed files are checked into git directly. It guarantees a clean checkout is immediately runnable with zero network dependency and zero risk of the registry serving something different than what was originally installed. The downsides are what push most teams away from it as a default: repository size balloons (node_modules trees routinely reach hundreds of megabytes with deeply nested transitive dependencies), diffs on any dependency change become enormous and unreviewable, native binary addons compiled for one OS/architecture can be wrong for another developer's machine, and — most importantly — standard vulnerability scanners that read package.json/package-lock.json don't reliably track a committed node_modules tree, creating a security blind spot. A package-lock.json with registry-level integrity hashes, plus a private registry mirror for availability, gets most of the same guarantees without these costs, which is why it's now the standard default.

go deeper

for a junior

Should recognize this as 'copying installed packages into git' and give one basic pro (works offline) or con (huge repo).

for a middle

Should name the native-binary cross-platform problem and the vulnerability-scanning blind spot as concrete costs.

for a senior

Should place this in historical context (pre-lockfile npm) and explain why package-lock.json plus npm ci made it largely unnecessary.

for a principal

Should connect this specific practice to the general vendoring cost/benefit framework and articulate the remaining narrow cases (true offline/air-gapped pipelines) where it's still the right call.

## What committing node_modules means Committing `node_modules` to version control is the JavaScript ecosystem's most literal expression of full **vendoring**: rather than treating `npm install` (or `pnpm install/yarn install`) as a required build step that fetches every dependency's files from the npm registry, a team runs it once, gets the resulting folder tree of installed packages, and checks that folder directly into git alongside the application source. From that point on, anyone who clones the repository already has every dependency's exact files on disk — no install step needed, no network call, and no possibility that a registry serves a different artifact today than it did when the folder was originally generated. This was a genuinely popular practice in parts of the Node ecosystem in the mid-2010s, partly because npm's early versions (before `package-lock.json` existed, introduced in npm 5 in 2017) offered weaker reproducibility guarantees than modern tooling does — without a lockfile, `npm install` could resolve slightly different transitive versions on two different machines or two different days, so committing the literal installed output was, for a while, one of the few ways to guarantee everyone on a team and in CI was running byte-identical dependency code. ## The benefits The benefits mirror general vendoring benefits but are sharpened by specifics of the npm ecosystem. - `node_modules` trees are notoriously **deep and numerous** — a moderately complex frontend app can easily pull in many hundreds of transitive packages. - The npm registry, being a single, heavily-used, publicly writable service, has had real **availability incidents** and high-profile **trust incidents** (the **left-pad** unpublish event in 2016 being the canonical example, where an author unpublished an 11-line utility package that thousands of projects depended on transitively, instantly breaking builds across the ecosystem). Teams burned by an outage or an unpublish event at a bad moment — mid-deploy, mid-CI-run — sometimes reached for committing `node_modules` specifically as insurance against that class of failure, since a checked-in folder can never be unpublished out from under you. ## The costs The costs, though, are what pushed this from common practice to a discouraged one for most teams today. 1. **Repository size** is the most visible: `node_modules` trees routinely reach hundreds of megabytes to low gigabytes for larger applications, and committing that means every clone, every CI checkout, and every fetch operation gets proportionally slower, with git's storage model compounding this over time as the tree changes across many commits. 2. **Diff review** is the second major cost: a single `npm install` after a dependency bump can rewrite tens of thousands of lines across hundreds of files, which no human reviewer will meaningfully read, so code review of dependency changes becomes theater rather than substance — exactly the opposite of the audit benefit vendoring is supposed to provide. 3. Third, and specific to Node, is the **native-binary-addon problem**: some npm packages (`bcrypt`, `sharp`, various database drivers) ship or compile native code specific to the installing machine's OS and CPU architecture; a `node_modules` tree committed on a Mac developer's laptop can contain binaries that are simply wrong — and fail to load — on a Linux CI runner or production container, a footgun unique to this style of vendoring that a fresh `npm install` on the target platform avoids entirely. 4. Fourth, and the one with the most serious long-term consequence, is the same **vulnerability-scanning blind spot** general vendoring creates: tools like `npm audit`, Dependabot, and Snyk are built to read `package.json` and `package-lock.json` and check declared versions against CVE databases; a committed `node_modules` tree isn't the thing those tools primarily reason about, so a team relying on it as their source of truth can drift out of sync with what their manifest says and lose automated coverage of known vulnerabilities in their actual shipped dependency code. ## Why it fell out of favor The practice fell out of favor specifically because npm's tooling closed the reproducibility gap that originally motivated it: `package-lock.json` (and equivalent lockfiles in Yarn and pnpm) now records the exact resolved version and a cryptographic integrity hash for every package in the tree, so `npm ci` (the CI-oriented install command, stricter than `npm install`) reproduces byte-identical results from the lockfile alone, without needing the tree itself committed. Pairing that lockfile with a private registry mirror or cache (Verdaccio, Artifactory) gets teams the availability and reproducibility guarantees that motivated committing `node_modules` in the first place, without the repo bloat, unreviewable diffs, cross-platform binary problems, or the security-scanning blind spot — which is why `package-lock.json` plus .gitignore-ing `node_modules` is now the near-universal default, and committing the folder itself is reserved for edge cases like genuinely offline/air-gapped deployment pipelines.

  • How did the introduction of package-lock.json in npm 5 change the case for committing node_modules?
    Before package-lock.json, npm install had no guaranteed way to resolve identical transitive dependency versions across machines or over time, so committing the actual installed output was one of the few ways to guarantee reproducibility. Once package-lock.json recorded exact versions and integrity hashes, npm ci could reproduce the same tree deterministically from the lockfile alone, removing the main reproducibility argument for committing the folder itself.
  • Why can a node_modules folder committed on a developer's Mac break a Linux CI build?
    Some npm packages include native addons — compiled binary code specific to an operating system and CPU architecture — generated during install. A tree installed and committed on macOS can contain Mac-specific compiled binaries that simply won't load on a Linux CI runner, whereas running npm install fresh on the target platform compiles or fetches the correct binary for that platform.
  • What's the practical alternative that gives similar guarantees without committing the folder?
    A package-lock.json with integrity hashes, combined with a private registry proxy/mirror the organization controls, reproduces the exact same dependency versions deterministically and keeps builds available even if the public registry has an outage or unpublish event — without the repo size, diff-review, cross-platform, and vulnerability-scanning costs of committing node_modules directly.

It's like photocopying every book in a library and stuffing the copies into your own garage instead of getting a library card with guaranteed same-day holds — you're covered if the library burns down, but now your garage is packed with copies you never sort through for recalls, and some of the photocopies (compiled for a different machine) don't even work when you try to use them elsewhere.

saying these in an interview costs you the question

  • Doesn't know package-lock.json exists or what it solved
  • Unaware of native binary addon cross-platform breakage
  • Thinks committing node_modules has no real downside
  • Can't name why it was ever common practice historically
  • Confuses this with normal vendoring of a single library

context