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 →