| |

npm 05 ๐ŸŒ Installing Packages (dependencies vs devDependencies)

โฑ๏ธ Estimated read: 12 min | |

Every package you install with npm lands in one of two places in your package.json: dependencies or devDependencies. The distinction is not cosmetic. It determines whether the package is installed when you build for production, whether it is shipped in your deployment artifact, and whether your production server can actually run the code after deployment. The npm documentation defines dependencies as “packages required by your application in production” and devDependencies as “packages that are only needed for local development and testing” . Getting the classification wrong is one of the most common causes of production failures, and the failure mode is often a cryptic Cannot find module error that appears only after deployment.

The default behavior of npm install <package-name> is to save the package to dependencies. This has been the case since npm 5; before that, you had to pass --save explicitly . To save to devDependencies, you pass --save-dev or its shorthand -D . The choice is not about whether the package is “important” or “small” or “only used in tests.” It is about one question: will the production runtime need this package to execute the application’s code? If yes, it belongs in dependencies. If no, it belongs in devDependencies.

Key point: npm install <pkg> saves to dependencies; npm install -D <pkg> saves to devDependencies. Production installs with --omit=dev (or the legacy --production flag) skip devDependencies entirely. Build tools, test runners, linters, and TypeScript compilers belong in devDependencies. Runtime libraries that the deployed code imports belong in dependencies.


Why the separation exists

The production install problem. A production deployment does not need Jest, ESLint, or the TypeScript compiler. Installing them in the production environment wastes disk space, slows down the container build, and expands the attack surface with packages that are never executed. The devDependencies field is the declaration that a package is safe to omit in production. npm install --omit=dev reads the manifest and skips every package in that field, while still resolving it in the lockfile . The production image contains only the packages the application actually needs to run.

The accidental omission problem. The separation has a failure mode that is the mirror image of the waste problem. If a package that the runtime actually imports is placed in devDependencies by mistake, the production install skips it, and the application crashes with Cannot find module on the first request that touches that code path. The package was present in development because the developer ran a full npm install, so the bug does not appear until deployment. The rule is to ask whether the code that runs in production imports the package. If it does, it is a dependency, regardless of how trivial or utility-like it seems.

The environment variable problem. NODE_ENV=production has the same effect as --omit=dev: it tells npm to skip devDependencies . This is convenient for deployments that set NODE_ENV for other reasons, but it creates a trap in CI/CD pipelines. A CI environment that sets NODE_ENV=production and then runs npm install for a build step will silently skip the build toolchain (TypeScript, Vite, esbuild) that lives in devDependencies, and the build fails with tsc: command not found . The fix is to pass --include=dev explicitly when the build step needs the toolchain, or to ensure the build stage of a multi-stage Dockerfile does not inherit NODE_ENV=production .

The peer dependency distinction. A third category, peerDependencies, exists for packages that a plugin expects the host application to provide. A React component library declares react as a peer dependency so that the consuming application installs one copy of React, rather than the library bundling its own . Peer dependencies are installed automatically in modern npm if they are missing, but they are not the same as dependencies: the library does not own the package, it only requires that the host provides it. This chapter focuses on the dependencies versus devDependencies split, but the peer distinction is the next layer of the dependency model.


a. The install commands and their flags

The command you run determines the field the package is saved to. The default is dependencies, and the -D flag is the switch to devDependencies.

# Install a runtime dependency
# The package is saved to "dependencies" in package.json
npm install express

# Install a development dependency
# The -D flag (or --save-dev) saves to "devDependencies"
npm install -D jest

# Install a specific version as a runtime dependency
npm install express@4.18.2

# Install a package without saving it to package.json
# The --no-save flag is for one-off testing
npm install --no-save some-package

# Install all dependencies (including dev) from package.json
npm install

# Install only production dependencies
# --omit=dev is the modern form; --production is the legacy alias
npm install --omit=dev

The npm install with no arguments installs both fields from package.json. The --omit=dev flag is the production form, and it is the command that a deployment pipeline should use when it needs only the runtime packages . The --production flag is the older name and still works, but the npm documentation describes --omit=dev as the clearer, modern form.

The --include=dev flag is the override for the case where NODE_ENV=production or a config file is causing npm to skip the development dependencies. It forces the install to include them regardless of the environment . This is the flag to use in a build stage that needs the toolchain even though the environment says production.

# Force devDependencies even when NODE_ENV=production
# The --include=dev flag overrides the environment
npm install --include=dev

