Conventional Commits: Writing Structured Git History

Conventional Commits: Writing Structured Git History Conventional Commits is a specification for writing consistent, machinereadable commit messages. It gi…

Conventional Commits: Writing Structured Git History

Conventional Commits is a specification for writing consistent, machine-readable commit messages. It gives every commit a clear purpose at a glance — and unlocks automation on top of your git history.


The Format

<type>(<scope>): <short summary>
PartRequiredDescription
typeYesWhat kind of change this is
scopeNoWhat area it touches (e.g. navbar, auth, api)
summaryYesImperative, lowercase, no trailing period

Imperative means write it like a command: add, fix, remove — not added, fixing, removes.


Commit Types

TypeWhen to use
featNew feature added
fixBug fix
refactorCode restructure — no new feature, no bug fix
styleFormatting, whitespace, missing semicolons — no logic change
docsDocumentation only (README, comments, changelogs)
testAdding or fixing tests
choreTooling, deps, config (e.g. updating package.json)
perfPerformance improvement
ciCI/CD pipeline changes (GitHub Actions, etc.)
buildBuild system changes (webpack, next.config, etc.)
revertReverts a previous commit

Real-World Examples

feat(auth): add Google OAuth login
fix(navbar): correct active link highlight on mobile
refactor(theme): replace hardcoded hex values with CSS tokens
chore(deps): upgrade MUI to v6
perf(images): convert banners to next/image with lazy loading
docs: add setup instructions to README
style: format components with prettier
test(api): add integration tests for /users endpoint
ci: add lint step to GitHub Actions workflow
build: migrate from webpack to Vite
revert: revert "feat(auth): add Google OAuth login"

Breaking Changes

When a commit introduces a change that breaks the existing API or contract, signal it in one of two ways.

Option 1 — Add ! after the type:

feat(api)!: remove deprecated /v1 endpoints

Option 2 — Add a BREAKING CHANGE: footer in the commit body:

feat(api): remove deprecated /v1 endpoints

BREAKING CHANGE: The /v1/users and /v1/posts endpoints have been removed.
Migrate to /v2/users and /v2/posts respectively.

Both approaches cause tools like semantic-release to bump the major version number automatically.


Scopes in Practice

The scope is optional but adds valuable context in larger codebases:

fix(auth): handle expired JWT tokens
fix(cart): prevent duplicate item addition
fix(checkout): round currency values correctly

Without scopes these would all just read fix: ... — harder to scan at a glance.

Agree on scopes with your team and keep them consistent. Common patterns:

  • Feature areas: auth, dashboard, billing

  • Layers: api, db, ui

  • Packages: core, utils, cli (monorepos)


Why Bother?

Changelogs write themselves. Tools like conventional-changelog parse your history and generate a CHANGELOG.md automatically — grouped by type, filtered to user-facing changes.

Automatic version bumps. semantic-release reads your commits and decides whether to cut a patch, minor, or major release:

Commit typeVersion bump
fixPatch 1.0.x
featMinor 1.x.0
feat! / BREAKING CHANGEMajor x.0.0

Readable git log. Compare:

# Without conventions
update stuff
fix
wip
changes
finally working

# With conventions
feat(auth): add Google OAuth login
fix(navbar): correct active link highlight on mobile
refactor(theme): replace hardcoded hex values with CSS tokens
chore(deps): upgrade MUI to v6

The second log tells you exactly what shipped, where, and why — without opening a single file.

Teammates (and future you) understand instantly. A refactor: commit signals "nothing changed for users — safe to skip in a hotfix cherry-pick." A feat: signals "new surface area — worth a closer review."


Quick Reference

# New feature
git commit -m "feat(search): add keyboard shortcut to open search modal"

# Bug fix
git commit -m "fix(form): prevent double submission on slow connections"

# No user impact
git commit -m "chore: update ESLint config to flat format"

# Breaking change
git commit -m "feat(api)!: require authentication on all /admin routes"

Keep summaries under 72 characters so they fit cleanly in git log --oneline output.