Episode 3 — NodeJS MongoDB Backend Architecture / 3.1 — Starting with NodeJS
3.1.e — Package.json Deep Dive
In one sentence: The
package.jsonfile is the single source of truth for your Node.js project — defining its identity, dependencies, scripts, module system, and every configuration that npm, Node.js, and other tools need to manage your application.
Navigation: <- 3.1 Overview · Next -> 3.1 Exercise Questions
Table of Contents
- 1. What Is package.json?
- 2. Anatomy of package.json — Every Field Explained
- 3. The name and version Fields
- 4. main, module, and exports — Entry Points
- 5. scripts — Your Project's Command Center
- 6. dependencies vs devDependencies vs peerDependencies
- 7. type: "module" — ES Modules vs CommonJS
- 8. engines — Enforcing Node.js Versions
- 9. package-lock.json — Why It Matters
- 10. private: true and Publishing
- 11. Real-World package.json Walkthrough
- Key Takeaways
- Explain-It Challenge
1. What Is package.json?
Every Node.js project has a package.json at its root. Think of it as the identity card + instruction manual for your project.
┌─────────────────────────────────────────────────────────────────┐
│ package.json IS YOUR PROJECT'S: │
│ │
│ 1. IDENTITY CARD │
│ └── name, version, description, author, license │
│ │
│ 2. DEPENDENCY MANIFEST │
│ └── Every package your project needs to work │
│ │
│ 3. SCRIPT RUNNER │
│ └── Custom commands: start, test, build, deploy │
│ │
│ 4. CONFIGURATION HUB │
│ └── Module system, engines, entry points, exports │
│ │
│ 5. PUBLISHING INSTRUCTIONS │
│ └── What to include/exclude when sharing on npm │
└─────────────────────────────────────────────────────────────────┘
Creating package.json
# Interactive — asks questions
npm init
# Quick — all defaults
npm init -y
2. Anatomy of package.json — Every Field Explained
Here is every commonly used field at a glance:
| Field | Required? | Purpose |
|---|---|---|
name | Yes (for publishing) | Unique package name on npm |
version | Yes (for publishing) | SemVer version number |
description | No | One-line summary (shown on npmjs.com) |
main | No | Entry point for require() (CommonJS) |
module | No | Entry point for import (used by bundlers) |
exports | No | Modern entry point map (Node 12+) |
type | No | "module" for ESM, "commonjs" (default) for CJS |
scripts | No | Custom npm commands |
dependencies | No | Packages needed at runtime |
devDependencies | No | Packages needed only for development |
peerDependencies | No | Packages the consumer must provide |
engines | No | Required Node.js/npm versions |
private | No | If true, prevents accidental publishing to npm |
author | No | Author name, email, URL |
license | No | SPDX license identifier (MIT, ISC, Apache-2.0) |
keywords | No | Search terms for npm registry |
repository | No | Link to source code (GitHub, GitLab) |
homepage | No | Project homepage URL |
bugs | No | Issue tracker URL |
files | No | Whitelist of files to include when publishing |
3. The name and version Fields
name
{
"name": "my-awesome-api"
}
Rules for the name field:
- Must be lowercase
- No spaces (use hyphens or underscores)
- Max 214 characters
- Cannot start with
.or_ - Must be unique on npm (if you plan to publish)
- Scoped names:
@myorg/my-package(for organizations)
version
{
"version": "1.2.3"
}
Uses Semantic Versioning (SemVer):
MAJOR . MINOR . PATCH
1 . 2 . 3
MAJOR → Breaking changes (API changed, old code may break)
MINOR → New features, backward-compatible
PATCH → Bug fixes, backward-compatible
Bumping versions
# Automatically increment version numbers
npm version patch # 1.2.3 → 1.2.4
npm version minor # 1.2.4 → 1.3.0
npm version major # 1.3.0 → 2.0.0
# These commands update package.json AND create a git tag
4. main, module, and exports — Entry Points
The entry point tells Node.js (and bundlers) which file to load when someone require()s or imports your package.
main (CommonJS entry)
{
"main": "src/index.js"
}
When someone writes const myPkg = require('my-package'), Node.js loads whatever main points to. Defaults to index.js if omitted.
module (ES Module entry for bundlers)
{
"main": "dist/index.cjs",
"module": "dist/index.mjs"
}
The module field is not official Node.js — it is used by bundlers (webpack, Rollup, esbuild) to load the ES Module version for better tree-shaking.
exports (Modern, Node 12+)
The exports field is the modern replacement for main. It gives you fine-grained control:
{
"exports": {
".": {
"require": "./dist/index.cjs",
"import": "./dist/index.mjs"
},
"./utils": {
"require": "./dist/utils.cjs",
"import": "./dist/utils.mjs"
}
}
}
// Consumer code
const pkg = require('my-package'); // → dist/index.cjs
import pkg from 'my-package'; // → dist/index.mjs
import { helper } from 'my-package/utils'; // → dist/utils.mjs
Which to use?
| Scenario | Use |
|---|---|
| Simple app (not a library) | main is enough |
| Library supporting both CJS and ESM | exports with require and import conditions |
| Library consumed by bundlers | Add module as a fallback |
5. scripts — Your Project's Command Center
Defining scripts
{
"scripts": {
"start": "node server.js",
"dev": "node --watch server.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"format": "prettier --write .",
"build": "tsc",
"clean": "rm -rf dist node_modules",
"seed": "node scripts/seed-database.js",
"db:migrate": "node scripts/migrate.js"
}
}
Running scripts
# Special scripts (no "run" needed)
npm start # Runs "start"
npm test # Runs "test" (alias: npm t)
npm stop # Runs "stop"
npm restart # Runs "stop" then "restart" then "start"
# Custom scripts (need "run")
npm run dev
npm run lint
npm run build
npm run seed
Pre and post hooks
npm automatically runs pre<script> before and post<script> after any script:
{
"scripts": {
"pretest": "npm run lint",
"test": "jest",
"posttest": "echo 'All tests passed!'",
"prebuild": "rm -rf dist",
"build": "tsc",
"postbuild": "echo 'Build complete! Check the dist/ folder.'"
}
}
npm test
# Runs in order:
# 1. pretest → npm run lint
# 2. test → jest
# 3. posttest → echo 'All tests passed!'
Chaining commands in scripts
{
"scripts": {
"validate": "npm run lint && npm run test",
"dev:all": "npm run build && npm run dev",
"clean:install": "rm -rf node_modules && npm install"
}
}
| Operator | Meaning |
|---|---|
&& | Run next command only if previous succeeds |
|| | Run next command only if previous fails |
; | Run next command regardless of previous result |
Passing arguments to scripts
npm run test -- --verbose --coverage
# The -- passes everything after it as arguments to the script
# Equivalent to: jest --verbose --coverage
6. dependencies vs devDependencies vs peerDependencies
dependencies
Packages that your application needs to run in production.
{
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.6.3",
"dotenv": "^16.3.1",
"bcrypt": "^5.1.1",
"jsonwebtoken": "^9.0.2",
"cors": "^2.8.5"
}
}
npm install express # Adds to dependencies
npm i express # Shorthand
devDependencies
Packages needed only during development — not in production.
{
"devDependencies": {
"nodemon": "^3.0.2",
"jest": "^29.7.0",
"eslint": "^8.56.0",
"prettier": "^3.1.1",
"supertest": "^6.3.3",
"typescript": "^5.3.3"
}
}
npm install nodemon --save-dev # Adds to devDependencies
npm i nodemon -D # Shorthand
peerDependencies
Packages that your library expects the consumer to provide. Used when building plugins or libraries.
{
"name": "my-express-plugin",
"peerDependencies": {
"express": "^4.0.0 || ^5.0.0"
}
}
This says: "I work with Express, but I do not install it myself. The project using me must have Express already installed."
Comparison table
| Type | Installed in production? | Who installs it? | Example |
|---|---|---|---|
dependencies | Yes | npm install | express, mongoose, bcrypt |
devDependencies | No (--production skips them) | npm install (dev) | jest, eslint, nodemon |
peerDependencies | Must already be present | The consumer of your library | express (for a plugin) |
The production install
# Skip devDependencies in production (saves space, faster deploys)
npm install --production
# OR
NODE_ENV=production npm install
7. type: "module" — ES Modules vs CommonJS
CommonJS (the default)
{
"type": "commonjs"
}
// CommonJS syntax — the Node.js classic
const express = require('express');
const { readFile } = require('fs');
module.exports = { myFunction };
module.exports = myFunction;
ES Modules
{
"type": "module"
}
// ES Module syntax — the modern standard
import express from 'express';
import { readFile } from 'fs/promises';
export const myFunction = () => { ... };
export default myFunction;
Comparison
| Feature | CommonJS (require) | ES Modules (import) |
|---|---|---|
| Syntax | require() / module.exports | import / export |
| Loading | Synchronous (blocking) | Asynchronous (non-blocking) |
| Top-level await | Not supported | Supported |
| Tree-shaking | Not possible | Possible (bundlers can remove unused code) |
| File extension | .js or .cjs | .js (with "type": "module") or .mjs |
__dirname | Available | Not available (use import.meta.url) |
| Browser compatible | No | Yes (same syntax) |
| Node.js support | Since the beginning | Stable since Node 14 |
Mixing CJS and ESM in the same project
my-project/
├── package.json ← "type": "module"
├── src/
│ ├── app.js ← Uses import/export (ESM, because of "type")
│ └── legacy.cjs ← Uses require/module.exports (forced CJS by extension)
type field | .js files use | .mjs files use | .cjs files use |
|---|---|---|---|
"commonjs" (default) | CommonJS | ES Modules | CommonJS |
"module" | ES Modules | ES Modules | CommonJS |
Which should you use?
New projects (2024+) → "type": "module" (ES Modules)
Existing projects → Keep "commonjs" unless you have time to migrate
Libraries → Support both (use exports field with CJS + ESM builds)
8. engines — Enforcing Node.js Versions
The engines field specifies which versions of Node.js (and npm) your project supports.
{
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
}
Common patterns
// Require Node.js 18 or higher
{ "engines": { "node": ">=18" } }
// Require exactly Node.js 20.x
{ "engines": { "node": "20.x" } }
// Require a range
{ "engines": { "node": ">=18.0.0 <23.0.0" } }
// Require LTS versions only
{ "engines": { "node": "^18.0.0 || ^20.0.0 || ^22.0.0" } }
Enforcing engines
By default, engines is just a warning. To make it a hard error:
# Create .npmrc in your project root
echo "engine-strict=true" > .npmrc
Now npm install will fail if the Node.js version does not match.
9. package-lock.json — Why It Matters
The problem it solves
Developer A runs: npm install express@^4.18.2
→ Gets express 4.18.2
Two months later...
Developer B clones the repo and runs: npm install
→ Gets express 4.19.1 (a newer version within the ^4.18.2 range)
→ Something breaks because of a minor change in 4.19.1
What package-lock.json does
The lock file records the exact version of every package (and sub-dependency) that was installed.
package.json → "What I want" (express ^4.18.2)
package-lock.json → "What I got" (express 4.18.2, exactly)
Rules for package-lock.json
| Rule | Why |
|---|---|
| Always commit it to Git | Ensures everyone gets the exact same versions |
| Never edit it manually | npm manages it automatically |
Use npm ci in CI/CD | Installs exactly from the lock file (faster, stricter) |
| Do not delete it | Regenerating it may change dependency versions |
npm install vs npm ci
| Command | Reads | Behavior |
|---|---|---|
npm install | package.json + package-lock.json | May update lock file |
npm ci | package-lock.json only | Deletes node_modules, installs exact versions, fails if lock file is out of sync |
# Local development
npm install
# CI/CD pipelines (production builds)
npm ci # Clean Install — faster, deterministic, strict
10. private: true and Publishing
private: true
{
"private": true
}
This prevents your package from being accidentally published to npm. Use it for:
- Application projects (APIs, websites) that should never be on npm
- Internal company packages not meant for public consumption
files field — What gets published
{
"files": [
"dist/",
"README.md",
"LICENSE"
]
}
Only the listed files/directories are included when you run npm publish. Everything else is excluded. This is the inverse of .npmignore.
Other publishing-related fields
{
"name": "@myorg/my-library",
"version": "2.1.0",
"description": "A useful utility library",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": ["dist/", "README.md"],
"keywords": ["utility", "helper", "node"],
"author": "Arjun Sharma <arjun@example.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/myorg/my-library"
},
"homepage": "https://github.com/myorg/my-library#readme",
"bugs": {
"url": "https://github.com/myorg/my-library/issues"
}
}
11. Real-World package.json Walkthrough
Here is a realistic package.json for a production Express API:
{
"name": "cohart-api",
"version": "1.0.0",
"description": "Backend API for the Cohart learning platform",
"main": "src/server.js",
"type": "module",
"private": true,
"scripts": {
"start": "node src/server.js",
"dev": "node --watch --env-file=.env src/server.js",
"test": "jest --forceExit --detectOpenHandles",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"format": "prettier --write 'src/**/*.js'",
"pretest": "npm run lint",
"seed": "node src/scripts/seed.js",
"db:migrate": "node src/scripts/migrate.js"
},
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.6.3",
"dotenv": "^16.3.1",
"bcrypt": "^5.1.1",
"jsonwebtoken": "^9.0.2",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"express-rate-limit": "^7.1.5"
},
"devDependencies": {
"nodemon": "^3.0.2",
"jest": "^29.7.0",
"supertest": "^6.3.3",
"eslint": "^8.56.0",
"prettier": "^3.1.1"
},
"engines": {
"node": ">=20.0.0"
},
"author": "Cohart Team",
"license": "MIT"
}
Reading this file line by line
| Field | What It Tells Us |
|---|---|
"name": "cohart-api" | Project is called "cohart-api" |
"version": "1.0.0" | First stable release |
"main": "src/server.js" | Entry point is in the src/ directory |
"type": "module" | Uses import/export syntax (ES Modules) |
"private": true | Cannot be published to npm (it is an app, not a library) |
"start" script | Production: node src/server.js |
"dev" script | Development: watch mode + loads .env automatically |
"pretest" | Lints code before running tests |
| 9 production dependencies | What the server needs to run |
| 5 dev dependencies | Tools for development only |
"engines" | Requires Node.js 20 or higher |
Key Takeaways
package.jsonis mandatory for every Node.js project — it defines your project's identity, dependencies, scripts, and configuration.nameandversionare the only truly required fields (for publishing) —npm init -ygenerates sensible defaults for everything else.scriptsare your project's command center —npm start,npm test, and custom scripts withnpm run <name>replace ad-hoc terminal commands.- Pre/post hooks (
pretest,postbuild) run automatically before and after their corresponding scripts — great for linting before tests or cleaning before builds. dependenciesrun in production;devDependenciesare skipped with--production;peerDependenciesmust be installed by the consumer.type: "module"enablesimport/exportsyntax in.jsfiles — the modern standard for new projects.enginesspecifies which Node.js version your project needs — enforce it withengine-strict=truein.npmrc.package-lock.jsonlocks exact dependency versions — always commit it, never edit it manually, and usenpm ciin CI/CD.private: trueprevents accidental publishing — use it for applications and internal projects.
Explain-It Challenge
Can you open the
package.jsonof any open-source project on GitHub and explain every field? Try it with Express.js (https://github.com/expressjs/express/blob/master/package.json) — identify the entry point, scripts, dependencies, and what theenginesfield does. If you can do it confidently, you have mastered this topic.
Navigation: <- 3.1 Overview · Next -> 3.1 Exercise Questions