Every Node.js project revolves around a single JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. file: package.json. It is the project's ID card, it tells npm what the project is called, what version it is on, which packages it depends on, and what commands are available to run. Understanding this file well will save you from dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. confusion, broken deployments, and "it works on my machine" debugging sessions.
Creating a project from scratch
The npm init command walks you through setting up package.json. Adding the -y flag accepts all defaults immediately:
mkdir my-api
cd my-api
npm init -yThe result is a minimal package.json:
{
"name": "my-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}You will customise most of these fields before pushing to production, but this is a perfectly valid starting point.
Important fields explained
name and version
The name field must be all lowercase with no spaces (hyphens are fine). It only needs to be globally unique if you plan to publish the package to the npm registryWhat is registry?A server that stores and distributes packages or container images - npm registry for JavaScript packages, Docker Hub for container images.. For private applications, it just needs to be recognisable.
The version field follows semantic versioningWhat is semantic versioning?A numbering system (major.minor.patch) that communicates whether a release contains breaking changes, new features, or bug fixes. (semver): MAJOR.MINOR.PATCH. Increment MAJOR for breaking changes, MINOR for new backwards-compatible features, and PATCH for bug fixes.
main
Tells Node.js which file to load when someone does require('your-package') or import from it. For a library this matters a lot. For an application it is less critical but still good practice to keep it accurate.
type: moduleWhat is module?A self-contained file of code with its own scope that explicitly exports values for other files to import, preventing name collisions. vs CommonJSWhat is commonjs?Node.js's original module format using require() and module.exports - distinct from and predating the ES module import/export syntax.
By default, Node.js treats .js files as CommonJS modules (require / module.exports). Add "type": "module" to switch to ES modulesWhat is es modules?The official JavaScript module standard using import and export - enabled in Node.js via "type": "module" in package.json. (import / export) across all .js files in the project:
{
"type": "module"
}.mjs for ES module files and .cjs for CommonJS files, regardless of the type field. But choosing one style and sticking to it is much simpler.engines
Declares which versions of Node.js and npm your project requires. This is advisory by default, but tools like Heroku and certain CI systems enforce it:
{
"engines": {
"node": ">=20.0.0",
"npm": ">=10.0.0"
}
}private
Setting "private": true prevents you from accidentally running npm publish and leaking an internal application to the public registry. Always include this for applications (as opposed to libraries):
{
"private": true
}Installing dependencies
# A production dependency - added to "dependencies"
npm install express
# A dev-only dependency - added to "devDependencies"
npm install --save-dev nodemon
# Multiple packages at once
npm install express lodash dotenv
npm install --save-dev jest eslintAfter running these commands, the packages appear in node_modules/ and the version ranges are recorded in package.json.
Dependencies vs devDependencies
{
"dependencies": {
"express": "^4.18.0",
"dotenv": "^16.0.0"
},
"devDependencies": {
"nodemon": "^3.0.0",
"jest": "^29.0.0",
"eslint": "^8.0.0"
}
}When you deploy to a server and run npm install --omit=dev (or the older npm install --production), npm only installs dependencies. This keeps the production bundleWhat is bundle?A single JavaScript file (or set of files) that a build tool creates by combining all your source code and its imports together. lean and avoids shipping test tools to your server.
dependencies works the same way as one in devDependencies, it is just a signal about when it is needed.Semantic versioningWhat is semantic versioning?A numbering system (major.minor.patch) that communicates whether a release contains breaking changes, new features, or bug fixes. ranges
When npm updates your packages, it uses the version prefix to decide which releases are acceptable:
| Prefix | Range allowed | Example |
|---|---|---|
1.2.3 (no prefix) | Exactly that version | Only 1.2.3 |
^1.2.3 | Minor and patch updates | >=1.2.3 <2.0.0 |
~1.2.3 | Patch updates only | >=1.2.3 <1.3.0 |
>=1.2.0 | Anything at or above | 1.2.0, 2.0.0, … |
* | Any version | Latest available |
The caret (^) is the npm default. It allows new features (minor) and bug fixes (patch) but blocks breaking changes (major). For most packages this is a reasonable policy. For packages with a history of breaking minor versions, pin to an exact version instead.
Scripts
The scripts field is a map of short names to shell commands. Using it means your whole team runs tasks the same way, regardless of which tools are installed globally on their machines:
{
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "jest",
"test:watch": "jest --watch",
"lint": "eslint src/",
"build": "tsc",
"prebuild": "npm run lint",
"postbuild": "echo 'Build complete'"
}
}npm start # No 'run' needed for 'start'
npm test # No 'run' needed for 'test'
npm run dev # Custom scripts need 'run'
npm run lint
npm run build # Runs 'prebuild' first, then 'build', then 'postbuild'Hooks with the pre and post prefix run automatically before and after the matching script. This is useful for enforcing linting before every build.
The package-lock.jsonWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. file
Every time you run npm install, npm also writes (or updates) package-lock.json. This file records the exact version of every package in your dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. tree, not just the top-level ones, but every package those packages depend on.
Why does this matter? The version ranges in package.json (like ^4.18.0) allow updates. Two developers running npm install on different days might get different minor versions. package-lock.json eliminates that variability by pinning everything exactly.
Always commitWhat is commit?A permanent snapshot of your staged changes saved in Git's history, identified by a unique hash and accompanied by a message describing what changed. package-lock.json. Never commit node_modules/.
package-lock.json ever gets into a confused state, you can safely delete it and run npm install to regenerate it from scratch. The result will install the latest allowed versions of all packages.Project structure best practices
my-api/
├── package.json ← committed
├── package-lock.json ← committed
├── .gitignore ← committed
├── .nvmrc ← committed
├── src/
│ ├── index.js
│ ├── routes/
│ ├── middleware/
│ └── utils/
├── tests/
└── node_modules/ ← NEVER committedA solid .gitignore for a Node.js project looks like this:
# Dependencies - always large, always regeneratable
node_modules/
# Logs
*.log
npm-debug.log*
# Environment secrets
.env
.env.local
.env.*.local
# Build output
dist/
build/
# OS noise
.DS_Store
Thumbs.dbQuick reference
| Command | What it does |
|---|---|
npm init -y | Create package.json with defaults |
npm install express | Add to dependencies |
npm install -D jest | Add to devDependencies |
npm install | Install all dependencies from package.json |
npm install --omit=dev | Install production dependencies only |
npm run dev | Run the dev script |
npm start | Run the start script (no run needed) |
npm update | Update packages within their allowed ranges |