npm
03 / 08

Versioning & Publishing

npm Versioning & Publishing

Semantic versioning, version ranges, and the publish workflow are essential knowledge for anyone maintaining a library or CLI tool. Understanding them also helps you manage dependencies responsibly in applications.

Semantic Versioning (semver)

# semver format: MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]
# 1.4.2-beta.1+build.123

# MAJOR: breaking (incompatible API changes)
# MINOR: new features (backward compatible)
# PATCH: bug fixes (backward compatible)

# Version ranges in package.json:
# Exact:    "1.4.2"    - only this version
# Caret:    "^1.4.2"  - >=1.4.2 <2.0.0 (same major; allows minor+patch)
# Tilde:    "~1.4.2"  - >=1.4.2 <1.5.0 (same minor; allows patch only)
# Range:    ">=1.4.0 <2.0.0"
# Latest:   "*" or ""  - any version (dangerous)
# Any 1.x:  "1.x" or "1.*"

# Special cases:
# "^0.4.2"  → >=0.4.2 <0.5.0   (0.x.x: ^ only allows patch changes!)
# "^0.0.3"  → >=0.0.3 <0.0.4   (0.0.x: ^ pins exactly)
# Reason: before 1.0, minor versions may have breaking changes

# Prerelease versions (not matched by ^ unless explicitly specified)
# alpha < beta < rc < stable
# 1.0.0-alpha.1 < 1.0.0-alpha.2 < 1.0.0-beta.1 < 1.0.0-rc.1 < 1.0.0

# Check what a range resolves to
npx semver -r "^1.4.2" 1.4.3 1.5.0 2.0.0   # Tests versions against range

# Lock file pins exact versions regardless of ranges
# package.json: "^4.18.2" → package-lock.json: "4.18.3" (exact)

Bumping Versions

# npm version: bumps package.json, creates git commit + tag automatically
npm version patch        # 1.4.2 → 1.4.3  (bug fix)
npm version minor        # 1.4.2 → 1.5.0  (new feature)
npm version major        # 1.4.2 → 2.0.0  (breaking change)

# Prerelease increments
npm version prerelease --preid=alpha   # 1.4.2 → 1.4.3-alpha.0
npm version prerelease                 # 1.4.3-alpha.0 → 1.4.3-alpha.1
npm version prepatch                   # 1.4.2 → 1.4.3-0
npm version preminor                   # 1.4.2 → 1.5.0-0
npm version premajor                   # 1.4.2 → 2.0.0-0

# Set specific version
npm version 2.0.0-rc.1

# Skip git tag
npm version patch --no-git-tag-version

# Custom commit message
npm version patch -m "chore: release v%s"

# After version bump, push commit AND tag:
git push && git push --tags

Publishing to npm Registry

# Login to npm
npm login                        # Interactive
npm login --scope=@myorg         # For scoped packages
npm whoami                       # Check who you're logged in as
npm token list                   # List auth tokens
npm token create --read-only     # Create CI token

# What gets published?
# Whitelist: "files" field in package.json
# Blacklist: .npmignore (like .gitignore for publishing)
# Default: everything EXCEPT node_modules, .git, tests

# .npmignore example:
# src/
# tests/
# *.test.ts
# tsconfig.json
# .env*

# Preview what would be published (dry run)
npm pack --dry-run               # Lists files that would be packed
npm pack                         # Creates .tgz; inspect it
npm publish --dry-run            # Full dry run of publish

# Publish
npm publish                      # Publish public package
npm publish --access public      # Required for scoped packages on first publish
npm publish --access restricted  # Scoped package, private (requires paid account)
npm publish --tag beta           # Publish as beta dist-tag (not "latest")

# Publish prerelease without updating "latest" tag
npm publish --tag next           # Users install with npm install pkg@next

# Deprecate a version
npm deprecate my-pkg@1.0.0 "Use 2.x instead"
npm deprecate my-pkg@"<2.0.0" "All 1.x versions are deprecated"

# Unpublish (72-hour window, or request support)
npm unpublish my-pkg@1.0.0
npm unpublish my-pkg --force     # Unpublish entire package (careful!)

# dist-tags
npm dist-tag ls my-pkg           # List all dist-tags
npm dist-tag add my-pkg@2.0.0 latest
npm dist-tag add my-pkg@1.0.0 legacy
npm dist-tag rm my-pkg beta

Scoped Packages & Private Registry

# Scoped packages: @scope/package-name
# Scope can be your username (@alice/utils) or org (@myorg/sdk)
# Free scoped packages are public; private requires paid npm org account

# Install scoped package
npm install @myorg/design-system
npm install @myorg/design-system@2.1.0

# Publish scoped package
npm publish --access public       # First publish must set access explicitly
npm publish                       # Subsequent publishes remember access setting

# Use GitHub Package Registry instead of npmjs.com
# .npmrc in project root:
@myorg:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}

# Use Verdaccio for self-hosted npm registry
npm install -g verdaccio
verdaccio                         # Start server on :4873
npm config set registry http://localhost:4873
npm adduser --registry http://localhost:4873

# .npmrc for team (commit to repo, never commit tokens)
# registry=https://registry.npmjs.org/
# @myorg:registry=https://npm.pkg.github.com
# save-exact=true
# engine-strict=true

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free