Conventional commits cheat sheet (with real examples)
Conventional commits is a small format that makes Git history readable, enables automated changelog tools, and signals to senior engineers that you know what you're doing. Bookmark this page — you'll come back to it.
The format in one line
<type>(<optional scope>): <short description>
[optional body]
[optional footer]
Examples:
feat: add password reset flow
fix(auth): use env secret instead of hardcoded JWT key
chore(deps): upgrade React to 18.3
docs: clarify env var setup in README
refactor(api): extract user-fetching into a helper
test: add coverage for null-email case
That's the whole format. The next section is the cheat sheet of types.
The complete type reference
| Type | Use when… | Example |
|---|---|---|
feat | You added a new user-facing feature | feat: add CSV export to dashboard |
fix | You fixed a bug | fix: prevent crash when user has no avatar |
chore | Maintenance — dependency upgrades, tooling, repo housekeeping | chore: upgrade ESLint to 9.0 |
docs | Documentation only — README, comments, JSDoc | docs: add API rate-limit table |
refactor | Restructuring code without changing behavior | refactor: extract retry logic into helper |
test | Adding or changing tests only | test: cover edge cases in date parser |
perf | Performance improvements with no behavior change | perf: cache user lookups for 5 minutes |
build | Build system, package config, bundler | build: switch from webpack to vite |
ci | CI/CD config — GitHub Actions, GitLab CI, etc. | ci: run tests on Node 20 and 22 |
style | Formatting only — whitespace, semicolons, prettier | style: run prettier on src/ |
revert | Reverting a previous commit | revert: feat: add CSV export |
The hard part: choosing between feat, fix, chore, and refactor
Most beginners get tangled here. The decision tree:
- Did the user-visible behavior change? If yes, it's
feat(added something) orfix(corrected something broken). - Did internal code change but behavior is identical? It's
refactor. - Did nothing in the application change but config/tooling did? It's
choreorbuildorci.
Examples of the same change with different correct types:
feat: add dark mode toggle— user can now toggle dark mode (new behavior).fix: dark mode toggle doesn't persist across reloads— existing feature was broken.refactor: extract dark mode logic into useTheme hook— same behavior, cleaner code.chore: rename dark-mode files to lowercase— neither user nor code logic changed.
The optional scope
Scope tells reviewers where the change happened. It's optional but useful in larger codebases:
feat(auth): add password reset flow
fix(api): return 400 instead of 500 on null email
chore(deps): upgrade React to 18.3
docs(readme): document env var setup
Common scopes: auth, api, ui, db, deps, the name of a specific module. Whatever your team uses. Don't invent new scopes — match what's in the repo's history.
Breaking changes
If your change breaks backward compatibility, mark it with ! after the type:
feat!: rename `userId` to `user_id` in API responses
BREAKING CHANGE: All API consumers must update field name.
The ! tells everyone "this will require coordinating with consumers." The BREAKING CHANGE: footer gives details. Don't sneak breaking changes into fix or refactor commits — your downstream consumers will hate you.
The body (when to add one)
Most commits don't need a body. Add one when:
- The why isn't obvious from the description
- You made a non-obvious tradeoff
- You want to reference a ticket or issue
fix: cache user lookups for 5 minutes
We were making a DB query on every request, which was fine
at 10 req/s but starting to show in p99 latency at 200 req/s.
Five minutes was chosen because user role changes are rare
and the existing /users PATCH already invalidates the cache.
Closes IQ-142.
Real-world commit examples (good vs. bad)
Bad commits, and why
"updates"— describes nothing"WIP"— should never reach main; squash before merging"fixed bug"— which bug? In what file? What was the fix?"updated user.py"— describes the file, not the change"feat: lots of changes to auth and api and ui"— too broad; split into multiple commits"fix: typo"— okay if it's literally a typo, but in commit messages a scope helps:fix(readme): typo in install command
Good commits, and why
fix(auth): return 401 instead of 500 on expired token— clear scope, clear before/afterfeat: add dark mode toggle to settings page— what was added, wherechore(deps): upgrade React from 18.2 to 18.3— exact versions, easy to grep laterrefactor(api): extract retry logic into withRetry helper— what moved, into whatperf(db): add index on users.email— what was changed, what it affects
Why bother with all this
Three concrete reasons, in order of how much you'll feel them:
- Senior engineers will respect you instantly. A clean commit history signals that you understand professional norms. It's a 30-second cost that pays back forever.
- Automated tools work. Tools like
standard-version,semantic-release, andchangesetsread your commits to auto-generate changelogs and bump versions. You don't have to write release notes — they write themselves. - Future-you can debug faster. Six months from now, you'll
git log --grep="auth"looking for when a bug was introduced. With conventional commits, you find it in 10 seconds. Without, you read a thousand lines of "updates" and "wip" and want to quit.
The two-line rule
- Subject line under 72 characters, lowercase, no period.
- If you need a body, leave one blank line, then write it. Wrap at 72 characters.
That's it. You now know more about commit messages than most third-year engineers.
Practice writing real commits
InternQuest gives you 150+ real engineering tickets where you fix a bug, write a conventional commit message, and open a PR — same flow as a professional team. Free.
Start your first mission →