npm Scripts — Automating Your MERN Workflow

The "scripts" field in package.json is one of the most powerful and underused features of npm. It lets you define short, memorable command aliases for the long shell commands you run every day — starting servers, running tests, building for production, linting code, and seeding databases. npm scripts run in the context of your project’s node_modules/.bin, which means you can use locally installed CLI tools (like nodemon, jest, and eslint) directly in scripts without installing them globally. In MERN development, a well-designed set of npm scripts becomes the standard interface everyone on the team uses to interact with the project.

How npm Scripts Work

# Running a script
npm run script-name

# Special scripts that have shorthand (no "run" needed)
npm start     # runs "start" script
npm test      # runs "test" script
npm stop      # runs "stop" script

# All other custom scripts need "run"
npm run dev
npm run lint
npm run build
npm run seed
Note: npm automatically adds node_modules/.bin to the PATH when running scripts. This means you can call locally installed CLI tools directly in scripts without their full path. For example, if nodemon is installed locally in your project, "dev": "nodemon index.js" works — you do not need ./node_modules/.bin/nodemon index.js or a global install.
Tip: Use the pre and post prefix hooks to automatically run scripts before or after another script. Define "prestart" to run before "start", or "posttest" to run after "test". For example: "prebuild": "npm run lint" automatically lints your code before every build — failing the build if lint errors exist.
Warning: npm scripts run in a shell — the exact shell depends on the operating system (sh on Unix, cmd.exe on Windows). Commands that work in bash (like &&, ||, environment variable syntax) may not work on Windows. For cross-platform scripts, use the cross-env package for environment variables and rimraf instead of rm -rf.

Server Scripts (server/package.json)

{
  "scripts": {
    "start":    "node index.js",
    "dev":      "nodemon index.js",
    "test":     "jest --runInBand --detectOpenHandles",
    "test:watch": "jest --watch",
    "lint":     "eslint src/ --ext .js",
    "lint:fix": "eslint src/ --ext .js --fix",
    "seed":     "node src/scripts/seedDatabase.js",
    "seed:clear": "node src/scripts/clearDatabase.js"
  }
}

Client Scripts (client/package.json)

{
  "scripts": {
    "dev":     "vite",
    "build":   "vite build",
    "preview": "vite preview",
    "lint":    "eslint . --ext js,jsx --report-unused-disable-directives",
    "lint:fix": "eslint . --ext js,jsx --fix"
  }
}

Root Scripts — Run Both Servers Together (root package.json)

{
  "scripts": {
    "dev":         "concurrently -n SERVER,CLIENT -c blue,green \"npm run server\" \"npm run client\"",
    "server":      "cd server && npm run dev",
    "client":      "cd client && npm run dev",
    "build":       "cd client && npm run build",
    "test":        "cd server && npm test",
    "lint":        "concurrently \"cd server && npm run lint\" \"cd client && npm run lint\"",
    "install:all": "npm install && cd server && npm install && cd ../client && npm install"
  },
  "devDependencies": {
    "concurrently": "^8.2.2"
  }
}

Practical Script Patterns

{
  "scripts": {
    "start":   "node index.js",
    "dev":     "nodemon index.js",

    "test":            "jest",
    "test:coverage":   "jest --coverage",
    "test:watch":      "jest --watch",

    "lint":    "eslint src/",
    "lint:fix": "eslint src/ --fix",

    "prebuild": "npm run lint",
    "build":    "echo 'Server has no build step'",

    "db:seed":   "node src/scripts/seed.js",
    "db:clear":  "node src/scripts/clear.js",
    "db:reset":  "npm run db:clear && npm run db:seed"
  }
}

Chaining Scripts

{
  "scripts": {
    "lint":   "eslint src/",
    "test":   "jest",
    "check":  "npm run lint && npm run test",

    "db:clear": "node scripts/clear.js",
    "db:seed":  "node scripts/seed.js",
    "db:reset": "npm run db:clear && npm run db:seed"
  }
}
Chaining operators in npm scripts:
  &&   runs next command only if previous succeeded (exit code 0)
  ||   runs next command only if previous failed
  ;    always runs both commands regardless of exit code
  |    pipes output from one command to another

Example:
  "check": "npm run lint && npm run test"
  → Runs lint first. If lint fails (any errors), test is skipped.
  → Both must pass for the check script to succeed.
  → CI pipelines use this pattern to gate deployments.

Using Environment Variables in Scripts

{
  "scripts": {
    "start": "NODE_ENV=production node index.js",
    "dev":   "NODE_ENV=development nodemon index.js",
    "test":  "NODE_ENV=test jest"
  }
}
# For cross-platform compatibility (works on Windows too) use cross-env:
npm install -D cross-env
{
  "scripts": {
    "start": "cross-env NODE_ENV=production node index.js",
    "dev":   "cross-env NODE_ENV=development nodemon index.js",
    "test":  "cross-env NODE_ENV=test jest"
  }
}

Common Mistakes

Mistake 1 — Using globally installed tools in scripts

❌ Wrong — script relies on a globally installed package that other developers may not have:

"scripts": {
  "dev": "nodemon index.js"   // assumes nodemon is installed globally
}
// Another developer clones the repo, nodemon is not global → script fails

✅ Correct — install the tool as a local devDependency so it is always available via node_modules/.bin:

npm install -D nodemon   # now available locally to all scripts ✓

Mistake 2 — Duplicating long commands everywhere instead of using scripts

❌ Wrong — every developer manually types the full command:

NODE_ENV=development nodemon --watch src --ext js,json index.js
# Different developers use slightly different flags — inconsistent behaviour

✅ Correct — define it once in package.json and everyone uses the same command:

"scripts": {
  "dev": "cross-env NODE_ENV=development nodemon --watch src --ext js,json index.js"
}

Mistake 3 — Not testing the start script before deploying

❌ Wrong — only ever running npm run dev locally and assuming npm start (used by Render/Heroku) works the same:

npm run dev  → nodemon index.js → works
npm start    → "start" script not defined → "Missing script: start" error on deployment

✅ Correct — always define and test both start (production) and dev (development) scripts:

"scripts": {
  "start": "node index.js",
  "dev":   "nodemon index.js"
}

Quick Reference

Script Command to run Purpose
start npm start Production — plain Node.js
dev npm run dev Development — nodemon with auto-restart
test npm test Run Jest test suite
lint npm run lint Check code for ESLint errors
lint:fix npm run lint:fix Auto-fix ESLint errors
build npm run build Build React app for production
seed npm run seed Populate database with test data
install:all npm run install:all Install deps for all sub-projects at once

🧠 Test Yourself

A new developer clones your MERN project and runs npm run dev in the server directory, getting nodemon: command not found. nodemon is listed in devDependencies in package.json. What is the most likely cause?