Suggest an editImprove this articleRefine the answer for “What is package.json and how does npm work?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**package.json** is the configuration file that lists a Node.js project's dependencies, scripts, and metadata. npm reads it to install packages and run commands. ```bash npm install express # adds express to package.json dependencies npm start # runs the "start" script defined in package.json ``` **Key rule:** always commit `package-lock.json` alongside `package.json` for consistent installs across machines.Shown above the full answer for quick recall.Answer (EN)Image**package.json** is the configuration file at the root of every Node.js project. It lists dependencies, defines scripts, and stores metadata. **npm** reads it to install packages from the public registry and run the commands you define. ## Theory ### TL;DR - package.json is your project's shopping list; npm is the delivery service that fetches everything on it - `dependencies` go to production; `devDependencies` are for build tools and tests only - `package-lock.json` locks exact resolved versions so every `npm install` produces the same result - Never commit `node_modules/`; always commit `package-lock.json` - Use `npm ci` in CI pipelines instead of `npm install` ### Quick example ```bash mkdir my-app && cd my-app npm init -y # generates package.json instantly npm install express # adds to dependencies npm install --save-dev jest # adds to devDependencies npm start # runs the "start" script ``` ```json { "name": "my-app", "version": "1.0.0", "scripts": { "start": "node index.js" }, "dependencies": { "express": "^4.19.2" }, "devDependencies": { "jest": "^29.0.0" } } ``` Running `npm start` triggers the `"start"` entry in scripts. That is the whole idea. ### Key fields | Field | What it does | |---|---| | `name` | Package name, unique if published to the npm registry | | `version` | SemVer: major.minor.patch | | `main` | Entry file Node loads when someone `require()`s your package | | `scripts` | Commands accessible via `npm run <name>` | | `dependencies` | Packages required at runtime in production | | `devDependencies` | Packages for development only, excluded with `--production` | | `peerDependencies` | Packages the consumer of your library must install themselves | | `engines` | Minimum required Node.js version | ### Version ranges and SemVer ```json "express": "4.19.2" // exact, never auto-updates "express": "^4.19.2" // compatible: >=4.19.2 <5.0.0 (npm default) "express": "~4.19.2" // patch only: >=4.19.2 <4.20.0 "express": "*" // any version - avoid this in real projects ``` The `^` prefix is what npm writes by default on `npm install`. It allows minor and patch updates, which semver says should be backward compatible. The `~` is stricter: patches only. Pinning exact versions sounds safer but means you miss security patches unless you bump versions manually. ### How npm install works When you run `npm install`, npm does three things in sequence. It parses package.json for required version ranges. Then it checks `package-lock.json` for exact versions already resolved on a previous run. Finally, it downloads tarballs from `registry.npmjs.org` and unpacks them into `node_modules/`. If no lockfile exists yet, npm resolves the latest matching version for each package and creates one. `npm ci` skips the resolution step entirely. It reads the lockfile directly and errors out if the file is out of sync with package.json. That is why CI/CD pipelines prefer it: the result is deterministic and the install is faster. I have seen projects break in staging because someone deleted `package-lock.json` to "clean things up" and minor version bumps quietly changed behavior. The lockfile exists for a reason. ### npm scripts and lifecycle hooks ```json { "scripts": { "start": "node dist/index.js", "dev": "nodemon src/index.ts", "build": "tsc", "test": "jest --coverage", "prestart": "npm run build", "posttest": "echo 'Tests done'" } } ``` The `pre` and `post` prefixes are lifecycle hooks. `prestart` runs before `start` automatically, no extra call needed. You run scripts with `npm run build`, but `npm start` and `npm test` have built-in shortcuts that skip the `run` keyword. ### When to use - New project from scratch: `npm init -y` - Add a runtime library: `npm install express` - Add a dev tool: `npm install --save-dev typescript jest` - Share the project: commit `package.json` + `package-lock.json`, teammates run `npm install` - CI/CD or production deploy: `npm ci` instead of `npm install` - Check for known vulnerabilities: `npm audit` ### Common mistakes **Not committing package-lock.json.** Without it, `npm install` fetches the latest version matching your range. Your `^4.19.2` could resolve to `4.20.1` for a colleague next month if a new minor version dropped. Things break. ```bash # Wrong echo "package-lock.json" >> .gitignore git commit -m "cleanup" # teammates now get different installs # Right git add package-lock.json # always commit this file ``` **Committing node_modules.** This folder can reach 200MB on a modest project. npm rebuilds it in seconds from the lockfile. No reason to track it in git. **Putting dev tools in dependencies.** TypeScript, Jest, nodemon: none of these run your app in production. They belong in `devDependencies`. If they end up in `dependencies`, they get installed on every production deploy for nothing. **Wildcard engines field.** `"node": "*"` means your code might land on Node 12, which does not support top-level `await` or several newer APIs. Pin a minimum: `"node": ">=20.0.0"`. **Using `npm install` in CI pipelines.** It can mutate the lockfile if it finds drift between package.json and the current lockfile state. Use `npm ci` in pipelines; it errors on mismatch, which surfaces the problem instead of silently patching it. ### Real-world usage - Express projects: `"start": "node server.js"` in scripts, `express@^4.19.2` in dependencies - Create React App: `react`, `react-dom`, `react-scripts` in dependencies; `npm start` spins up webpack dev server - Next.js: `"build": "next build"`, `"start": "next start"` for SSR in production - NestJS: `@nestjs/core`, `@nestjs/common` in dependencies, `"node": ">=18"` in engines - For a single-app repo, npm works fine. Monorepos with multiple packages often switch to pnpm for better workspace support and lower disk usage. ### Follow-up questions **Q:** What is the difference between `dependencies` and `devDependencies`? **A:** `dependencies` get installed in production. `devDependencies` are skipped when you run `npm install --production`. Jest, TypeScript, and nodemon belong in `devDependencies` because they serve no purpose at runtime. **Q:** Explain what `"^4.19.2"` means in semver terms. **A:** The `^` allows minor and patch updates: any version `>=4.19.2 <5.0.0`. The `~` is stricter: patches only, `>=4.19.2 <4.20.0`. Major version bumps are never automatic because semver treats them as breaking changes. **Q:** What happens if package-lock.json does not exist? **A:** npm resolves the latest version matching each range in package.json and creates a new lockfile. Two separate `npm install` runs at different times can resolve different versions, which causes the classic "works on my machine" problem. **Q:** What is the difference between `npm install` and `npm ci`? **A:** `npm install` resolves versions and can update the lockfile as a side effect. `npm ci` reads the lockfile exactly as written and errors if it is out of sync with package.json. Use `npm ci` in automated pipelines. **Q:** (Senior) How do npm workspaces manage shared dependencies in a monorepo? **A:** Adding `"workspaces": ["packages/*"]` to the root package.json tells npm to hoist shared dependencies to the root `node_modules`. If three packages all depend on React, npm installs it once instead of three times. The `overrides` field forces a specific version across all sub-packages when peer dependency conflicts come up. ## Examples ### Basic: Express server with npm start ```json // package.json { "name": "basic-api", "version": "1.0.0", "main": "index.js", "scripts": { "start": "node index.js" }, "dependencies": { "express": "^4.19.2" } } ``` ```js // index.js const express = require('express'); const app = express(); app.get('/', (req, res) => res.send('Hello World')); app.listen(3000, () => console.log('Server on port 3000')); ``` Run `npm install` to get Express, then `npm start`. You see "Server on port 3000" in the terminal. GET to `localhost:3000` returns "Hello World". This is the minimal setup every Express project starts with. ### Intermediate: TypeScript dev setup with build and watch scripts ```json // package.json { "name": "ts-api", "version": "1.0.0", "scripts": { "start": "node dist/index.js", "dev": "nodemon src/index.ts", "build": "tsc", "prestart": "npm run build" // compiles before starting }, "dependencies": { "express": "^4.19.2", "dotenv": "^16.4.5" }, "devDependencies": { "nodemon": "^3.1.4", "typescript": "^5.6.2", "@types/express": "^4.17.21" }, "engines": { "node": ">=20.0.0" } } ``` ```ts // src/index.ts import express from 'express'; import dotenv from 'dotenv'; dotenv.config(); const app = express(); const port = process.env.PORT || 3000; app.get('/', (req, res) => res.send(`Running on port ${port}`)); app.listen(port); ``` `npm run dev` starts nodemon watching TypeScript files and restarts on every change. `npm start` triggers `prestart` first, which compiles TypeScript to `dist/`, then starts the compiled output. The split between `devDependencies` (nodemon, typescript, @types) and `dependencies` (express, dotenv) is the key pattern here. ### Advanced: Peer dependency conflict and the overrides fix ```json // packages/ui-lib/package.json (sub-package in a monorepo) { "name": "ui-lib", "peerDependencies": { "react": "^18.0.0" }, "dependencies": { "lodash": "^4.17.21" } } ``` If the root app installs React 17 but `ui-lib` declares `react@^18` as a peer, npm warns and installs anyway. At runtime, components using React 18 APIs on a React 17 runtime throw errors. ```json // Root package.json - forces unified React version { "workspaces": ["packages/*"], "overrides": { "react": "^18.3.0" } } ``` The `overrides` field forces every sub-package to resolve React to `^18.3.0`, eliminating the mismatch. Without it: `Module not found` or runtime type errors. With it: one React version across the whole workspace.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.