Setting Up Pre-Commit Hooks: Husky + lint-staged for TypeScript Monorepos

Table of Contents

  • What are Pre-Commit Hooks?
  • Why Use Husky and lint-staged?
  • Installing Husky and lint-staged
  • Basic Setup for Husky
  • Configuring lint-staged
  • Full Example: TypeScript + ESLint + Prettier Workflow
  • Best Practices
  • Conclusion

What are Pre-Commit Hooks?

Pre-commit hooks are scripts that automatically run before you commit your code to Git.

You can use them to:

  • Lint code
  • Format code
  • Run tests
  • Prevent bad commits

This saves you (and your team) from introducing broken or badly formatted code into the repository.


Why Use Husky and lint-staged?

  • Husky: Makes it super easy to manage Git hooks like pre-commit, pre-push, etc.
  • lint-staged: Runs linters and formatters only on changed (staged) files, making it fast.

Together, they ensure only clean, well-formatted code gets committed — without slowing down developers.


Installing Husky and lint-staged

At the root of your monorepo:

# Install both as dev dependencies
npm install --save-dev husky lint-staged

or if you’re using yarn:

yarn add -D husky lint-staged

Basic Setup for Husky

  1. Enable Husky in your repo:
npx husky install
  1. Make sure it’s installed automatically after npm install:

In your package.json:

{
"scripts": {
"prepare": "husky install"
}
}
The prepare script makes sure Husky hooks are available after someone installs dependencies.
  1. Add a Pre-commit Hook:
npx husky add .husky/pre-commit "npx lint-staged"

This creates a .husky/pre-commit file that will run lint-staged every time you commit.


Configuring lint-staged

Now configure lint-staged in your package.json:

{
"lint-staged": {
"**/*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"**/*.{js,json,css,md,yml}": [
"prettier --write"
]
}
}

Explanation:

  • For .ts and .tsx files:
    • First, run ESLint and auto-fix issues.
    • Then, format with Prettier.
  • For other file types (.js, .json, .css, .md, .yml):
    • Only format using Prettier.

Full Example: TypeScript + ESLint + Prettier Workflow

Your package.json would look like:

{
"scripts": {
"prepare": "husky install",
"lint": "eslint . --ext .ts,.tsx",
"format": "prettier --write ."
},
"devDependencies": {
"husky": "^9.0.0",
"lint-staged": "^15.0.0",
"eslint": "^8.0.0",
"prettier": "^3.0.0",
"typescript": "^5.0.0"
},
"lint-staged": {
"**/*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"**/*.{js,json,css,md,yml}": [
"prettier --write"
]
}
}

And your .husky/pre-commit file would contain:

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

Best Practices

✅ Keep pre-commit hooks lightweight.
(Only lint and format staged files, not full project.)

✅ Fail fast.
If lint or formatting fails, block the commit.
Fix and re-stage before committing again.

✅ Integrate with CI/CD.
Pre-commit is local safety.
You should still run full lints/tests in CI pipelines to catch missed issues.

✅ Keep Husky configuration in Git
(.husky directory should be committed.)


Conclusion

Setting up Husky + lint-staged creates an automatic quality gate for your TypeScript monorepo.

Without even thinking about it, your team will:

  • Catch ESLint errors early
  • Maintain formatting standards
  • Save time during code review
  • Ship cleaner code

Summary Workflow:

Developer → git add → git commit → (Husky triggers) → lint-staged runs → commit succeeds or fails based on code quality ✅