TL;DR: Use type(scope): subject to write clear commit messages. Choose the right type for each change, keep commits small, and commit when one task is complete. This makes your Git history easier to read and your work easier to manage.
The Git History Nobody Wants to Read
A few months ago, I opened the Git history of an old project to find out when a bug was introduced.
Here is what I found:
final final_v2 update fixed stuff asdf final_final_REAL The problem? I could not understand what I had changed in any of those commits.
I think many developers have done this at some point. I have done it too.
When we are working on a project, writing update or fixed stuff may feel easy. But after a few months, those messages become useless.
This is why writing good commit messages matters.
A good commit message tells other developers what changed and why. It also helps you understand your own work when you return to a project later.
Let's look at how to write better Git commit messages.
1. What Are Conventional Commits?
Conventional Commits is a simple way to write Git commit messages using a common format.
Instead of writing messages in different styles, we follow one structure.
<type>(<scope>): <subject> <body> <footer> Here is a simple example:
git commit -m "feat(auth): add Google login" Let's understand why this format is useful.
- Everyone can understand the commit history more easily.
- Developers can find changes faster.
- Code reviews become easier.
- Tools can create changelogs from commit messages.
- Release tools can use commit messages to decide version numbers.
It also makes teamwork easier because everyone follows the same format.
2. Understanding the Commit Message Structure
Let's break down this example:
feat(auth): add Google login There are three main parts.
Type: feat
It tells us what kind of change was made.
Scope: auth
It tells us which part of the project was changed. The scope is optional.
Subject: add Google login
It gives a short description of the change.
We can also add a body and footer when needed.
Body
The body explains why the change was made or gives more details.
Footer
The footer can reference an issue or describe a breaking change.
A complete example:
feat(auth): add Google login Allow users to sign in using their Google accounts. This makes the login process easier for new users. Closes #42 You do not need a body and footer for every commit. Use them when they add useful information.
3. Git Commit Types Explained
This is one of the most important parts of writing good commit messages.
Different types describe different kinds of changes.
Let's go through the most commonly used types and see when to use them.
3.1. init
Use init when starting a new project or creating the initial project setup.
Example:
git commit -m "init: set up Node.js project" Other examples:
init: create Next.js application init: add initial project structure init: configure TypeScript When should you use it?
Use it when you are setting up a new project for the first time.
Note that init is a common team convention, not a required Conventional Commits type. You can also use chore or another suitable type for initial setup.
3.2. feat
Use feat when adding a new feature to an application.
For example:
- Adding Google login.
- Creating a new API endpoint.
- Adding a search feature.
- Adding a new dashboard page.
- Adding payment integration.
Example:
git commit -m "feat(auth): add Google login" Other examples:
feat(user): add profile update feature feat(api): add product search endpoint feat(payment): integrate Stripe checkout When should you use it?
Use feat whenever you add new functionality that users or other parts of the application can use.
3.3. fix
Use fix when correcting a bug or something that does not work as expected.
For example:
- Login fails with valid credentials.
- Product quantity becomes negative.
- API returns incorrect data.
- Payment status is not updated.
- Form validation does not work correctly.
Example:
git commit -m "fix(cart): prevent negative quantity" Other examples:
fix(auth): handle expired access tokens fix(api): return correct user data fix(payment): prevent duplicate transactions When should you use it?
Use fix when the main purpose of your change is to correct existing behavior.
3.4. docs
Use docs when changing project documentation.
For example:
- Updating README.
- Adding installation instructions.
- Writing API documentation.
- Updating project guides.
- Correcting documentation errors.
Example:
git commit -m "docs(readme): add installation steps" Other examples:
docs(api): add authentication examples docs(project): update setup guide docs(readme): add environment variables When should you use it?
Use docs when you are changing documentation rather than application functionality.
3.5. style
Use style for code formatting changes that do not change how the application works.
For example:
- Fixing indentation.
- Removing unnecessary spaces.
- Formatting code with Prettier.
- Changing semicolon usage.
- Organizing whitespace.
Example:
git commit -m "style(ui): format components with prettier" Other examples:
style(api): fix code indentation style(auth): apply consistent formatting style(project): format TypeScript files Important: style usually refers to code formatting, not visual changes to a website.
If you change a button's color or add a new UI feature, use a suitable type such as feat or fix.
3.6. refactor
Use refactor when improving the code structure without changing its existing behavior.
For example:
- Moving repeated logic into reusable functions.
- Separating business logic from controllers.
- Improving folder structure.
- Extracting validation middleware.
- Removing unnecessary code duplication.
Example:
git commit -m "refactor(api): extract validation middleware" Other examples:
refactor(auth): simplify token validation refactor(user): separate service logic refactor(database): improve query structure When should you use it?
Use refactor when the code structure changes but the application's external behavior remains the same.
3.7. perf
Use perf when improving application performance.
For example:
- Optimizing database queries.
- Reducing API response time.
- Adding database indexes.
- Improving caching.
- Reducing unnecessary API requests.
Example:
git commit -m "perf(search): optimize database queries" Other examples:
perf(api): reduce response processing time perf(database): add index for user queries perf(cache): improve Redis caching When should you use it?
Use perf when your main goal is to make something faster or use fewer resources.
3.8. test
Use test when adding, updating, or fixing tests.
For example:
- Adding unit tests.
- Writing integration tests.
- Updating API test cases.
- Fixing failing test cases.
- Adding authentication tests.
Example:
git commit -m "test(auth): add login unit tests" Other examples:
test(user): add profile update tests test(api): cover error responses test(payment): add checkout integration tests When should you use it?
Use test when the main change is related to tests rather than production code.
3.9. build
Use build when changing the build system, dependencies, or build configuration.
For example:
- Updating package dependencies.
- Changing TypeScript configuration.
- Updating Docker build configuration.
- Changing Webpack or Vite settings.
- Updating build scripts.
Example:
git commit -m "build(deps): update React dependencies" Other examples:
build(docker): update Node.js image build(config): update TypeScript settings build(deps): add Zod dependency 3.10. ci
Use ci when changing Continuous Integration or Continuous Delivery configuration.
For example:
- Adding GitHub Actions.
- Updating automated test workflows.
- Changing deployment pipelines.
- Adding lint checks to CI.
- Updating build automation.
Example:
git commit -m "ci(github): add test workflow" Other examples:
ci(actions): run tests on pull requests ci(deploy): update deployment workflow ci(project): add lint checks 3.11. chore
Use chore for general project maintenance that does not fit another suitable type.
For example:
- Updating
.gitignore. - Cleaning unused configuration files.
- Organizing project scripts.
- Updating development tools.
- General maintenance tasks.
Example:
git commit -m "chore: update gitignore" Other examples:
chore(deps): update development dependencies chore(project): remove unused files chore(config): update environment example One thing to remember: Do not use chore for every change.
If you add a feature, use feat. If you fix a bug, use fix. If you improve the code structure, use refactor.
Choose the type that best describes the actual change.
3.12. revert
Use revert when undoing a previous commit.
For example, imagine you added a new feature but later found that it caused problems. You may need to revert that commit.
Example:
git commit -m "revert: remove Google login feature" You can also reference the original commit:
revert: remove Google login feature This reverts commit abc1234. When should you use it?
Use revert when you want to record the removal of an earlier change in Git history.
4. Other Commit Types You May See
Apart from the common types above, you may see other commit types in different projects.
These are not all part of the official Conventional Commits type list. Teams may define their own rules.
| Type | Common use | Example |
|---|---|---|
wip | Work in progress | wip: work on user dashboard |
merge | Merge-related commit | merge: integrate feature branch |
release | Release preparation | release: prepare version 1.2.0 |
improvement | General improvement | improvement: improve search experience |
init | Initial project setup | init: create backend project |
Use these only when your team has agreed to use them.
For example, wip may be useful on a personal development branch, but it is usually better to keep the main branch history clean.
5. How to Choose the Right Scope
A scope tells us which part of the project was changed.
It makes commit messages easier to understand, especially in large applications.
For example:
git commit -m "feat(auth): add email verification" git commit -m "fix(payment): handle failed transactions" git commit -m "refactor(user): extract validation logic" Here are some common scopes:
| Scope | Used for |
|---|---|
auth | Authentication |
user | User management |
payment | Payment features |
api | API-related changes |
database | Database operations |
ui | User interface |
admin | Admin dashboard |
notification | Notifications |
socket | Real-time communication |
config | Configuration |
You do not need to use every scope in every project.
Choose scopes that match your project structure and keep them consistent across your team.
Also, remember that the scope is optional.
If a change does not belong to one clear area, you can leave it out.
6. How to Write a Good Commit Subject
The subject is the first line of your commit message.
It should explain the change clearly and briefly.
Here are five simple rules.
1. Use imperative wording
Write:
add user authentication Avoid:
added user authentication adding user authentication Think of it as completing the sentence: "If applied, this commit will..."
2. Keep it short
Try to keep the subject around 50 characters when practical. This is a useful guideline, not a strict Git rule.
3. Avoid unnecessary punctuation
Do not add a full stop at the end.
4. Use clear words
Avoid vague descriptions like update code or fix issue.
5. Describe the actual change
Write what changed, not a long explanation of how you implemented it.
7. When Should You Add a Body or Footer?
Most commits only need a subject line.
But sometimes, the subject is not enough.
That is when a body or footer becomes useful.
Commit body
Use the body to explain why you made the change or provide important details.
Example:
fix(auth): handle expired refresh tokens Previously, expired refresh tokens were not handled correctly. This change returns an authentication error and asks the user to log in again. Commit footer
Use the footer to reference an issue or describe a breaking change.
Example:
feat(api): add product filtering Allow users to filter products by category and price. Closes #42 Breaking changes
A breaking change is a change that may cause existing users or applications to stop working without an update.
You can mark it with !:
feat(api)!: change user response format Or use a footer:
feat(api): change user response format BREAKING CHANGE: The API now returns user data inside a data object instead of returning an array. Both approaches are supported by Conventional Commits.
8. When Should You Commit Your Code?
This is an important question, especially for developers who are new to Git.
My simple rule is:
Commit when one logical piece of work is complete.
You do not need to wait until the entire feature or project is finished.
You can commit after completing a small, meaningful task.
Good times to commit
1. After completing a small feature
For example, after implementing user registration:
git commit -m "feat(auth): add user registration" 2. After fixing a specific bug
git commit -m "fix(cart): update total after item removal" 3. After completing a refactoring task
git commit -m "refactor(user): separate service logic" 4. After adding or updating tests
git commit -m "test(auth): add registration tests" 5. Before switching to another task
If your current work is in a stable and meaningful state, committing it can help you switch tasks safely.
6. After completing a meaningful development step
For example, you may first create the database model, then add the service logic, and finally connect it to the API.
Each step can have its own commit if it is complete and useful on its own.
When should you avoid committing?
Avoid creating normal commits that contain:
- Unrelated changes from different tasks.
- Unnecessary debugging logs.
- Accidentally included
.envfiles or credentials. - Temporary files that should not be tracked.
- Half-finished changes that break the shared branch.
However, work-in-progress commits are sometimes useful on personal feature branches. The important thing is to keep shared project history understandable.
9. Why Should We Commit Regularly?
Some developers wait until they finish a large feature before creating a commit.
That can make things difficult later.
Here are some reasons to commit regularly.
Easy to understand
Small commits tell you exactly what changed.
Easy to review
Other developers can review smaller changes without going through hundreds of unrelated lines.
Easy to undo
If a particular change causes a problem, you can revert that commit without removing unrelated work.
Easy to debug
Git has a useful command called git bisect. It helps find which commit introduced a bug.
This works best when commits represent clear and complete changes.
Better project history
When you run:
git log --oneline You should be able to understand how your project has changed over time.
10. Bad vs Good Commit Messages
Let's compare some common examples.
| Bad commit message | Better commit message |
|---|---|
fixed stuff | fix(cart): prevent negative quantity |
update | feat(auth): add Google login |
changes | refactor(api): extract validation logic |
final version | docs(readme): update setup guide |
bug fixed | fix(payment): handle failed transactions |
new things | feat(user): add profile update |
code update | style(ui): format React components |
The better messages tell us what changed without opening the code.
That is the main goal.
11. Commit Types and Semantic Versioning
Conventional Commits can also help with software version management.
Many projects use Semantic Versioning, which follows this format:
MAJOR.MINOR.PATCH For example:
1.0.0 Here is how the main commit types relate to version changes when using a compatible release setup.
| Commit | Version change | Example |
|---|---|---|
fix | PATCH | 1.0.0 to 1.0.1 |
feat | MINOR | 1.0.0 to 1.1.0 |
| Breaking change | MAJOR | 1.0.0 to 2.0.0 |
A breaking change can be marked using ! or BREAKING CHANGE:.
Tools such as semantic-release and release-please can read commit messages and help generate changelogs and releases.
One important thing: these version changes depend on your release tool and its configuration. Git itself does not automatically increase version numbers based on commit types.
12. How to Follow These Rules Automatically
Remembering all these rules can take time.
Fortunately, there are tools that can check commit messages for you.
commitlint
commitlint checks whether your commit messages follow the selected convention.
Install it:
npm install --save-dev @commitlint/cli @commitlint/config-conventional Create a configuration file:
// commitlint.config.js module.exports = { extends: ['@commitlint/config-conventional'], }; If your project uses ES modules, use:
// commitlint.config.js export default { extends: ['@commitlint/config-conventional'], }; Husky
Husky allows you to run Git hooks automatically.
Install and initialize it:
npm install --save-dev husky npx husky init Create a file named .husky/commit-msg with this content:
npx --no -- commitlint --edit "$1" Now, when you create a commit, commitlint checks the message.
For example:
git commit -m "fixed stuff" The commit will be rejected if the message does not follow your configured rules.
But this message should pass:
git commit -m "fix(auth): handle expired tokens" commitizen
If you prefer answering questions instead of writing commit messages manually, you can use Commitizen.
It guides you through the process of choosing a type, scope, and description.
Install it:
npm install --save-dev commitizen cz-conventional-changelog Then run:
npx cz It will help you create a properly formatted commit message.
13. Useful Git Commands Before Committing
Before creating a commit, I recommend checking your changes.
Here are some useful commands.
Check changed files:
git status Review your changes:
git diff Stage the files you want to commit:
git add . You can also stage a specific file:
git add src/controllers/auth.controller.ts Review staged changes:
git diff --cached Create a commit:
git commit -m "feat(auth): add email verification" View recent commits:
git log --oneline View detailed commit history:
git log One more useful command:
git add -p It allows you to select specific parts of a file instead of staging the entire file.
This is helpful when one file contains changes from different tasks.
14. Common Commit Mistakes to Avoid
Here are some mistakes developers should try to avoid.
Using chore for everything
Not every change is maintenance. Choose the type that describes the actual work.
Writing very long subject lines
Keep the first line short. Add more details in the body when needed.
Mixing unrelated changes
Do not combine a payment bug fix and a dashboard redesign in one commit.
Committing sensitive information
Never commit passwords, API keys, private tokens, or production credentials.
If a secret has already been pushed, removing it from the latest commit is not enough. You should rotate or revoke that secret immediately.
Committing without checking staged files
Always review what you are about to commit.
Creating commits with unclear messages
Avoid messages such as update, changes, done, and final.
A little extra time spent writing a clear message can save a lot of time later.
15. A Simple Commit Message Cheat Sheet
Here is a quick reference you can keep for your next project.
| Type | When to use | Example |
|---|---|---|
feat | New feature | feat(auth): add Google login |
fix | Bug fix | fix(cart): prevent negative quantity |
docs | Documentation | docs(readme): update setup guide |
style | Code formatting | style(ui): format components |
refactor | Code restructuring | refactor(api): extract middleware |
perf | Performance improvement | perf(db): optimize queries |
test | Test changes | test(user): add signup tests |
build | Build and dependencies | build(deps): update packages |
ci | CI/CD changes | ci: add GitHub Actions workflow |
chore | General maintenance | chore: update gitignore |
revert | Undo a previous commit | revert: remove login feature |
Additional team-specific conventions:
| Type | When to use |
|---|---|
init | Initial project setup |
wip | Work in progress |
merge | Merge-related commit |
release | Release preparation |
improvement | General improvement |
16. My Checklist Before Every Commit
Before creating a commit, check these things:
- [ ] Does this commit contain one logical change?
- [ ] Did I review the changed files?
- [ ] Did I check the staged changes?
- [ ] Did I choose the correct commit type?
- [ ] Is the commit message clear?
- [ ] Did I avoid unrelated changes?
- [ ] Did I exclude passwords and API keys?
- [ ] Did I run the relevant tests?
You do not need to follow every rule perfectly from day one. Start with clear messages and small commits. Build the habit over time.
Final Thoughts
Writing good commit messages is a small habit, but it makes a real difference when working on software projects.
You do not need to write long messages or use complicated words. Just explain what changed and choose the right commit type.
When you work with other developers, a clean Git history makes everyone's job easier.
Start using feat, fix, docs, refactor, and the other commit types in your next project.
Your future self will thank you.
What is the most unusual commit message you have ever written or seen?
Share it in the comments
Happy coding






