Why Structured Commit Messages?
Conventional Commits enables three key things:
- Automated changelogs — tools like
conventional-changelogandsemantic-releasecan generateCHANGELOG.mdautomatically by parsing commit messages - Semantic version bumps —
featcommits trigger a minor bump,fixtriggers a patch bump,BREAKING CHANGEtriggers a major bump — no more manual version decisions - Readable history — a developer reading
git log --onelineimmediately understands what type of change each commit was
The Format
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
- type — required. What kind of change is this? (see table below)
- scope — optional. What part of the codebase? (
auth,api,ui,docs) - description — required. Imperative mood, present tense, no capital first letter, no period at end. Max ~72 chars.
- body — optional. Explain what and why (not how). Separate from subject with blank line.
- footer — optional. Breaking changes, issue references.
feat(auth): add OAuth2 login with GitHub
fix(api): handle null response from user endpoint
docs(readme): update installation instructions for Node 20
feat!: remove support for Node 14
refactor(dashboard): extract chart logic into separate component
perf(images): lazy-load images below the fold
Commit Types Reference
| Type | When to use | SemVer impact |
|---|---|---|
feat | A new feature for the user | MINOR bump |
fix | A bug fix for the user | PATCH bump |
docs | Documentation only changes | No bump |
style | Formatting, whitespace (no logic change) | No bump |
refactor | Code restructure (not a fix or feature) | No bump |
perf | Performance improvement | PATCH bump |
test | Adding or fixing tests | No bump |
chore | Build process, dependency updates, tools | No bump |
ci | CI/CD configuration changes | No bump |
build | Changes affecting build system or dependencies | No bump |
revert | Reverts a previous commit | Depends |
Breaking Changes
A breaking change is any change that requires users of your library/API to update their code. Mark it with:
- ! after the type:
feat!: rename API endpoint— triggers MAJOR bump - BREAKING CHANGE footer: detailed explanation of what broke and how to migrate
git commit -m "feat!: migrate authentication to JWT tokens
BREAKING CHANGE: The /api/login endpoint now returns a JWT token
instead of a session cookie. Update your API client to store the
token and send it as a Bearer header:
Authorization: Bearer <token>
Old behavior (sessions) is no longer supported.
Closes #142"
Real-World Examples
| Commit message | What it communicates |
|---|---|
feat(cart): add quantity selector to cart items | New feature in the cart module |
fix(auth): prevent redirect loop on expired token | Bug fix in auth, will be in next patch release |
chore(deps): upgrade react from 18.2 to 18.3 | Dependency update, no user-visible change |
docs(api): add examples to user endpoint docs | Documentation only, no code change |
perf(db): add index on users.email column | Performance improvement |
test(checkout): add integration tests for payment flow | Tests added, no production code change |
refactor(utils): extract date formatting to shared helper | Code quality improvement, no behavior change |
feat!: remove deprecated v1 API endpoints | Breaking change — MAJOR version bump required |
Tooling
# commitlint: Validates commit messages against Conventional Commits
npm install --save-dev @commitlint/cli @commitlint/config-conventional
# commitlint.config.js
echo "module.exports = { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
# Add to Husky pre-commit
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
# commitizen: Interactive CLI commit helper
npm install --save-dev commitizen
npx commitizen init cz-conventional-changelog --save-dev --save-exact
# Now use: git cz instead of git commit
# It prompts you through type, scope, description, body, footer
# semantic-release: Fully automated version management
npm install --save-dev semantic-release
# On every push to main, it:
# 1. Analyzes commits since last release
# 2. Determines the next version number
# 3. Generates CHANGELOG.md
# 4. Creates a GitHub release + git tag
# 5. Publishes to npm (if configured)
Not every developer writes perfect conventional commits on their feature branch — and that's okay. Many teams allow free-form commits during development and only require a conventional commit message for the squash commit when merging the PR. Configure GitHub to default to "Squash and merge" and set the PR title to be a conventional commit message.
📋 Summary
- Format: type(scope): description — e.g.,
feat(auth): add OAuth2 login - Core types: feat (MINOR), fix (PATCH), docs/style/refactor/test/chore (no bump)
- Breaking change: add ! after type or BREAKING CHANGE: footer — triggers MAJOR bump
- Description: imperative mood, present tense, lowercase start, no trailing period, max ~72 chars
- commitlint validates format; commitizen provides interactive CLI; semantic-release automates versioning
- Practical team approach: require conventional format for squash commit at PR merge, not for every WIP commit
FAQ
No. The scope is optional but recommended for projects with multiple subsystems. Without scope: feat: add dark mode. With scope: feat(ui): add dark mode toggle. The scope should be consistent across the team — agree on a set of scopes in your CONTRIBUTING.md (e.g., auth, api, ui, db, docs) rather than having each developer invent their own.
fix corrects behavior that was wrong — a user-visible bug. refactor changes the code's internal structure without changing its external behavior. If a test was failing and now passes, that's a fix. If you extracted a function and all tests still pass identically, that's a refactor. The distinction matters for semantic versioning: fix triggers a PATCH version bump while refactor does not.
Use the type that best represents the primary change. If you added a feature and documented it in the same commit, use feat — the documentation is secondary. However, it's generally better practice to make separate commits: one feat commit for the code and one docs commit for the documentation. Atomic commits (one logical change per commit) make history cleaner and easier to revert if needed.
Yes — even for solo projects. The discipline of writing good commit messages forces you to think about what each commit actually does. Six months later when you're debugging an old project, a well-written git log is incredibly valuable. It also builds the muscle memory so it becomes second nature when you join a team or contribute to open source. Start with just feat, fix, and chore — you don't need all 11 types immediately.