Conventional Commits

What are Conventional Commits?

Conventional Commits is a specification for writing clear, structured commit messages that follow a consistent format. It’s a lightweight convention that sits on top of Git commit messages, making your project history readable, searchable, and automatable.

Instead of commit messages like this:

fixed stuff
updated code
changes
WIP
asdfasdf

You write messages like this:

feat: add user authentication endpoint
fix: resolve null reference in product service
docs: update API documentation for orders endpoint
refactor: simplify database connection logic

The format is simple:

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

[optional body]

[optional footer]

Why Use Conventional Commits?

1. Readable History

Compare these two git logs:

Without convention:

commit abc123
fixed bug

commit def456
updates

commit ghi789
more changes

commit jkl012
stuff

With Conventional Commits:

commit abc123
fix: resolve database connection timeout in product repository

commit def456
feat: add pagination support to GET /api/products

commit ghi789
refactor: extract validation logic into separate service

commit jkl012
docs: add API usage examples to README

Which one tells you what actually changed?

2. Automated Changelog Generation

With conventional commits, tools can automatically generate changelogs:

## Version 2.1.0 (2024-03-15)

### Features
- add user authentication endpoint
- add pagination support to products API
- implement search functionality

### Bug Fixes
- resolve database connection timeout
- fix null reference in order service
- correct validation error messages

### Documentation
- update API documentation
- add usage examples

3. Semantic Versioning

Conventional commits can automatically determine version bumps:

  • feat: → Minor version bump (1.0.0 → 1.1.0)
  • fix: → Patch version bump (1.0.0 → 1.0.1)
  • BREAKING CHANGE: → Major version bump (1.0.0 → 2.0.0)

4. Better Collaboration

Team members instantly understand what changed:

feat: add OAuth2 authentication
  ↓
"Oh, a new feature was added"

fix: resolve memory leak in background service
  ↓
"A bug was fixed"

refactor: simplify order processing logic
  ↓
"Code was improved but no behavior changed"

The Format

Basic Structure

<type>: <description>

The most common format is just type and description:

feat: add product search functionality
fix: resolve null pointer exception in user service
docs: update installation instructions

With Scope (Optional)

Add a scope to specify what part of the codebase changed:

feat(auth): add JWT token validation
fix(api): resolve CORS configuration issue
refactor(database): optimize query performance
test(orders): add unit tests for order processing

With Body (Optional)

Add more details in the body:

feat: add user notification system

Implemented email and SMS notifications for order updates.
Users can configure their notification preferences in settings.
Notifications are sent asynchronously using background jobs.

Add metadata like issue references or breaking changes:

fix: resolve database connection pooling issue

The connection pool was not properly releasing connections,
causing the application to hang under high load.

Fixes #123

Breaking Changes

Mark breaking changes explicitly:

feat!: change API response format

BREAKING CHANGE: All API responses now return data in a 
consistent envelope format with 'data', 'success', and 'errors' fields.
Clients must update to handle the new response structure.

Commit Types

Here are the standard types:

feat

A new feature for the user:

feat: add password reset functionality
feat: implement real-time notifications
feat: add export to CSV feature

fix

A bug fix:

fix: resolve authentication token expiration issue
fix: correct calculation in order total
fix: prevent duplicate email sends

docs

Documentation changes only:

docs: add API authentication guide
docs: update README with deployment instructions
docs: fix typos in contributing guidelines

style

Code style changes (formatting, missing semi-colons, etc.) - no code logic changes:

style: format code with Prettier
style: fix indentation in ProductService
style: remove trailing whitespace

refactor

Code changes that neither fix bugs nor add features:

refactor: extract validation logic into separate class
refactor: simplify error handling in API controllers
refactor: rename variables for clarity

perf

Performance improvements:

perf: optimize database queries in product search
perf: add caching for frequently accessed data
perf: reduce API response payload size

test

Adding or updating tests:

test: add unit tests for user authentication
test: increase test coverage for order service
test: add integration tests for payment API

build

Changes to build system or dependencies:

build: upgrade to .NET 8.0
build: update NuGet packages
build: configure Docker build optimization

ci

