May 14, 20253 min read

Conventional Commits: how to write commit messages that are clear, useful and machine-readable

Visual example of Conventional Commits with structured messages and Git branches. Title: How to write clear, useful commit messages, by Efeele

Having a convention for commit messages makes a project’s history much easier to read, and it also lets you automate things like the changelog, versioning or deploys. Here’s the Conventional Commits 1.0.0 spec, explained so you can start using it right away.

The shape of a commit message

<type>[optional scope]: <description>

[optional body]

[optional footer]

Examples

feat: add filter-based search
fix: correct validation error on the form
feat(api)!: drop support for v1
docs: update the install section in the README

The main types

  • feat: adds a new feature (a minor version in SemVer).
  • fix: fixes a bug (a patch version in SemVer).
  • BREAKING CHANGE: breaks backwards compatibility (a major version in SemVer).

There are two ways to flag a breaking change:

  • In the footer: BREAKING CHANGE: Node 18 is now required
  • Or with a ! after the type or scope: feat!: drop legacy support

Other common types

Borrowed from the Angular convention:

  • build: changes to the build setup or external tooling.
  • ci: changes to continuous integration.
  • docs: documentation only.
  • style: formatting, whitespace, semicolons, nothing that changes behavior.
  • refactor: restructuring without changing what the code does.
  • perf: performance improvements.
  • test: adding or fixing tests.
  • chore: small housekeeping with no functional impact (bumping dependencies, for instance).
  • revert: undoing a previous commit.

Style rules

  • Write in the imperative: add, fix, remove.
  • No period at the end of the subject line.
  • Keep the subject short, 50 characters at most.
  • If it needs more context, put it in the body.
  • Use scopes when they add clarity: feat(api), fix(auth).

What it’s for

Besides a history that’s easier to read, the convention lets tools do the work for you: automatic changelogs with conventional-changelog, versions and releases with semantic-release, and message validation with commitlint. I explain how to set all that up in From commit to changelog.

Tools

commitlint

Checks that your commits actually follow the convention.

npm install --save-dev @commitlint/cli @commitlint/config-conventional
echo 'npx --no -- commitlint --edit $1' > .husky/commit-msg

Commitizen

An interactive prompt that walks you through writing a well-formed commit.

npm install -g commitizen
npx commitizen init cz-conventional-changelog --save-dev --save-exact

Husky

Runs scripts before commits and pushes: validation, tests, whatever you need.

npm install --save-dev husky
npx husky init

husky init creates the .husky/ folder and adds the prepare script to package.json, so run it before setting up commitlint.

Common questions

What if I use the wrong type?

  • If you haven’t pushed it yet, git rebase -i lets you edit the message.
  • If you already pushed it, nothing serious happens: you just lose a bit of automation, like that change not showing up in the changelog.

Can I invent my own types?

You can. Just remember that only feat, fix and BREAKING CHANGE feed directly into semantic versioning. Everything else is yours to define.

Does the whole team have to follow it?

Not necessarily. If you work with pull requests and squash & merge, you can clean up the final message at merge time without asking everyone to change how they commit.

Two books on Git and GitHub I recommend (both in Spanish)

Resources

Share