The npm ci command is the lockfile-driven install. It installs exactly what the lockfile specifies, fails if the lockfile and package.json disagree, and deletes node_modules first. It respects --omit=dev and NODE_ENV=production in the same way as npm install . The combination npm ci --omit=dev is the production deployment command.


b. The classification decision

The decision about which field a package belongs in is determined by what imports it. A package that is imported by code that runs in production is a dependency. A package that is only imported by test files, build configuration, or development scripts is a devDependency.

// The runtime code imports express
// express is a dependency
import express from 'express'
// The test file imports jest
// jest is a devDependency
import { describe, it } from '@jest/globals'

The build toolchain is the category that causes the most confusion. TypeScript, Babel, Vite, Webpack, and esbuild are devDependencies because the production runtime executes the compiled output, not the compiler. The tsc binary is never invoked in production. The same applies to ESLint and Prettier: they run on the developer’s machine or in CI, not in the deployed application .

The @types/* packages are also devDependencies. They provide TypeScript type definitions for libraries that are written in JavaScript. The type definitions are erased at compile time and are not part of the runtime bundle, so they are not needed in production .

The package that is sometimes misclassified is a library that is imported by the runtime code but seems like a “utility.” For example, a date formatting library that the application uses in its API response is a dependency, even if it is small and simple. The question is not what the package does; it is whether the production code imports it.


c. The production build and the verification

The production build should use --omit=dev or npm ci --omit=dev, and the result should be verified by checking which packages are installed.

# The production install
# The --omit=dev flag skips devDependencies
npm ci --omit=dev

# Verify that only production dependencies are installed
# The --omit=dev flag on npm ls lists only the production tree
npm ls --omit=dev --depth=0

The npm ls --omit=dev --depth=0 command is the verification step. It lists the top-level packages that are installed in the production tree. If a package that the application needs at runtime is missing from the list, it is in devDependencies and was skipped. The fix is to move it to dependencies with npm install <pkg> and re-run the production install.

The Docker multi-stage build is the pattern that separates the build toolchain from the production image. The build stage installs all dependencies, including devDependencies, and runs the build. The runtime stage copies only the compiled output and the production node_modules . The --omit=dev flag in the runtime stage is what keeps the image small.

# The build stage installs all dependencies
FROM node:22-slim AS build
RUN npm ci
RUN npm run build

# The runtime stage installs only production dependencies
FROM node:22-slim AS runtime
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist

The npm ci in the build stage installs the devDependencies, which is correct because the build needs the compiler. The npm ci --omit=dev in the runtime stage installs only the production packages, which is correct because the compiled output does not need the compiler.


Complete Example Session

The following session installs a runtime dependency and a development dependency, verifies the classification, and performs a production install.

# ============================================
# PART 1: INSTALL A RUNTIME DEPENDENCY
# ============================================
# The default behavior saves to dependencies
npm install express

# The package.json now lists express in dependencies
grep -A2 '"dependencies"' package.json
# "dependencies": {
#   "express": "^4.18.2"
# }

# ============================================
# PART 2: INSTALL A DEVELOPMENT DEPENDENCY
# ============================================
# The -D flag saves to devDependencies
npm install -D jest

# The package.json now lists jest in devDependencies
grep -A2 '"devDependencies"' package.json
# "devDependencies": {
#   "jest": "^29.7.0"
# }

# ============================================
# PART 3: VERIFY THE INSTALLED TREE
# ============================================
# The full install shows both
npm ls --depth=0

# ============================================
# PART 4: PRODUCTION INSTALL
# ============================================
# Remove node_modules and reinstall with --omit=dev
rm -rf node_modules
npm ci --omit=dev

# Verify that only express is installed
npm ls --omit=dev --depth=0
# Output shows express but not jest

# ============================================
# PART 5: THE MISCLASSIFICATION FAILURE
# ============================================
# If a runtime package is in devDependencies, it is missing here
node -e "require('jest')"
# This works because jest is in the full install
# But in production, the same require would fail

The five parts cover the runtime install, the development install, the full tree verification, the production install, and the misclassification failure. What the session demonstrates is that the flag on the install command determines the field, and the production install respects the field.


Quick Reference

Install Commands

CommandSaves ToInstalled in Production
npm install pkgdependenciesYes
npm install -D pkgdevDependenciesNo
npm installBothYes (unless omitted)
npm install --omit=devโ€”Only dependencies
npm ci --omit=devโ€”Only dependencies

Classification Rules

Package TypeFieldReason
Runtime librarydependenciesImported by production code
Test runnerdevDependenciesOnly used in tests
Compiler (tsc)devDependenciesCompiled output runs in production
LinterdevDependenciesDevelopment-time only
Type definitionsdevDependenciesErased at compile time

Environment Overrides

SettingEffect
NODE_ENV=productionSkips devDependencies
--omit=devSkips devDependencies
--include=devForces devDependencies

Verification

CommandShows
npm ls --depth=0All top-level packages
npm ls --omit=dev --depth=0Production packages only

Best Practices

โœ… Do This:

# Runtime library
npm install express

# Build tool
npm install -D typescript

# Production install
npm ci --omit=dev

โŒ Don’t Do This:

# Runtime library in devDependencies
npm install -D express # missing in production

# Build tool in dependencies
npm install typescript # bloats production image

Common Pitfalls

PitfallWhy It HappensFix
Cannot find module in productionRuntime package in devDependenciesMove to dependencies
Production image too largeBuild tools in dependenciesMove to devDependencies
CI build fails with tsc: not foundNODE_ENV=production skips devDeps--include=dev
Dev dependency installed in productionUsed npm install without --omit=devUse --omit=dev

Real-World Examples

1. Runtime dependency

npm install express

2. Development dependency

npm install -D jest

3. Production install

npm ci --omit=dev

4. Force dev dependencies in CI

npm install --include=dev

5. Verify production tree

npm ls --omit=dev --depth=0

6. Install specific version

npm install express@4.18.2

7. Install without saving

npm install --no-save some-package

8. Docker build stage

RUN npm ci

9. Docker runtime stage

RUN npm ci --omit=dev

10. Check a package’s field

npm pkg get dependencies.devDependencies

Visual

The Two Fields

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  package.json                                โ”‚
โ”‚  โ”œโ”€โ”€ dependencies: express, axios            โ”‚
โ”‚  โ”‚   โ†’ installed in production               โ”‚
โ”‚  โ”‚   โ†’ imported by runtime code              โ”‚
โ”‚  โ”‚                                           โ”‚
โ”‚  โ””โ”€โ”€ devDependencies: jest, typescript       โ”‚
โ”‚      โ†’ skipped with --omit=dev               โ”‚
โ”‚      โ†’ only for development and build        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Production Install

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  npm ci --omit=dev                           โ”‚
โ”‚         โ”‚                                    โ”‚
โ”‚         โ–ผ                                    โ”‚
โ”‚  dependencies only                           โ”‚
โ”‚  devDependencies skipped                     โ”‚
โ”‚         โ”‚                                    โ”‚
โ”‚         โ–ผ                                    โ”‚
โ”‚  smaller image, smaller attack surface       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Misclassification Failure

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Runtime package in devDependencies          โ”‚
โ”‚         โ”‚                                    โ”‚
โ”‚         โ–ผ                                    โ”‚
โ”‚  Development: npm install (all)              โ”‚
โ”‚  โ†’ package present, code works               โ”‚
โ”‚         โ”‚                                    โ”‚
โ”‚         โ–ผ                                    โ”‚
โ”‚  Production: npm ci --omit=dev               โ”‚
โ”‚  โ†’ package skipped                           โ”‚
โ”‚  โ†’ Cannot find module                        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Runtime fielddependencies
Development fielddevDependencies
Default installdependencies
Dev install flag-D or --save-dev
Production flag--omit=dev
Force dev flag--include=dev
Environment overrideNODE_ENV=production
Verificationnpm ls --omit=dev --depth=0

Key takeaways:

  • The flag determines the field. npm install pkg saves to dependencies; npm install -D pkg saves to devDependencies .
  • The field determines production presence. --omit=dev and NODE_ENV=production skip devDependencies entirely .
  • The classification rule is import-based. If production code imports the package, it is a dependency. If only tests, build config, or development scripts import it, it is a devDependency .
  • Misclassification causes production failures. A runtime package in devDependencies is missing after deployment, and the error appears only at runtime.
  • The CI trap is NODE_ENV=production. A build step that needs the toolchain must pass --include=dev or avoid inheriting the production environment .
  • The multi-stage Dockerfile is the standard pattern. The build stage installs everything; the runtime stage uses --omit=dev .

Remember: The dependencies and devDependencies split is the line between what runs in production and what only runs on your machine. The install command determines the field, and the field determines whether the package survives the production install. Get the classification wrong in one direction and the production image bloats with compilers and test runners. Get it wrong in the other direction and the application crashes with a missing module on the first request. The question to ask is not “is this package important?” but “does the code that runs in production import this package?” The answer to that question is the field.



Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!