
Semantic Versioning: The Weight of v1.0.0
The silent promise between developers. Meaning of Major, Minor, Patch and the difference between `^` and `~` in `npm install`.

The silent promise between developers. Meaning of Major, Minor, Patch and the difference between `^` and `~` in `npm install`.
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

Establishing TCP connection is expensive. Reuse it for multiple requests.

November 2023, Friday 4:30 PM.
30 minutes before deployment, I was confident.
"Last deploy of the week, let me update some libraries."
npm update
Green checkmarks flooded the terminal. Everything looked smooth.
Ran a few local tests, "No issues!" and hit the deploy button.
5:10 PM, Slack notifications cascaded like a waterfall of red.
ERROR: Cannot find module 'createUser'
ERROR: login is not a function
ERROR: Uncaught TypeError at UserService.js:45
Production completely crashed.
CEO called me directly. Cold sweat dripping, I started the rollback process. The question was: "Which version do I roll back to?"
Opened package-lock.json:
{
"dependencies": {
"user-auth-lib": {
"version": "3.0.0" // ← Was 2.8.1 before
}
}
}
Senior developer looked at my screen and sighed.
"Dude, that's a Major version update. 2.x → 3.x is a Breaking Change. Of course it crashed."
Me: "But doesn't npm update only do safe updates?"
Senior: "What's in your package.json?"
{
"dependencies": {
"user-auth-lib": "^2.8.1"
}
}
Senior: "See that caret (^)? That allows Minor updates too. But I think this library developer broke SemVer. They went to 3.0.0 without documenting Breaking Changes."
Spent until 11 PM on emergency fixes. Monday, CEO lectured me for an hour.
That's when I realized: "Version numbers aren't just numbers. They're contracts and promises."After that incident, I needed answers:
Most importantly, if I ever build a library: "How do I version it so users can trust it?"
"1.0.0 to 1.0.1 or 2.0.0, isn't it just developer preference?"
"npm will only install safe versions, right?"
"Isn't package.json enough?"
"What's v1.0.0-alpha.1? Alpha? Is this official?"
"^1.0.0 means all 1.x.x are safe, right?"
Biggest misconception: "Versions are just numbers developers randomly bump."During rollback, senior drew on the whiteboard:
┌─────────────────────────────────────────────────────┐
│ MAJOR . MINOR . PATCH - PreRelease + Build │
│ 2 . 8 . 1 - beta.3 + 20231115 │
│ │ │ │ │ │ │
│ │ │ │ │ └─ Build metadata
│ │ │ │ └─ Pre-release version
│ │ │ └─ Bug fixes (compatible)
│ │ └─ Features (compatible)
│ └─ Breaking changes (incompatible)
└─────────────────────────────────────────────────────┘
Senior: "This is Semantic Versioning. Each number has clear meaning."
"Bumping MAJOR = Warning that existing code might break" "Bumping MINOR = New features, existing code safe" "Bumping PATCH = Only bug fixes, completely safe"
Me: "So user-auth-lib going from 2.8.1 → 3.0.0..."
Senior: "They renamed createUser() to registerUser(), or changed parameter order, or return type... Something in the API changed. That's why they bumped MAJOR."
That moment I understood: "Versions aren't numbers. They're promises. Contracts."vMAJOR.MINOR.PATCH
─┬─── ─┬─── ─┬───
│ │ └─ PATCH: Bug fixes (no API changes)
│ └────── MINOR: Features (existing API preserved)
└─────────── MAJOR: API changes (existing code may break)
UserAPI Library// UserAPI v1.0.0
export function getUser(id) {
return fetch(`/api/users/${id}`)
.then(res => res.json());
}
export function createUser(data) {
return fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
}).then(res => res.json());
}
User code:
import { getUser, createUser } from 'user-api';
getUser(123).then(user => console.log(user));
createUser({ name: 'John' }).then(user => console.log(user));
// UserAPI v1.0.1
// Fixed: Crash when ID is null → Added error handling
export function getUser(id) {
if (!id) {
return Promise.reject(new Error('ID is required')); // ← Fix
}
return fetch(`/api/users/${id}`)
.then(res => res.json());
}
export function createUser(data) {
if (!data || !data.name) {
return Promise.reject(new Error('Name is required')); // ← Fix
}
return fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
}).then(res => res.json());
}
Compatibility: Existing code works ✅
Users don't need to change anything.
// UserAPI v1.1.0
// New feature: Bulk user fetch
export function getUser(id) {
if (!id) {
return Promise.reject(new Error('ID is required'));
}
return fetch(`/api/users/${id}`)
.then(res => res.json());
}
export function createUser(data) {
if (!data || !data.name) {
return Promise.reject(new Error('Name is required'));
}
return fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
}).then(res => res.json());
}
// ← New functions! (existing functions unchanged)
export function getUsers(ids) {
return Promise.all(ids.map(id => getUser(id)));
}
export function deleteUser(id) {
return fetch(`/api/users/${id}`, { method: 'DELETE' })
.then(res => res.json());
}
Compatibility: Existing code works ✅
Users can use new functions if they want. Not required.
// UserAPI v2.0.0
// Breaking Change: Function name changes, async/await
// getUser → fetchUser (renamed!)
export async function fetchUser(id) { // ← Name changed!
if (!id) throw new Error('ID is required');
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// createUser → registerUser (renamed!)
export async function registerUser(data) { // ← Name changed!
if (!data || !data.name) throw new Error('Name is required');
const res = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
});
return res.json();
}
export async function fetchUsers(ids) {
return Promise.all(ids.map(id => fetchUser(id)));
}
export async function removeUser(id) { // ← deleteUser → removeUser
const res = await fetch(`/api/users/${id}`, { method: 'DELETE' });
return res.json();
}
Compatibility: Existing code breaks ❌
// Old code (v1.x)
import { getUser, createUser } from 'user-api'; // ← Error! Functions don't exist
getUser(123).then(user => console.log(user)); // ← getUser is not defined
Users must modify code:
// Updated code (v2.x)
import { fetchUser, registerUser } from 'user-api'; // ← Names changed
const user = await fetchUser(123); // ← Changed to async/await
console.log(user);
# package.json (day before deploy)
{
"dependencies": {
"axios": "^0.21.0" // ← Caret present
}
}
# npm install (on new server, deploy day)
npm install
# Result: package-lock.json
{
"axios": "1.0.0" // ← MAJOR jump from 0.x → 1.x!
}
Problem: axios 0.x → 1.x was a Breaking Change.
axios.get() response structure changedLesson: v0.x.y is considered unstable, so ^0.21.0 allows 1.0.0.
March 2016, an 11-line npm package called left-pad was deleted.
// Entire left-pad package (11 lines)
module.exports = leftpad;
function leftpad(str, len, ch) {
str = String(str);
ch = ch || ' ';
var i = -1;
len = len - str.length;
while (++i < len) {
str = ch + str;
}
return str;
}
But thousands of packages depended on it, including Babel.
npm install
# Error
npm ERR! 404 'left-pad' is not in the npm registry.
Result: Tens of thousands of projects worldwide using React and Babel failed to build simultaneously.
Causes:
Lesson: "One tiny dependency can bring down everything."
# package.json
{
"dependencies": {
"some-logging-lib": "~2.3.4" // ← Tilde: PATCH only
}
}
# npm update
npm update
# Result
{
"some-logging-lib": "2.3.5" // ← PATCH update
}
After update, production logs exploded:
[INFO] User logged in
[DEBUG] Database query: SELECT * FROM users WHERE id = 123
[DEBUG] Query time: 45ms
[DEBUG] Result: {...}
[INFO] Session created
...
Problem: v2.3.5 developer added "debug logging" as PATCH, but log volume increased 100x, filling disk.
Lesson: "Even PATCH can have side effects. Nothing is 100% safe."
^ (Most Common){
"dependencies": {
"react": "^18.2.0"
}
}
Rule: "Leftmost non-zero digit is fixed"
^18.2.0 allows:
✅ 18.2.0
✅ 18.2.1 (PATCH)
✅ 18.3.0 (MINOR)
✅ 18.99.99 (MINOR)
❌ 19.0.0 (MAJOR)
^0.5.2 allows (careful!):
✅ 0.5.2
✅ 0.5.3 (PATCH)
❌ 0.6.0 (Blocks MINOR too!)
❌ 1.0.0 (MAJOR)
^0.0.3 allows (strictest!):
✅ 0.0.3
❌ 0.0.4 (Blocks even PATCH!)
Why designed this way?
~ (Conservative){
"dependencies": {
"lodash": "~4.17.21"
}
}
Rule: "PATCH updates only"
~4.17.21 allows:
✅ 4.17.21
✅ 4.17.22 (PATCH)
✅ 4.17.99 (PATCH)
❌ 4.18.0 (MINOR)
❌ 5.0.0 (MAJOR)
~1.2 allows (PATCH omitted):
✅ 1.2.0
✅ 1.2.1
❌ 1.3.0
~1 allows (MINOR, PATCH omitted):
✅ 1.0.0
✅ 1.1.0 (MINOR)
✅ 1.99.99 (MINOR)
❌ 2.0.0 (MAJOR)
{
"dependencies": {
"critical-lib": "3.5.2" // ← Exactly 3.5.2 only
}
}
Allows: 3.5.2 only
When to use:
{
"dependencies": {
"pkg1": ">=1.2.0", // 1.2.0 or above
"pkg2": ">1.2.0", // Above 1.2.0
"pkg3": "<=2.0.0", // 2.0.0 or below
"pkg4": "<2.0.0", // Below 2.0.0
"pkg5": ">=1.0.0 <2.0.0" // 1.x versions only
}
}
||{
"dependencies": {
"pkg": "^1.0.0 || ^2.0.0" // 1.x or 2.x
}
}
Use case: Plugin supports two host versions
{
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0" // React 17 or 18 both OK
}
}
-{
"dependencies": {
"pkg": "1.2.0 - 1.5.0" // 1.2.0 ≤ version ≤ 1.5.0
}
}
Equivalent:
{
"dependencies": {
"pkg": ">=1.2.0 <=1.5.0"
}
}
{
"dependencies": {
"pkg1": "1.x", // >=1.0.0 <2.0.0 (1.x versions)
"pkg2": "1.2.x", // >=1.2.0 <1.3.0 (1.2.x versions)
"pkg3": "*" // >=0.0.0 (all versions) ← Dangerous!
}
}
| Notation | Safety | Update Range | Recommended When |
|---|---|---|---|
^1.2.3 | Medium | MINOR + PATCH | Most cases (npm default) |
~1.2.3 | High | PATCH only | Stability-critical production |
1.2.3 | Highest | No updates | Legacy, banking/healthcare |
>=1.2.3 | Low | All newer versions | Dev tools |
* | Dangerous | All versions | Never use |
^0.x.y | Careful | PATCH only (0.x special) | Unstable packages |
v1.0.0-alpha.1
v1.0.0-alpha.2
v1.0.0-beta.1
v1.0.0-beta.2
v1.0.0-rc.1 (Release Candidate)
v1.0.0 (Official release)
Meanings:
alpha: Early development, incomplete features, many bugsbeta: Features complete, testing, bugs possiblerc (Release Candidate): Release candidate, nearly complete, final testingVersion Precedence:
1.0.0-alpha.1 < 1.0.0-alpha.2 < 1.0.0-beta.1 < 1.0.0-rc.1 < 1.0.0
v1.0.0+20231115
v1.0.0+build.123
v1.0.0-beta.1+exp.sha.5114f85
Meaning: Build time, commit hash, etc. (doesn't affect version comparison)
1.0.0+build.1 == 1.0.0+build.2 (versions are identical)
# Actual React version examples
18.0.0-alpha-e6be2d531-20211019
18.0.0-beta-24dd07bd2-20211208
18.0.0-rc.0
18.0.0-rc.1
18.0.0 # Official release
18.0.1 # Patch
18.1.0 # Minor
18.2.0
# Official versions only
npm install react # → 18.2.0
# Include pre-releases
npm install react@next # → 19.0.0-rc.1 (latest beta)
# Specific pre-release
npm install react@18.0.0-rc.1
# Pin pre-release in package.json
{
"dependencies": {
"react": "18.0.0-rc.1" // ← Exactly this version
}
}
Note: Pre-releases not included in ^ or ~ ranges!
{
"dependencies": {
"react": "^18.0.0" // ← 18.0.0-rc.1 won't install!
}
}
Teammate A's computer (Nov 1, 2023):
npm install
# package.json
{
"dependencies": {
"axios": "^1.5.0"
}
}
# Installed version
axios 1.5.0
Teammate B's computer (Nov 15, 2023):
npm install # Same package.json
# Installed version
axios 1.6.0 # ← Minor update released!
Result: Works on A's computer, bugs on B's computer.
"Works on my machine?" crisis.
# package-lock.json (shared with team)
{
"name": "my-project",
"version": "1.0.0",
"lockfileVersion": 3,
"dependencies": {
"axios": {
"version": "1.5.0", # ← Exact version locked
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
"integrity": "sha512-..." # ← Checksum for tampering detection
}
}
}
Now Teammate B:
npm install # ← Reads package-lock.json
# Installed version
axios 1.5.0 # ← Same version as A!
| Tool | Lockfile | Characteristics |
|---|---|---|
| npm | package-lock.json | JSON format, low readability |
| yarn | yarn.lock | YAML format, high readability |
| pnpm | pnpm-lock.yaml | YAML, uses symlinks |
# Commit lockfile to git
git add package-lock.json
git commit -m "Lock dependencies"
# Use npm ci when lockfile exists (in CI/CD)
npm ci # ← Install exactly per package-lock.json
# Add lockfile to .gitignore (absolutely forbidden!)
echo "package-lock.json" >> .gitignore # ❌
# Install ignoring package-lock.json
npm install --no-package-lock # ❌
# Manually edit lockfile
vim package-lock.json # ❌ (npm manages it)
# Current version: 1.2.3
# Bump PATCH (1.2.3 → 1.2.4)
npm version patch
# Bump MINOR (1.2.3 → 1.3.0)
npm version minor
# Bump MAJOR (1.2.3 → 2.0.0)
npm version major
Actions:
version field in package.jsonv1.2.4 message)v1.2.4)# Current version: 1.2.3
# Bump to pre-release (1.2.3 → 1.2.4-0)
npm version prerelease
# Continue pre-release (1.2.4-0 → 1.2.4-1)
npm version prerelease
# Release official version (1.2.4-1 → 1.2.4)
npm version patch
# Current version: 1.2.3
# Alpha version (1.2.3 → 1.2.4-alpha.0)
npm version preminor --preid=alpha
# Beta version (1.2.4-alpha.0 → 1.3.0-beta.0)
npm version preminor --preid=beta
# RC version (1.3.0-beta.0 → 1.3.0-rc.0)
npm version prerelease --preid=rc
# Don't create git commit/tag
npm version patch --no-git-tag-version
# 1. Develop feature
git checkout -b feature/new-feature
# 2. Development complete, commit
git add .
git commit -m "feat: Add new feature"
# 3. Return to main branch
git checkout main
git merge feature/new-feature
# 4. Bump version (auto commit/tag)
npm version minor # → v1.3.0
# 5. Publish to npm
npm publish
# 6. Push to git (include tags)
git push origin main --tags
# Format
<type>(<scope>): <subject>
# Examples
feat(auth): Add login function # → Bump MINOR
fix(api): Fix null pointer bug # → Bump PATCH
feat(api)!: Change API signature # → Bump MAJOR
# Or
feat(api): Change API signature
BREAKING CHANGE: API signature changed # → Bump MAJOR
Type Categories:
feat: New feature → MINORfix: Bug fix → PATCHdocs: Documentation → No version bumpstyle: Code style → No version bumprefactor: Refactoring → PATCHperf: Performance → PATCHtest: Tests → No version bumpchore: Build config → No version bump# Install
npm install --save-dev standard-version
# Add script to package.json
{
"scripts": {
"release": "standard-version"
}
}
# Use
npm run release
Actions:
feat exists, bump MINOR; if only fix, bump PATCHBREAKING CHANGE exists, bump MAJORExample CHANGELOG.md:
# Changelog
## [2.1.0] (2023-11-15)
### Features
- **auth**: Add OAuth login ([a3f2c1b](link-to-commit))
- **api**: Support pagination ([8d9e4f7](link-to-commit))
### Bug Fixes
- **api**: Fix null pointer in getUser ([5c6d8a2](link-to-commit))
## [2.0.0] (2023-11-01)
### BREAKING CHANGES
- **api**: Remove deprecated createUser function
Migration: Use registerUser instead
### Features
- **api**: Add registerUser function
# Install
npm install --save-dev semantic-release
# Configure .releaserc.json
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer", // Analyze commits
"@semantic-release/release-notes-generator", // Generate release notes
"@semantic-release/changelog", // Generate CHANGELOG
"@semantic-release/npm", // Publish to npm
"@semantic-release/git", // Git commit/tag
"@semantic-release/github" // Create GitHub release
]
}
# Run in CI/CD
npx semantic-release
CI/CD Workflow (GitHub Actions):
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci
- run: npm test
# Auto version management & deployment
- run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Results:
feat: commit → CI auto-bumps MINOR and publishes to npmv2.3.4 = MAJOR.MINOR.PATCH
Pros:
Cons:
Examples: npm, pip, gems, most libraries
Ubuntu: YY.MM (e.g., 22.04, 23.10)
PyPI: YYYY.MM.MICRO (e.g., 2023.11.1)
Pros:
Cons:
Examples: Ubuntu, Windows (20H2, 21H1), pip itself
Windows 10 21H2 (2021 second half)
Windows 11 22H2 (2022 second half)
Chrome 119.0.6045.123
│ │ │ └─ Patch (bug fixes)
│ │ └─────── Build (auto build number)
│ └────────── Branch (dev branch)
└────────────── Major (feature changes)
Bumps MAJOR every 4-6 weeks (even without API changes).
iOS 17.1.1
│ │ └─ Patch (urgent bug fixes)
│ └─── Minor (feature additions)
└────── Major (major update, annual release)
| Scheme | Format | Pros | Cons | Examples |
|---|---|---|---|---|
| SemVer | MAJOR.MINOR.PATCH | Clear compatibility | Marketing disadvantage | npm, pip, gems |
| CalVer | YY.MM.MICRO | Easy timing | No compatibility info | Ubuntu, pip |
| Chrome-style | MAJOR.BRANCH.BUILD.PATCH | Fast releases | Confusing | Chrome, Edge |
| iOS-style | MAJOR.MINOR.PATCH | Intuitive | Different from SemVer | iOS, macOS |
v1.5.3 → v1.5.4 (PATCH)
// v1.5.3
function login(username, password) {
return fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
}
// v1.5.4
function login(email, password) { // ← username → email changed!
return fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
}
Result:
~1.5.3 (PATCH only)v2.3.0 → v2.4.0 (MINOR)
// v2.3.0
export function getUser(id) {
return fetch(`/api/users/${id}`)
.then(res => res.json());
}
// v2.4.0
export async function getUser(id) { // ← Sync → Async change!
const res = await fetch(`/api/users/${id}`);
return res.json();
}
Problem:
// User code (v2.3.0)
getUser(123).then(user => console.log(user)); // ← Works
// After v2.4.0 update
getUser(123).then(user => console.log(user)); // ← Still works, but...
// If user wrote this:
const user = getUser(123); // ← Returns Promise instead of undefined
console.log(user.name); // ← TypeError!
Correct way: Should bump MAJOR (2.3.0 → 3.0.0).
// v2.3.0
export function getUser(id) {
console.warn('getUser() is deprecated. Use fetchUser() instead.');
return fetchUser(id);
}
export function fetchUser(id) { // ← Add new function (MINOR)
return fetch(`/api/users/${id}`)
.then(res => res.json());
}
Timeline:
fetchUser(), keep getUser() (MINOR)getUser() deprecated warning (MINOR)getUser() (MAJOR)# v3.0.0 Migration Guide
## Breaking Changes
### 1. `getUser()` → `fetchUser()`
**Before (v2.x)**:
\`\`\`javascript
import { getUser } from 'my-lib';
getUser(123).then(user => console.log(user));
\`\`\`
**After (v3.x)**:
\`\`\`javascript
import { fetchUser } from 'my-lib';
const user = await fetchUser(123);
console.log(user);
\`\`\`
**Codemod (auto migration)**:
\`\`\`bash
npx @my-lib/codemod v2-to-v3
\`\`\`
### 2. Login API signature change
**Before**:
\`\`\`javascript
login(username, password)
\`\`\`
**After**:
\`\`\`javascript
login(email, password)
\`\`\`
* or latest{
"dependencies": {
"some-lib": "*" // ❌ Dangerous!
}
}
Problem: MAJOR updates install automatically.
Solution:
{
"dependencies": {
"some-lib": "^2.3.0" // ✅ Blocks MAJOR
}
}
{
"devDependencies": {
"eslint": "8.56.0", // ← Fixed version
"prettier": "3.1.0"
}
}
Problem: Dev tools update frequently, need manual management.
Solution:
{
"devDependencies": {
"eslint": "^8.56.0", // ✅ Allow Minor updates
"prettier": "^3.1.0"
}
}
{
"peerDependencies": {
"react": "18.2.0" // ❌ Only exactly 18.2.0
}
}
Problem: User with React 18.3.0 gets warnings.
Solution:
{
"peerDependencies": {
"react": "^18.0.0" // ✅ Allow all React 18.x
// Or
"react": "^17.0.0 || ^18.0.0" // ✅ Support both 17, 18
}
}
# .gitignore
node_modules/
package-lock.json # ❌ Absolutely forbidden!
Problem: Different team members install different versions → "Works on my machine?" crisis.
Solution:
# .gitignore
node_modules/
# Never add package-lock.json!
# Install
npm install -g npm-check-updates
# Check updatable versions
ncu
Checking package.json
[====================] 12/12 100%
axios ^1.5.0 → ^1.6.5 (Minor)
react ^18.2.0 → ^18.2.0 (Up to date)
lodash ^4.17.21 → ^4.17.21 (Up to date)
# Include MAJOR updates
ncu --target latest
react ^18.2.0 → ^19.0.0 (MAJOR! Careful!)
# Auto-update package.json
ncu -u
# Specific package only
ncu axios
npm outdated
Package Current Wanted Latest Location
axios 1.5.0 1.6.5 1.6.5 my-project
react 18.2.0 18.2.0 19.0.0 my-project
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
reviewers:
- "my-team"
labels:
- "dependencies"
Actions: Auto-creates PR weekly, shows updatable dependencies.
Stronger alternative to Dependabot:
// renovate.json
{
"extends": ["config:base"],
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"automerge": true // Auto-merge Minor/Patch
},
{
"matchUpdateTypes": ["major"],
"labels": ["major-update"],
"reviewers": ["team-lead"] // Major needs review
}
]
}
v0.x.y = "Initial development. Anything MAY change at any time."
Meaning: MAJOR of 0 means unstable.
React v0.14.0 → v0.15.0 (MINOR update)
// v0.14.0
React.render(<App />, document.body);
// v0.15.0
ReactDOM.render(<App />, document.body); // ← Breaking Change!
Problem: MINOR update but API completely changed!
{
"dependencies": {
"stable-lib": "^1.5.0", // Allows all 1.x
"unstable-lib": "^0.5.0" // Only 0.5.x (PATCH only!)
}
}
Reason: v0.x.y MINOR might be Breaking, so only PATCH allowed.
{
"dependencies": {
"unstable-lib": "0.5.2" // ← Fixed version
// Or
"unstable-lib": "~0.5.2" // ← PATCH only (explicit)
}
}
Are there changes?
├─ YES
│ ├─ Does existing code break? (Breaking Change)
│ │ ├─ YES → Bump MAJOR (1.0.0 → 2.0.0)
│ │ └─ NO
│ │ ├─ New features?
│ │ │ ├─ YES → Bump MINOR (1.0.0 → 1.1.0)
│ │ │ └─ NO
│ │ │ ├─ Bug fixes?
│ │ │ │ ├─ YES → Bump PATCH (1.0.0 → 1.0.1)
│ │ │ │ └─ NO → No bump (docs, tests only)
└─ NO → No bump
Any of these = MAJOR:
{
"dependencies": {
// Frameworks (critical): Fixed or tilde
"react": "18.2.0",
"vue": "~3.3.0",
// Utility libraries: Caret
"lodash": "^4.17.21",
"axios": "^1.6.0",
// v0.x.y (unstable): Fixed
"new-experimental-lib": "0.5.2",
// Plugins: OR range (support multiple host versions)
"eslint-plugin-react": "^7.0.0"
},
"devDependencies": {
// Dev tools: Caret (freely update)
"eslint": "^8.56.0",
"prettier": "^3.1.0",
"jest": "^29.7.0"
},
"peerDependencies": {
// Host library: Wide range
"react": "^17.0.0 || ^18.0.0"
}
}
When I started learning version management, I thought "It's just numbers, right?"
After blowing up production, I realized.
"Versions aren't numbers. They're trust protocols between developers."Going from v2.3.4 to v2.3.5 isn't just incrementing. It's a promise: "This update is safe. Your code will still work."
Going from v2.3.4 to v3.0.0 is a warning: "Be careful. You might need to modify code."
Lessons learned:
If I ever build a library, I'll follow SemVer rigorously.
Because I don't want to ruin someone's Friday night.
"Trust builds slowly, but one Breaking Change destroys it."If you build libraries, bump version numbers carefully.
Put responsibility into each digit.
That's the minimum courtesy to maintain as a developer.