dependencies vs devDependencies — What Goes Where

Every npm package you install belongs in one of two categories: packages your application needs to run in production, and packages you only need during development. npm formalises this distinction with two separate fields in package.json: dependencies and devDependencies. Getting this right matters beyond just cleanliness — it directly affects your production deployment bundle size, your security attack surface, and how clearly your codebase communicates its architecture to other developers.

The Core Distinction

dependencies devDependencies
Needed at Runtime — server must have these to run Development time only — not needed to run the app
Install flag npm install pkg (no flag) npm install -D pkg
package.json key "dependencies" "devDependencies"
Installed in production? Yes — always No — skipped with npm install --production
Examples express, mongoose, jsonwebtoken, axios nodemon, eslint, jest, prettier, supertest
Note: On deployment platforms like Render, Railway, and Heroku, the build process runs npm install --omit=dev (equivalent to the older --production flag) by default. This skips all devDependencies and only installs what is listed under dependencies. If you accidentally place a runtime package under devDependencies, your app will crash in production with Cannot find module even though it works perfectly on your local machine.
Tip: When in doubt, ask: “Does the running Node.js process need to require() this package to serve requests?” If yes, it is a production dependency. If the package is only used by scripts you run before or after the server starts (building, testing, linting), it is a dev dependency.
Warning: Vite is a devDependency for the React client — it is only used during development and to build the production bundle. The built output in dist/ is plain HTML, CSS, and JavaScript with no reference to Vite at all. However, react and react-dom are listed under dependencies in the client’s package.json because they are bundled into the final output by Vite during build.

Classifying Your MERN Packages

Package Category Reason
express dependency Server requires it to handle HTTP requests at runtime
mongoose dependency Server requires it to connect to MongoDB at runtime
dotenv dependency Server calls require('dotenv').config() on startup
cors dependency Express middleware active at runtime
jsonwebtoken dependency Used to sign/verify tokens in auth routes at runtime
bcryptjs dependency Used to hash passwords in register/login routes at runtime
nodemailer dependency Used to send emails from Express routes at runtime
nodemon devDependency Watches files and restarts server — only needed in development
eslint devDependency Lints code during development — not needed in production
jest devDependency Test runner — tests do not run in production
supertest devDependency HTTP testing helper — used only in test scripts
prettier devDependency Code formatter — formats source files during development
vite (client) devDependency Dev server and build tool — not present at runtime
react (client) dependency Bundled into the final JS output by Vite
axios (client) dependency Bundled into the final JS output by Vite

Installing With the Correct Flag

# ── Server: production dependencies ──────────────────────────────────────────
cd server
npm install express mongoose dotenv cors helmet bcryptjs jsonwebtoken nodemailer multer

# ── Server: dev dependencies ──────────────────────────────────────────────────
npm install -D nodemon eslint jest supertest

# ── Client: production dependencies (bundled by Vite) ────────────────────────
cd ../client
npm install react react-dom react-router-dom axios

# ── Client: dev dependencies (build tools, linting) ──────────────────────────
npm install -D vite @vitejs/plugin-react eslint eslint-plugin-react

Production Install — What Gets Skipped

# On your local machine — installs everything including devDependencies
npm install

# In production / CI — skips devDependencies
npm install --omit=dev      # npm 7+  (modern)
npm install --production     # older syntax (same effect)

# Verify what would be installed in production
npm list --omit=dev --depth=0
Example: server/ node_modules in production vs development

Development install — includes:
  express, mongoose, dotenv, cors, bcryptjs, jsonwebtoken (production)
  nodemon, eslint, jest, supertest (dev — 4 extra packages, ~40MB)

Production install (--omit=dev) — includes only:
  express, mongoose, dotenv, cors, bcryptjs, jsonwebtoken
  → Smaller image, faster deploy, smaller security surface

Common Mistakes

Mistake 1 — Installing runtime packages as devDependencies

❌ Wrong — dotenv installed as a dev dependency:

npm install -D dotenv   # dotenv goes into devDependencies
Local:      Works — all deps installed
Production: CRASH — "Cannot find module 'dotenv'"
            process.env variables are never loaded → MongoDB URI is undefined

✅ Correct — dotenv is needed at runtime (server startup) so it belongs in production dependencies:

npm install dotenv   # no -D flag ✓

Mistake 2 — Installing build tools as production dependencies

❌ Wrong — nodemon in production dependencies bloats the deployment:

npm install nodemon   # goes into dependencies — deployed to production unnecessarily

✅ Correct — nodemon is a dev tool that only watches files during development:

npm install -D nodemon   # devDependencies — excluded from production deploy ✓

Mistake 3 — Using nodemon in production

❌ Wrong — package.json start script using nodemon:

"scripts": {
  "start": "nodemon index.js"   // nodemon is a devDependency — not available in production!
}

✅ Correct — use node for production, nodemon only for development:

"scripts": {
  "start": "node index.js",      // production — plain Node.js
  "dev":   "nodemon index.js"    // development — auto-restart on change
}

Quick Reference

Question Answer
Does the running server require() this package? dependency — no flag
Is this only used during development (test, lint, build)? devDependency — use -D
How to skip devDeps in production? npm install --omit=dev
Does Render/Heroku skip devDeps automatically? Yes — they run --omit=dev by default
Should nodemon be in start or dev script? dev script only

🧠 Test Yourself

Your Express app works perfectly locally but crashes in production with Error: Cannot find module 'jsonwebtoken'. What is the most likely cause?