Changes to CI/CD configuration:

ci: add automated deployment to staging
ci: configure code coverage reporting
ci: update GitHub Actions workflow

chore

Other changes that don’t modify source or test files:

chore: update .gitignore
chore: clean up old migration files
chore: bump version to 2.0.0

revert

Reverting a previous commit:

revert: revert "feat: add experimental caching"

This reverts commit abc123. The caching implementation
caused issues in production.

Real-World Examples

Example 1: New Feature

feat(api): add pagination support to products endpoint

Implemented offset and limit query parameters for GET /api/products.
Default page size is 20 items. Maximum page size is 100.

Example: GET /api/products?offset=0&limit=20

Example 2: Bug Fix

fix(auth): resolve JWT token validation failing for valid tokens

The token validation was incorrectly checking the expiration time
against UTC instead of local time, causing valid tokens to be
rejected. Fixed by converting all times to UTC for comparison.

Fixes #456

Example 3: Breaking Change

feat(api)!: standardize all API error responses

BREAKING CHANGE: All error responses now use a consistent format:
{
  "success": false,
  "errors": [
    {
      "code": "ERROR_CODE",
      "message": "Human readable message"
    }
  ]
}

Previously, error formats varied across endpoints. Clients must
update their error handling logic.

Example 4: Refactoring

refactor(services): extract database connection logic into repository pattern

Moved database access code from services into dedicated repository classes.
This improves testability and follows the repository pattern.
No functional changes to the API.

Example 5: Multiple Changes

feat(orders): add order status tracking and email notifications

- Add new OrderStatus enum with Pending, Processing, Shipped, Delivered
- Implement status transition validation
- Send email notifications on status changes
- Add GET /api/orders/{id}/history endpoint to retrieve status history

Closes #123, #124

Best Practices

1. Use Imperative Mood

Write commit messages as commands - as if you’re telling the codebase what to do:

✅ Good:
feat: add user authentication
fix: resolve memory leak
docs: update API guide

❌ Bad:
feat: added user authentication
fix: resolved memory leak
docs: updated API guide

Why imperative mood?

It matches how Git itself writes commit messages:

Merge branch 'main' into feature/auth
Revert "Add experimental caching"

Git doesn’t say “Merged branch” or “Reverted” - it uses imperative mood. Your commits should follow the same convention for consistency.

More importantly, imperative mood describes what the commit does, not what you did:

✅ "fix: resolve null reference in order service"
   → Tells you what applying this commit will do
   → "This commit will resolve null reference in order service"

❌ "fix: resolved null reference in order service"
   → Tells you what happened in the past
   → Less clear what the commit actually does

Think of each commit as a patch that will be applied to the codebase. The message should complete the sentence: “If applied, this commit will…”

"If applied, this commit will... add user authentication" ✅
"If applied, this commit will... added user authentication" ❌

Over hundreds of commits, the cleaner, more consistent format makes your history much easier to scan and understand.

2. Keep Description Concise

Aim for 50 characters or less in the description:

✅ Good:
feat: add product search

❌ Bad:
feat: add comprehensive product search functionality with filters and sorting

Put details in the body:

feat: add product search

Implemented full-text search for products with support for:
- Name and description search
- Category and tag filters
- Price range filtering
- Sort by relevance, price, or date

3. Don’t End with Period

✅ Good:
feat: add user login

❌ Bad:
feat: add user login.

4. Capitalize First Letter

✅ Good:
feat: Add user authentication

❌ Bad:
feat: add user authentication

(Note: Some teams prefer lowercase - pick one style and be consistent)

5. One Logical Change Per Commit

Don’t mix unrelated changes:

❌ Bad:
feat: add authentication and fix bug in orders and update docs

✅ Good:
feat: add JWT authentication
fix: resolve null reference in order service
docs: update authentication guide

Make three separate commits instead.

6. Reference Issues

Link commits to your issue tracker:

fix: resolve login redirect loop

The authentication middleware was incorrectly handling the
redirect URL parameter, causing infinite loops.

Fixes #789
Refs #456

Tools and Automation

Commitizen

Interactive tool to help write conventional commits:

