Ad – 728×90
🔀 Workflows

Conventional Commits – Writing Structured Commit Messages

Bad commit messages like "fix stuff" or "WIP" make your Git history useless for future debugging and code archaeology. Conventional Commits is a simple specification that structures commit messages so machines can parse them (for automated changelogs and version bumps) and humans can scan them quickly. It takes two minutes to learn and pays dividends for the entire life of a project.

⏱️ 15 min read 🎯 Beginner 📅 Updated 2026

Why Structured Commit Messages?

Conventional Commits enables three key things:

  1. Automated changelogs — tools like conventional-changelog and semantic-release can generate CHANGELOG.md automatically by parsing commit messages
  2. Semantic version bumpsfeat commits trigger a minor bump, fix triggers a patch bump, BREAKING CHANGE triggers a major bump — no more manual version decisions
  3. Readable history — a developer reading git log --oneline immediately understands what type of change each commit was

The Format

Commit message 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.
Examples
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

TypeWhen to useSemVer impact
featA new feature for the userMINOR bump
fixA bug fix for the userPATCH bump
docsDocumentation only changesNo bump
styleFormatting, whitespace (no logic change)No bump
refactorCode restructure (not a fix or feature)No bump
perfPerformance improvementPATCH bump
testAdding or fixing testsNo bump
choreBuild process, dependency updates, toolsNo bump
ciCI/CD configuration changesNo bump
buildChanges affecting build system or dependenciesNo bump
revertReverts a previous commitDepends
Ad – 336×280

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
Bash
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 messageWhat it communicates
feat(cart): add quantity selector to cart itemsNew feature in the cart module
fix(auth): prevent redirect loop on expired tokenBug fix in auth, will be in next patch release
chore(deps): upgrade react from 18.2 to 18.3Dependency update, no user-visible change
docs(api): add examples to user endpoint docsDocumentation only, no code change
perf(db): add index on users.email columnPerformance improvement
test(checkout): add integration tests for payment flowTests added, no production code change
refactor(utils): extract date formatting to shared helperCode quality improvement, no behavior change
feat!: remove deprecated v1 API endpointsBreaking change — MAJOR version bump required

Tooling

Bash
# 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)
💡
Team tip: squash and rewrite at PR merge time

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

Is the scope required in Conventional Commits? +

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.

What's the difference between fix and refactor? +

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.

What if I need to update both code and docs in the same commit? +

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.

Should I use Conventional Commits for personal projects? +

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.