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 log or a pull request diff should tell them what changed and why, without opening every file
  • Future you — six months from now, git blame on 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 CHANGE footer (or ! after the type) triggers a major release (1.0.0 → 2.0.0)
  • docs, style, chore, and test commits 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

What is the Conventional Commits format?

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.

Is there a free tool to generate Conventional Commit messages?

Yes. The Dev Brains AI Commit Message Generator turns a plain English description of your change into a properly formatted commit message, for free.

How do I indicate a breaking change in a Conventional Commit?

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.

Try the Free Commit Message Generator

Describe your change in plain English and get a properly formatted Conventional Commit message instantly. No signup, no cost.

Related articles