npm install -g commitizen
git cz

It prompts you:

? Select the type of change: (Use arrow keys)
❯ feat:     A new feature
  fix:      A bug fix
  docs:     Documentation only changes
  style:    Changes that don't affect code meaning
  ...

? What is the scope of this change: api

? Write a short description: add user authentication endpoint

? Provide a longer description: [optional]

Commitlint

Validates commit messages in CI/CD:

npm install --save-dev @commitlint/cli @commitlint/config-conventional
// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional']
};

In your CI pipeline:

echo "feat: add new feature" | commitlint
# ✓ passes

echo "bad commit message" | commitlint
# ✗ fails

Husky

Enforce conventional commits with Git hooks:

npm install --save-dev husky
// package.json
{
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  }
}

Now bad commit messages are rejected before they’re committed.

Standard Version

Automatically bump version and generate changelog:

npm install --save-dev standard-version
npm run release

This:

  1. Analyzes your commits
  2. Determines the version bump (major, minor, patch)
  3. Updates version in package.json
  4. Generates/updates CHANGELOG.md
  5. Creates a git tag
  6. Commits the changes

Semantic Release

Fully automated version management and package publishing:

npm install --save-dev semantic-release

In your CI/CD pipeline, it:

  1. Analyzes commits since last release
  2. Determines version bump
  3. Generates release notes
  4. Publishes to npm/NuGet/etc.
  5. Creates GitHub release

Team Adoption

Start Small

Don’t overwhelm the team - start with just the basic types:

feat: new features
fix: bug fixes
docs: documentation

Add more types as the team gets comfortable.

Add to Contributing Guide

Document your conventions:

## Commit Messages

We use Conventional Commits. Format: `type: description`

Types:
- feat: New features
- fix: Bug fixes
- docs: Documentation changes
- refactor: Code refactoring
- test: Test changes

Examples:
- feat: add user authentication
- fix: resolve database timeout
- docs: update README

Use Pull Request Templates

GitHub/GitLab PR templates can remind contributors:

## Description
Brief description of changes

## Type of Change
- [ ] feat: New feature
- [ ] fix: Bug fix
- [ ] docs: Documentation
- [ ] refactor: Code refactoring

## Commit Message
Please ensure your commits follow Conventional Commits format:
`type: description`

Make It Easy

  • Set up Commitizen for interactive prompts
  • Add commit message templates to Git
  • Use IDE plugins (VS Code has several)
  • Create aliases:
# .bashrc or .zshrc
alias gcf='git commit -m "feat: "'
alias gcx='git commit -m "fix: "'
alias gcd='git commit -m "docs: "'

Common Questions

What if I forget?

You can fix the last commit message:

git commit --amend -m "feat: corrected commit message"

Multiple types in one commit?

Keep commits focused on one type. If you have multiple types, make multiple commits:

git add src/auth/
git commit -m "feat: add user authentication"

git add docs/
git commit -m "docs: update authentication guide"

What about merge commits?

Merge commits can use conventional format too:

Merge pull request #123 from feature/authentication

feat: add JWT authentication support

Or keep them simple:

Merge branch 'main' into feature/search

Too verbose for small changes?

Even small changes benefit from structure:

fix: typo in error message
style: remove trailing whitespace
docs: fix code example in README

It’s not verbose - it’s clear.

Wrap Up

Conventional Commits turns your Git history from a chaotic mess into a structured, searchable record of how your project evolved. It’s a simple convention that pays dividends in:

  • Readability - instantly understand what changed
  • Automation - generate changelogs and version bumps
  • Collaboration - clear communication with team members
  • Maintenance - easily find when bugs were introduced or features added

Key takeaways:

  • Format: type(scope): description
  • Common types: feat, fix, docs, refactor, test, chore
  • Be consistent - pick a style and stick to it
  • Use tools - Commitizen, commitlint, husky make it easy
  • Start simple - don’t try to adopt everything at once

The beauty of Conventional Commits is its simplicity. You’re already writing commit messages - this just adds a tiny bit of structure that makes them exponentially more useful. Give it a try on your next project, and you’ll wonder how you ever lived without it.