Pin exact versions for all dependencies in production (no ^, ~, or * ranges). Unpinned dependencies silently pull in new versions that can introduce breaking changes, security vulnerabilities, or performance regressions — and you won't know until production breaks.
Why This Matters
Unpinned dependencies mean your production build is non-deterministic — the same code can produce different builds depending on when npm install runs. A patch release with a breaking change or a compromised package version gets pulled in silently. You discover the problem in production, not in CI, because the versions differ between environments.
When you specify "react": "^18.2.0", npm will install any version matching >=18.2.0 <19.0.0. This means your production build depends on when npm install runs — not on what you tested. A new patch release published between your last CI run and your production deploy will be silently included.
This has caused real-world incidents: the colors and faker packages were intentionally sabotaged by their maintainer, breaking thousands of production applications that had unpinned versions. The event-stream attack injected cryptocurrency-stealing code through a patch update that was automatically pulled into applications with ^ ranges.
Pinning exact versions ensures that your CI build, staging environment, and production deployment all use identical dependencies. Lockfiles help but are not sufficient — they can be regenerated, and not all tools respect them.
The rule
Pin exact versions for all production dependencies (dependencies in package.json). Use exact version strings (e.g., "18.2.0") without ^, ~, or * prefixes. Use a lockfile (package-lock.json or pnpm-lock.yaml) and commit it to version control.