Episode 3 — NodeJS MongoDB Backend Architecture / 3.1 — Starting with NodeJS

3.1.e — Package.json Deep Dive

In one sentence: The package.json file 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?

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:

FieldRequired?Purpose
nameYes (for publishing)Unique package name on npm
versionYes (for publishing)SemVer version number
descriptionNoOne-line summary (shown on npmjs.com)
mainNoEntry point for require() (CommonJS)
moduleNoEntry point for import (used by bundlers)
exportsNoModern entry point map (Node 12+)
typeNo"module" for ESM, "commonjs" (default) for CJS
scriptsNoCustom npm commands
dependenciesNoPackages needed at runtime
devDependenciesNoPackages needed only for development
peerDependenciesNoPackages the consumer must provide
enginesNoRequired Node.js/npm versions
privateNoIf true, prevents accidental publishing to npm
authorNoAuthor name, email, URL
licenseNoSPDX license identifier (MIT, ISC, Apache-2.0)
keywordsNoSearch terms for npm registry
repositoryNoLink to source code (GitHub, GitLab)
homepageNoProject homepage URL
bugsNoIssue tracker URL
filesNoWhitelist 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?

ScenarioUse
Simple app (not a library)main is enough
Library supporting both CJS and ESMexports with require and import conditions
Library consumed by bundlersAdd 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"
  }
}
OperatorMeaning
&&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

TypeInstalled in production?Who installs it?Example
dependenciesYesnpm installexpress, mongoose, bcrypt
devDependenciesNo (--production skips them)npm install (dev)jest, eslint, nodemon
peerDependenciesMust already be presentThe consumer of your libraryexpress (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

FeatureCommonJS (require)ES Modules (import)
Syntaxrequire() / module.exportsimport / export
LoadingSynchronous (blocking)Asynchronous (non-blocking)
Top-level awaitNot supportedSupported
Tree-shakingNot possiblePossible (bundlers can remove unused code)
File extension.js or .cjs.js (with "type": "module") or .mjs
__dirnameAvailableNot available (use import.meta.url)
Browser compatibleNoYes (same syntax)
Node.js supportSince the beginningStable 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)CommonJSES ModulesCommonJS
"module"ES ModulesES ModulesCommonJS

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

RuleWhy
Always commit it to GitEnsures everyone gets the exact same versions
Never edit it manuallynpm manages it automatically
Use npm ci in CI/CDInstalls exactly from the lock file (faster, stricter)
Do not delete itRegenerating it may change dependency versions

npm install vs npm ci

CommandReadsBehavior
npm installpackage.json + package-lock.jsonMay update lock file
npm cipackage-lock.json onlyDeletes 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

FieldWhat 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": trueCannot be published to npm (it is an app, not a library)
"start" scriptProduction: node src/server.js
"dev" scriptDevelopment: watch mode + loads .env automatically
"pretest"Lints code before running tests
9 production dependenciesWhat the server needs to run
5 dev dependenciesTools for development only
"engines"Requires Node.js 20 or higher

Key Takeaways

  1. package.json is mandatory for every Node.js project — it defines your project's identity, dependencies, scripts, and configuration.
  2. name and version are the only truly required fields (for publishing) — npm init -y generates sensible defaults for everything else.
  3. scripts are your project's command center — npm start, npm test, and custom scripts with npm run <name> replace ad-hoc terminal commands.
  4. Pre/post hooks (pretest, postbuild) run automatically before and after their corresponding scripts — great for linting before tests or cleaning before builds.
  5. dependencies run in production; devDependencies are skipped with --production; peerDependencies must be installed by the consumer.
  6. type: "module" enables import/export syntax in .js files — the modern standard for new projects.
  7. engines specifies which Node.js version your project needs — enforce it with engine-strict=true in .npmrc.
  8. package-lock.json locks exact dependency versions — always commit it, never edit it manually, and use npm ci in CI/CD.
  9. private: true prevents accidental publishing — use it for applications and internal projects.

Explain-It Challenge

Can you open the package.json of 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 the engines field does. If you can do it confidently, you have mastered this topic.


Navigation: <- 3.1 Overview · Next -> 3.1 Exercise Questions