npm & package.json
scripts, dependencies, exports, type: module, lockfiles.
Every Node project has a package.json. It's a manifest: what the project is called, what it depends on, how to run it, what Node version it expects. Read one well and you can guess everything else about a codebase before opening a single source file. Let's learn how to read and write them.
Starting a project
mkdir my-app && cd my-app
npm init -y
# Creates a minimal package.jsonThat's the manual route. Most real projects start from a template (create-next-app, create-vite) which writes a smarter package.json for you.
Anatomy of a real-world package.json
{
"name": "@acme/api",
"version": "1.4.0",
"type": "module",
"private": true,
"packageManager": "pnpm@9.15.0",
"engines": {
"node": ">=22.0.0"
},
"main": "./dist/index.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./client": {
"types": "./dist/client.d.ts",
"import": "./dist/client.js"
}
},
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc -p tsconfig.build.json",
"start": "node dist/index.js",
"test": "vitest",
"lint": "eslint . --max-warnings=0",
"db:migrate": "drizzle-kit migrate"
},
"dependencies": {
"hono": "^4.7.0",
"zod": "^3.24.0"
},
"devDependencies": {
"@types/node": "^22.10.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0",
"vitest": "^2.1.0"
}
}Every field above is doing real work. Let's unpack the important ones.
scripts: your project's commands
Scripts are shortcuts. npm run dev looks up dev in the scripts object and runs its value. Convention: keep the same script names across projects so newcomers know exactly what to type.
npm run dev- local development with hot reloadnpm run build- compile/bundle for productionnpm start- run the production build (norunneeded;startandtestare special-cased)npm test- run the test suite
npm run other-script, or use && to chain. Example: "ci": "npm run lint && npm run test && npm run build".dependencies vs devDependencies
Two arrays, both holding package names. The split matters:
- dependencies - packages your shipped code uses at runtime.
hono,zod,react, a database driver. - devDependencies - only needed while developing: TypeScript, ESLint, testing libraries,
tsx, build tools.
Production installs (npm install --omit=dev or NODE_ENV=production npm install) skip dev deps, so your Docker images stay small.
npm install hono # adds to dependencies
npm install -D typescript # adds to devDependencies
npm install --save-exact zod # pins exact version, no caretdevDependencies. Mis-classifying is a common mistake.type: "module"
Set "type": "module" and your .js files become ES modules. You can use import and export instead of require(). Omit it and Node falls back to CommonJS, the older module system.
// With "type": "module"
import { readFile } from "node:fs/promises";
export const greet = (name) => "hi " + name;
// Without - CommonJS
const { readFile } = require("node:fs/promises");
module.exports.greet = (name) => "hi " + name;New projects in 2026: always set type: module. CommonJS is a legacy concern unless you're working in an old codebase.
The exports field
When you publish a library, exportstells consumers which files they're allowed to import. Without it, anyone can import "your-pkg/lib/internals/secret.js". With it, only the paths you whitelist are reachable.
"exports": {
".": "./dist/index.js",
"./client": "./dist/client.js"
}
// Consumers can now do:
// import { x } from "your-pkg"
// import { y } from "your-pkg/client"
// But NOT: import { z } from "your-pkg/internals"engines
Declare which Node version your project supports. npm warns on install if the user's Node is out of range; pnpm errors by default.
"engines": {
"node": ">=22.0.0",
"pnpm": ">=9.0.0"
}packageManager
Modern tip: pin your package manager exactly. Tools like corepack (built into Node) read this and install the right pnpm/yarn version automatically.
"packageManager": "pnpm@9.15.0"npm vs pnpm vs bun
Three popular package managers, all reading the same package.json. They differ in speed and how they store files.
- npm - ships with Node. Fine. Slow. Duplicates files.
- pnpm - uses a content-addressed global store; your
node_modulesis a tree of symlinks. Disk-efficient and fast. The current default in most serious teams. - bun- Bun's own installer. Extremely fast (often 10x npm). Compatible with npm registries.
- yarn - still around (Yarn 4 / Berry), but the JS community has largely moved to pnpm.
# All three read the same package.json:
npm install
pnpm install
bun install
# Adding deps:
npm add hono
pnpm add hono
bun add honoLockfiles: don't .gitignore these
Every install writes a lockfile pinning the exact resolved version of every package and every transitive dependency. Without it, running install on a teammate's machine a week later can pull different versions and your "works on my machine" bug ships.
npmwritespackage-lock.jsonpnpmwritespnpm-lock.yamlbunwritesbun.lockyarnwritesyarn.lock
semver: what ^ and ~ mean
Version strings like ^4.7.0 are not literal. They're ranges. semver versions read MAJOR.MINOR.PATCH:
- PATCH bumps for bug fixes:
4.7.0 to 4.7.1 - MINOR bumps for new (backward-compatible) features:
4.7.1 to 4.8.0 - MAJOR bumps for breaking changes:
4.8.0 to 5.0.0
"hono": "^4.7.0" // caret: allows 4.7.0 up to (but not including) 5.0.0
"hono": "~4.7.0" // tilde: allows 4.7.0 up to (but not including) 4.8.0
"hono": "4.7.0" // exact: only 4.7.0
"hono": "*" // anything (chaotic, don't do it)
"hono": "latest" // tag, not a range - risky^4.7.0 in package.json, the lockfile pins to one resolved version. Your teammates install the exact same tree. CI installs the exact same tree. The caret only matters when you explicitly upgrade.npx: run packages without installing them
npx (ships with npm) downloads and runs a CLI from npm without adding it to your project. Useful for one-shot generators.
npx create-next-app my-site
npx auth secret # generate an Auth.js secret
npx prisma init
npx serve ./dist # quick static serverpnpm's equivalent is pnpm dlx; Bun's is bunx.
Quiz
What goes in devDependencies vs dependencies?
Recap
package.jsonis the project manifest: name, version, scripts, deps, engines.- dependencies = runtime imports. devDependencies = build/test/lint tools.
- Set
"type": "module"for ESM.exportscontrols what consumers can import. - pnpm is the sensible default in 2026. Bun is the fast one. npm is the one that ships with Node.
- Always commit the lockfile. Caret ^ allows minor+patch, tilde ~ allows patch only.
npxruns CLIs from npm without installing them locally.