Setting Up Husky, Commitlint, and Lint-Staged for Git Hooks
Husky runs scripts on Git hooks, Commitlint rejects commit messages that break the Conventional Commits format, and lint-staged lints only the files you staged. Together they refuse a commit unless it passes ESLint, Prettier, a TypeScript type check and a production build โ set up here for a TypeScript Express back end, with every commit type explained.


Install Required Packages
Run the following commands to install Husky, Commitlint, and related tools:
# Install Husky and Commitlint
npm install husky @commitlint/config-conventional @commitlint/cli --save-dev
# Install lint-staged for running linters on staged files
npm install lint-staged --save-dev
npx husky initSet Up Husky
Initialize Husky in your project:
npx husky installConfigure Commitlint
Open .husky/commit-msg and add the following script:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx commitlint --edit --config=.commitlintrc.ts ||
(
echo '๐ค๐ค๐ค๐ค Looks like your message format is not right ๐ค๐ค๐ค๐ค
Commit Lint failed: Reformat your commit message according to convention.
'
false;
)Configure Pre-Commit Hook
Open .husky/pre-commit and add the following script:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo '๐๏ธ๐ท Styling, testing and building your project before committing'
# Check staged files
npx lint-staged ||
(
echo '๐คข๐คฎ๐คข๐คฎ Its FOKING RAW - Your styling looks disgusting. ๐คข๐คฎ๐คข๐คฎ
Staged files Check Failed. Run npm run format, add changes and try commit again.';
false;
)
# Check Prettier standards
npm run format ||
(
echo '๐คข๐คฎ๐คข๐คฎ Its FOKING RAW - Your styling looks disgusting. ๐คข๐คฎ๐คข๐คฎ
Prettier Check Failed. Run npm run format, add changes and try commit again.';
false;
)
# Check ESLint Standards
npm run lint ||
(
echo '๐ค๐๐๐ค Get that weak shit out of here! ๐ค๐๐๐ค
ESLint Check Failed. Make the required changes listed above, add changes and try to commit again.'
false;
)
# Check tsconfig standards
npm run type-check ||
(
echo '๐คก๐โ๐คก Failed Type check. ๐คก๐โ๐คก
Are you seriously trying to write that? Make the changes required above.'
false;
)
## Run tests
#pnpm run test ||
#(
# echo '๐คก๐โ๐คก Failed Test. ๐คก๐โ๐คก
# Are you seriously trying to write that? Make sure you pass all tests and try to commit again.'
# false;
#)
# If everything passes... Now we can build
echo '๐ค๐ค๐ค๐ค... Alright.... Code looks good to me... Trying to build now. ๐ค๐ค๐ค๐ค'
npm run build ||
(
echo 'โ๐ท๐จโ Better call Bob... Because your build failed โ๐ท๐จโ
Next build failed: View the errors above to see why.
'
false;
)
# If everything passes... Now we can commit
echo 'โ
โ
โ
โ
You win this time... I am committing this now. โ
โ
โ
โ
'Create Commitlint Configuration
Create a .commitlintrc.ts file and add the following configuration:
import type { UserConfig } from '@commitlint/types';
const commitConfig: UserConfig = {
extends: ['@commitlint/config-conventional'],
};
module.exports = commitConfig;Update package.json
Add the following scripts and configuration to your package.json:
{
"name": "stationery-shop-backend",
"version": "1.0.0",
"main": "dist/server.js",
"scripts": {
"start": "node ./dist/server.js",
"start:prod": "node ./dist/server.js",
"start:dev": "ts-node-dev --respawn --transpile-only src/server.ts",
"build": "tsc",
"deploy-vercel": "vercel --prod",
"test": "echo "Error: no test specified" && exit 1",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix",
"format": "prettier --write .",
"prepare": "husky",
"type-check": "tsc --pretty --noEmit"
},
"lint-staged": {
"src/**/*.{ts,js}": [
"eslint --fix",
"git add"
]
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"mongoose": "^8.8.2",
"zod": "^3.23.8"
},
"devDependencies": {
"@commitlint/cli": "^19.6.0",
"@commitlint/config-conventional": "^19.6.0",
"@eslint/js": "^9.15.0",
"@types/cors": "^2.8.17",
"@types/eslint__js": "^8.42.3",
"@types/express": "^5.0.0",
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"globals": "^15.12.0",
"husky": "^9.1.7",
"lint-staged": "^15.2.10",
"prettier": "^3.3.3",
"ts-node-dev": "^2.0.0",
"typescript": "^5.6.3",
"typescript-eslint": "^8.15.0"
}
}Commit Types
Here are the supported commit types and their descriptions:
| Type | Description | Example |
|---|---|---|
feat: | Adding a new feature. | feat(auth): add user authentication module |
fix: | Fixing a bug. | fix(login): resolve token validation issue |
build: | Changes to the build system or dependencies. | build: upgrade TypeScript to version 5.6.3 |
chore: | Routine tasks like dependency updates or configs. | chore: update eslint configuration |
ci: | Continuous integration (CI/CD) changes. | ci: add GitHub Actions for testing |
docs: | Documentation changes only. | docs: add API documentation for user endpoints |
style: | Code style changes that donโt affect functionality. | style: format code with Prettier |
refactor: | Code refactoring without adding new features or fixing bugs. | refactor: optimize data fetching logic |
perf: | Performance improvements. | perf: reduce database query execution time |
test: | Adding or updating tests. | test: add unit tests for user service |
revert: | Reverting a previous commit. | revert: undo feat: add user authentication module |
hotfix: | Urgent fixes in production (*not part of the formal spec, but commonly used*). | hotfix: patch critical security vulnerability |
Common Questions
- What does Husky do?
- Husky runs your scripts on Git hooks such as
pre-commitandcommit-msg. The hooks live in the.husky/folder and are committed with the project, so everyone who clones the repository runs the same checks before a commit is made. - What is the difference between Husky and lint-staged?
- Husky decides when something runs โ on a Git hook. lint-staged decides what it runs on: only the files staged for the commit. Together, the pre-commit hook lints and formats just the changed files, which keeps it fast on a large project.
- What is a Conventional Commit message?
- A message in the form
type(scope): description, for examplefeat(auth): add login with Google. Common types are feat, fix, docs, style, refactor, test and chore. Commitlint with@commitlint/config-conventionalrejects any message that does not follow the format. - How do I skip the Husky hooks for one commit?
- Run
git commit --no-verify(or-n). It skips thepre-commitandcommit-msghooks for that commit only. Keep it for emergencies: the checks exist so that broken or unformatted code never reaches the shared branch.