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.
With Footer (Optional)
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:
- Analyzes your commits
- Determines the version bump (major, minor, patch)
- Updates version in package.json
- Generates/updates CHANGELOG.md
- Creates a git tag
- Commits the changes
Semantic Release
Fully automated version management and package publishing:
npm install --save-dev semantic-release
In your CI/CD pipeline, it:
- Analyzes commits since last release
- Determines version bump
- Generates release notes
- Publishes to npm/NuGet/etc.
- 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.