package.json & package-lock.json Essentials Β· Astro Tech Blog

The Heart of Every Node.js Project

Every Node.js project has a package.json file. It’s the manifest that describes your project β€” its name, version, dependencies, scripts, and more.

package.json         ───► Human-readable manifest
package-lock.json   ───► Machine-generated lock file
node_modules/       ───► Installed packages (gitignored)

package.json β€” Key Fields

1. Metadata

{
  "name": "my-awesome-app",
  "version": "1.0.0",
  "description": "A Node.js app that does awesome things",
  "author": "Abhishek Sharma",
  "license": "MIT",
  "keywords": ["nodejs", "api", "tutorial"]
}
FieldRequiredPurpose
nameYes (for publishing)Package name (lowercase, no spaces)
versionYes (for publishing)SemVer version
descriptionNoBrief summary
authorNoCreator name/email
licenseNoLicense type (MIT, ISC, GPL)

2. Entry Point

{
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    }
  }
}
  • main β€” CommonJS entry point (used by require())
  • module β€” ES Module entry point (used by bundlers)
  • exports β€” Modern field for conditional exports (Node.js 12+)
  • types β€” TypeScript type definitions

3. Dependencies

{
  "dependencies": {
    "express": "^4.18.0",
    "lodash": "^4.17.21"
  },
  "devDependencies": {
    "jest": "^29.0.0",
    "typescript": "^5.0.0"
  },
  "peerDependencies": {
    "react": "^18.0.0"
  },
  "optionalDependencies": {
    "fsevents": "^2.3.2"
  }
}
SectionPurposeInstalled by default?
dependenciesRuntime packages your app needsYes
devDependenciesBuild/lint/test toolsNo (npm install --production)
peerDependenciesHost package expected to provideNo (warning if missing)
optionalDependenciesNon-critical, might fail to installYes (silent failure)

4. Scripts

{
  "scripts": {
    "start": "node index.js",
    "dev": "node --watch index.js",
    "build": "tsc",
    "test": "jest --coverage",
    "lint": "eslint .",
    "preview": "node dist/index.js"
  }
}

Run with:

npm start
npm run dev
npm test
npm run build

Special scripts: prestart, poststart, prebuild, postbuild run automatically before/after the named script. For example, prebuild runs before npm run build.

5. Engines

{
  "engines": {
    "node": ">=18.0.0",
    "npm": ">=9.0.0"
  }
}

Blocks installation on incompatible Node.js versions. Set this in library packages to enforce minimum versions.

6. Configuration

{
  "type": "module",
  "private": true,
  "sideEffects": false
}
FieldPurpose
"type": "module"Use ES Modules by default (.js files are ESM)
"private": truePrevent accidental npm publish
"sideEffects": falseTells bundlers it’s safe to tree-shake

package-lock.json

What It Is

package-lock.json is automatically generated by npm. It records the exact version of every installed package (including transitive dependencies).

{
  "name": "my-app",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "packages": {
    "node_modules/express": {
      "version": "4.18.2",
      "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
      "integrity": "sha512-...sha512-hash..."
    }
  }
}

Why You Must Commit It

Without lock file:                 With lock file:
npm install          ===> 4.18.2   npm install       ===> 4.18.2 (locked)
npm install (day 2)  ===> 4.19.0   npm install (day 2) ===> 4.18.2 (same!)
package.jsonpackage-lock.json
AuthorYou (manual)npm (auto)
Committed to git?YesYes (always!)
Version ranges^4.17.21Exact: 4.17.21
PurposeDeclare intentLock exact versions
ChangesYou edit itnpm updates it

Critical: Always commit package-lock.json to git. Without it, two developers can get different node_modules trees from the same package.json.

Comparing package-lock.json and package.json

# Check what's actually installed
npm ls --depth=0

# See the full tree
npm ls

# Diff your installed versions against what's allowed
npm outdated

npm ci β€” Clean Install

# Deletes node_modules and installs exact versions from lock file
npm ci
npm installnpm ci
Uses lock fileUpdates itReads it (read-only)
SpeedSlower (resolves versions)Faster (skips resolution)
node_modulesUpdates existingDeletes and reinstalls
Use caseDevelopmentCI/CD, production builds

Minimal Production package.json

{
  "name": "my-api",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js",
    "test": "node --test src/**/*.test.js",
    "lint": "eslint src/"
  },
  "dependencies": {
    "express": "^4.18.0"
  },
  "devDependencies": {
    "eslint": "^8.0.0"
  },
  "engines": {
    "node": ">=20.0.0"
  }
}

Key Takeaways

  • package.json is the project manifest β€” name, version, dependencies, scripts, and metadata
  • package-lock.json locks exact dependency versions β€” always commit it
  • npm ci is the correct way to install in CI/CD β€” it’s faster and deterministic
  • Use "type": "module" to opt into ES Modules across your project
  • "private": true prevents accidental publishing
  • The exports field is the modern way to define package entry points