npm dependencies and devDependencies

By

Learn the difference between npm dependencies and devDependencies, what gets installed for users of your package, and how to pick, move and omit them.

~~~

When you install an npm package using npm install <package-name>, you are installing it as a dependency.

The package is automatically listed in the package.json file, under the dependencies list (as of npm 5: before you had to manually specify --save).

When you add the -D flag, or --save-dev, you are installing it as a development dependency, which adds it to the devDependencies list.

Development dependencies are intended as development-only packages, that are unneeded in production. For example testing packages, webpack or Babel.

Here’s what a package.json with both lists looks like:

{
  "name": "slugify-title",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "test": "vitest run",
    "lint": "eslint ."
  },
  "dependencies": {
    "slugify": "^1.6.9"
  },
  "devDependencies": {
    "@types/node": "^26.0.0",
    "eslint": "^10.0.0",
    "typescript": "^7.0.0",
    "vitest": "^5.0.0"
  }
}

Our code imports slugify when it runs, so it goes in dependencies. TypeScript, the type definitions, the test runner and the linter only run while we work on the project, so they go in devDependencies.

What gets installed when someone uses your package

This is the most important rule, and it’s the reason the two lists exist.

When someone installs your package from the npm registry, npm installs its dependencies, but never its devDependencies. Dev dependencies are only installed when you run npm install in the root of the project itself, like when you clone the repository to work on it.

If someone runs npm install slugify-title, they get slugify-title and slugify. They don’t get TypeScript, ESLint or Vitest, which they don’t need to use the package.

This matters a lot when you publish a library. If your code imports a package at runtime and you put it in devDependencies by mistake, everything works on your machine because it’s installed there. Then a user installs your package and gets an error like Cannot find package 'slugify' the first time your code runs.

The opposite mistake is less dramatic but still annoying. Put TypeScript in dependencies and every user of your package downloads the whole compiler for nothing.

How to decide where a package goes

Ask one question: does my code import or require() this package when it runs?

If yes, it’s a dependency. Think of Express in a server, a date library, a database driver, or React in a component library’s runtime code.

If it’s only used to build, test, check or format the code, it’s a dev dependency. Typical examples:

The frontend gotcha

For a frontend app that gets bundled at build time, the split is mostly about organisation.

The bundler copies the code you import into the final JavaScript files. So React ends up in the bundle whether you list it in dependencies or devDependencies, and nobody installs your app from npm anyway.

The place where it bites is the build step. Say your CI or hosting platform runs this before building:

npm ci --omit=dev
npm run build

If Vite or TypeScript are in devDependencies, they’re not installed, and the build fails with something like vite: not found. The same happens when NODE_ENV=production is set before the install, because npm then omits dev dependencies by default.

The fix is to install everything for the build step, and omit dev dependencies only where the code actually runs, like the server that runs your Node app.

Moving a package between lists

If a package ended up in the wrong list, install it again with the right flag. npm moves the entry for you.

To move a package to devDependencies:

npm install typescript --save-dev

To move it to dependencies, use --save-prod (or -P):

npm install slugify --save-prod

Other dependency lists

There are two more lists you’ll find in some package.json files. peerDependencies say “this package works with a version of X that you install yourself”, which is common for plugins (I wrote about them in npm peer dependencies). optionalDependencies are packages npm tries to install, but the install carries on if they fail.

When you go in production, if you type npm install and the folder contains a package.json file, they are installed, as npm assumes this is a development deploy.

To skip those development dependencies, use the --omit=dev flag (available since npm 7):

npm install --omit=dev

You might still see npm install --production in older tutorials. It still works, but it’s deprecated. npm 11 (the version that ships with Node 24 LTS) and npm 12 print a warning when you use it:

npm warn config production Use `--omit=dev` instead.

If you deploy from a package-lock.json file, for example on a server or in CI, use npm ci --omit=dev. It installs the exact versions from the lockfile and skips the dev dependencies.

Also, when the NODE_ENV environment variable is set to production, npm omits dev dependencies by default, even without the flag. More on that in the difference between development and production.

Tagged: Node.js · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about node: