How to Write Conventional Commit Messages — A Practical Guide
"fixed stuff", "updates", "wip" — if your git log looks like this, you are not alone. Most developers write commit messages as an afterthought, until the day they need to find when a bug was introduced, generate a changelog, or figure out what actually shipped in the last release. Conventional Commits is a lightweight specification that fixes this by giving every commit message a predictable structure. This guide covers the exact spec, the standard commit types, how to write in imperative mood, and how tools like semantic-release use this format to automatically version your releases.
Why Commit Message Format Matters
A commit message is not just a note to yourself — it is a permanent, searchable record of why the codebase changed, and it has three real audiences beyond the person writing it:
- Your teammates — reading
git logor a pull request diff should tell them what changed and why, without opening every file - Future you — six months from now,
git blameon a confusing line should lead to a message that explains the reasoning, not just "fix" - Automated tooling — changelog generators and release automation tools like semantic-release parse your commit messages to decide what goes in the changelog and what version number to publish next
A consistent format turns your commit history from a diary into structured data that both humans and tools can rely on.
The Conventional Commits Spec
The format is simple: a type, an optional scope in parentheses, a colon, and a short description.
type(scope): short description [optional longer body] [optional footer(s)]
Real examples:
feat(auth): add password reset via email fix(api): handle null response from payment gateway docs(readme): add local setup instructions chore(deps): bump next from 14.1 to 14.2
The scope is optional and describes what part of the codebase the change touches — a module name, a folder, a feature area. Skip it if the change is broad or the project is small.
The 8 Standard Commit Types
Type Use for --------- ------------------------------------------------ feat A new feature for the user fix A bug fix for the user docs Documentation changes only style Formatting, whitespace, semicolons — no logic change refactor Code change that neither fixes a bug nor adds a feature perf A change that improves performance test Adding or correcting tests chore Build process, tooling, dependency updates, config
feat and fix are the two types that directly drive semantic versioning — the other six matter for changelog organization and searchability but do not trigger a version bump on their own.
Write in Imperative Mood
The description should complete the sentence "If applied, this commit will ___________." That means imperative present tense — "add", "fix", "remove" — not past tense or a description of what you did.
Wrong: fix(api): fixed the bug where users couldn't log in Wrong: fix(api): fixes login bug Right: fix(api): resolve login failure for OAuth users Wrong: feat(cart): added a discount code field Right: feat(cart): add discount code field to checkout
This matches the convention git itself uses for auto-generated messages (like "Merge branch..."), and it reads naturally in a changelog: "This release will add discount code field to checkout" flows better than "This release will added a discount code field."
Good vs Bad Commit Messages
Bad: "stuff" Bad: "fix bug" Bad: "update code" Bad: "asdasd wip" Good: fix(cart): prevent negative quantity in cart update Good: feat(export): add CSV export for order history Good: perf(search): cache tag lookup to cut query time by 60% Good: refactor(auth): extract token validation into helper
The difference is specificity. A good message tells you the type of change, the area it touches, and the effect — you should rarely need to open the diff just to understand what a commit did.
How semantic-release Uses This Format
Tools like semantic-release read your commit history since the last release and decide the next version number automatically, based entirely on the commit types used:
fix:commits trigger a patch release (1.0.0 → 1.0.1)feat:commits trigger a minor release (1.0.0 → 1.1.0)- A
BREAKING CHANGEfooter (or!after the type) triggers a major release (1.0.0 → 2.0.0) docs,style,chore, andtestcommits do not trigger a release on their own
This means once your team commits to the format, you never have to manually decide "is this a minor or a patch release" again — the answer is derived directly from your commit log, and the changelog is generated from the same messages.
Breaking Change Footer Syntax
There are two ways to mark a breaking change, and you can use either or both:
feat(api)!: remove support for API key auth BREAKING CHANGE: API key authentication has been removed. Use OAuth 2.0 tokens instead. Existing API keys will stop working after this release.
The ! right after the scope (or type, if there is no scope) is a quick visual flag in the git log. The BREAKING CHANGE: footer in the commit body is what tooling actually parses to write the "Breaking Changes" section of your changelog and to force a major version bump — always include both for anything that genuinely breaks a public API or contract.
Frequently Asked Questions
It is a specification for commit messages in the form type(scope): description, using standard types like feat, fix, docs, and chore, so history is readable by humans and parseable by tools.
Yes. The Dev Brains AI Commit Message Generator turns a plain English description of your change into a properly formatted commit message, for free.
Add ! after the type or scope (e.g. feat!:) and include a BREAKING CHANGE: footer describing the change. Tools like semantic-release use this to trigger a major version bump.