Quick Answer

Conventional Commits is a lightweight convention for commit messages: a type such as feat or fix, an optional scope in parentheses, then a colon and a short description. It makes history easy to scan and lets tools derive changelogs and the next version number automatically. feat maps to a minor release, fix to a patch, and a breaking change to a major one.

The format

Every Conventional Commit follows one shape:

<type>(<optional scope>): <description>

[optional body]

[optional footer(s)]

A real example:

feat(auth): add OTP login for students

Sends a 6-digit code valid for 10 minutes. Falls back to
email if the SMS provider returns an error.

Refs: PRIO-482

The first line is the only required part. Per the v1.0.0 spec the type is a noun, followed by an optional scope, an optional !, and a required colon and space; the description then follows immediately. A body, if present, starts one blank line after the description. Footers come one blank line after the body, each a token such as Refs or Reviewed-by that uses hyphens in place of spaces.

Footers carry metadata tooling reads: Co-authored-by: for shared work, a review sign-off, or an issue link. The one footer with special meaning is BREAKING CHANGE:, covered below. Everything past the first line is optional, so the smallest valid commit is just fix: correct typo in login copy.

The types

The spec mandates only two types and borrows the rest from the Angular convention:

  • feat is a new feature. It correlates with a MINOR bump under SemVer.
  • fix is a bug fix. It correlates with a PATCH bump.
  • docs, style, refactor, perf, test, build, ci, chore cover everything else. revert is a common addition.

A scope in parentheses names the area of the codebase touched, for example fix(parser):. It is optional but pays off in a repo with several areas. Keep the description short and imperative, add rather than added or adds, with no trailing full stop by convention.

The spec says implementations MUST NOT treat any of this as case-sensitive, with one exception: the literal text BREAKING CHANGE must be uppercase. Teams still tend to pick lowercase types and stay consistent. Note that only feat and fix are required by the spec itself; the wider list is convention, and commitlint presets enforce a specific set.

Signalling breaking changes

A breaking change is any commit that forces consumers to change their code. The spec gives two ways to flag it, and you may use both:

  • Put a ! immediately before the colon: feat(api)!: drop v1 payment endpoints.
  • Add a footer beginning with the uppercase token BREAKING CHANGE: followed by what broke and how to migrate.
feat(api)!: drop v1 payment endpoints

BREAKING CHANGE: clients must move to /v2/payments.
The v1 routes now return 410 Gone.

Either signal bumps the major version, whatever the type: a fix! is still a major release. If you use !, the footer may be omitted and the description carries the explanation. A lowercase breaking change: does not count; the spec requires the exact uppercase token.

You can also use the footer alone with no !, which is handy when the break is buried in an otherwise ordinary commit. The spec makes BREAKING-CHANGE with a hyphen an exact synonym, since some tools dislike spaces in footer tokens.

What it buys you

The payoff is automation plus readability:

  • Generated changelogs. Tools like conventional-changelog and release-please group commits by type into a CHANGELOG with nobody writing it by hand.
  • Automatic version bumps. semantic-release reads the commits since the last tag: a feat cuts a minor release, a BREAKING CHANGE cuts a major one. No human picks the number.
  • Scannable history. git log --oneline becomes a list you can read, and git log --grep narrowed to a type pulls out just the features or just the fixes.
  • Review signal. A refactor touching 30 files is a different review from a feat of the same size.

It also lowers the cost of writing a message. The type prefix is a prompt: you decide up front whether this is a feature, a fix, or housekeeping, which tends to produce a sharper description than a blank line would. The cost is close to zero once it is a habit, and a commit hook keeps the whole team aligned.

Enforcing with commitlint

A convention decays without a check. commitlint validates messages against the spec. Install it and point a config at the conventional preset:

npm i -D @commitlint/cli @commitlint/config-conventional

// commitlint.config.cjs
module.exports = { extends: ['@commitlint/config-conventional'] };

Piping a good message and two bad ones through it, real output:

$ echo 'feat(auth): add OTP login' | npx commitlint
  (exit 0, no output)

$ echo 'fixed some stuff' | npx commitlint
[FAIL] subject may not be empty [subject-empty]
[FAIL] type may not be empty [type-empty]

$ echo 'feature: add dark mode' | npx commitlint
[FAIL] type must be one of [build, chore, ci, docs, feat, fix,
       perf, refactor, revert, style, test] [type-enum]

That last one is the classic slip: feature instead of feat. Wire commitlint into a commit-msg Git hook with Husky so a bad message is rejected before it can land:

npm i -D husky
npx husky init
# .husky/commit-msg
npx --no -- commitlint --edit $1

Teams that want help writing conforming messages add commitizen, which turns git cz into a guided prompt.

Frequently Asked Questions

Do I have to use the scope? No, the scope is optional. Add it when your repository has distinct areas and knowing which one changed is useful, for example api, ui, or auth.
Which types trigger a version bump? By convention only feat (minor) and fix (patch) bump the version, plus any commit with a breaking change (major). Types like docs and chore usually produce no release.
How do I mark a breaking change? Put a ! before the colon in the header, or add an uppercase BREAKING CHANGE: footer, or both. Either one causes a major version bump.
What if I write a bad commit message anyway? With a commitlint commit-msg hook the commit is blocked locally. Without a hook, a CI check on the pull request can still catch it before merge.
Can I use Conventional Commits without semantic-release? Yes. The convention is useful on its own for readable history and manual changelog generation. The automation tools are optional add-ons.