Versions and dependencies

Separate dependency roles

Place runtime, development, optional, and peer dependencies according to who must install and control them.

npm groups dependencies by who needs them at install time. Putting a package in the wrong section hides problems until a consumer installs your library in production.

The four sections that matter

dependencies: code your published package imports at runtime. If dist/index.js does import x from 'unicode-properties', that package belongs here.

devDependencies: tools and test helpers used only while developing the package. TypeScript, test runners, and bundlers live here. They are not installed when someone runs npm install @acme/slugify-title in their app.

peerDependencies: packages the host app must provide. Plugin libraries use peers so you do not ship a second copy of React, Express, or another framework inside your package.

optionalDependencies: rare. Use them when the package can run without an optional native addon and you want install to continue if that addon fails to build.

Audit with a production install

From a clean directory:

npm install @acme/slugify-title --omit=dev
npm ls --all

You should see @acme/slugify-title and its runtime tree. You should not see vitest or typescript.

If a runtime import is missing after --omit=dev, you filed a devDependency that belongs in dependencies. The fix is to move it, not to tell consumers to install your dev tools.

Peer dependency example

Imagine @acme/slugify-title-express middleware that expects Express 4:

{
  "peerDependencies": {
    "express": "^4.21.0"
  }
}

npm warns when the host app lacks a matching peer. That warning is useful. It means the app owner controls the framework version, not your package.

One mistake to avoid

Moving a package to devDependencies just to silence an install warning, while your published code still imports it, creates broken releases. Trace every import from the packed dist/ files, then assign the section from that evidence.

Lesson completed