Skip to main content

Husky + lint-staged

Overview​

The application uses Husky for managing Git hooks and lint-staged for running linters on staged files. Together, they enforce code quality by preventing commits that don't meet standards.

What Are Husky and lint-staged?​

Husky​

Husky is a Git hooks manager that:

  • Manages Git Hooks: Easy setup and management of pre-commit, pre-push, etc.
  • Prevents Bad Commits: Run checks before code reaches the repository
  • Developer Experience: Clear, actionable error messages
  • Cross-Platform: Works on macOS, Linux, and Windows

lint-staged​

lint-staged is a tool that:

  • Runs Linters on Staged Files: Only checks files you're committing
  • Prevents Partial Commits: Ensures entire staged changes are valid
  • Performance: Faster than linting entire codebase
  • Flexible: Works with any linter or script

App Configuration​

Installation​

# Install both tools
pnpm add -D husky lint-staged

# Initialize Husky
npx husky install

# Make Git hook executable
chmod +x .husky/pre-commit

Husky Setup​

.husky/pre-commit​

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

pnpm lint-staged

.husky/pre-push​

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

# Run tests before push
pnpm test -- --bail

# Validate builds
pnpm build

.husky/commit-msg​

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

# Validate commit message format
pnpm exec commitlint --edit $1

lint-staged Configuration​

.lintstagedrc.js (Alternative to package.json)​

module.exports = {
// TypeScript and JavaScript files
'*.{ts,tsx,js,jsx}': ['eslint --fix', 'prettier --write'],

// React components
'*.tsx': [
'eslint --fix',
'prettier --write',
// Optional: run tests for changed components
// 'jest --bail --findRelatedTests',
],

// JSON files
'*.json': ['prettier --write'],

// Markdown files
'*.md': ['prettier --write'],

// CSS/SCSS
'*.{css,scss}': ['prettier --write'],

// YAML files
'*.{yaml,yml}': ['prettier --write'],
};

Alternative: In package.json​

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

Commitlint Configuration​

Enforce conventional commits format:

commitlint.config.ts​

import { RuleConfigSeverity, type UserConfig } from '@commitlint/types';

const config: UserConfig = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
RuleConfigSeverity.Error,
'always',
[
'feat', // A new feature
'fix', // A bug fix
'docs', // Documentation only changes
'style', // Changes that don't affect code meaning
'refactor', // Code change that neither fixes nor adds feature
'perf', // Code change that improves performance
'test', // Adding or updating tests
'chore', // Changes to build process or dependencies
'ci', // Changes to CI configuration
'revert', // Revert a previous commit
],
],
'type-case': [RuleConfigSeverity.Error, 'always', 'lower-case'],
'type-empty': [RuleConfigSeverity.Error, 'never'],
'scope-empty': [RuleConfigSeverity.Warning, 'never'],
'scope-case': [RuleConfigSeverity.Error, 'always', 'lower-case'],
'subject-empty': [RuleConfigSeverity.Error, 'never'],
'subject-full-stop': [RuleConfigSeverity.Error, 'never', '.'],
'subject-case': [RuleConfigSeverity.Error, 'never', ['upper-case']],
'header-max-length': [RuleConfigSeverity.Error, 'always', 72],
},
};

export default config;

Git Hooks Explained​

Pre-commit Hook​

Runs before each commit:

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

# Run lint-staged
pnpm lint-staged

What it does:

  1. Lints staged files
  2. Formats code
  3. Prevents commit if errors found
  4. Developer must fix and re-stage

Pre-push Hook​

Runs before pushing to remote:

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

# Run tests
pnpm test -- --bail

# Build to ensure no build errors
pnpm build

What it does:

  1. Runs full test suite
  2. Builds project
  3. Prevents push if tests fail
  4. More comprehensive than pre-commit

Commit-msg Hook​

Validates commit message format:

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

# Validate conventional commit format
pnpm exec commitlint --edit $1

What it validates:

  • Format: type(scope): subject
  • Type must be one of allowed types
  • Subject must be lowercase
  • No period at end of subject

Workflow Examples​

Example 1: Full Workflow​

# 1. Make changes
echo "new code" > src/feature.ts

# 2. Stage file
git add src/feature.ts

# 3. Attempt commit
git commit -m "feat(feature): add new feature"

# 4. Pre-commit hook runs automatically:
# βœ“ eslint --fix (fixes formatting)
# βœ“ prettier --write (formats code)
# βœ“ If all passes, commit succeeds
# βœ“ If errors, commit fails and shows errors

# 5. If commit failed, fix errors:
# - Edit files to fix linting errors
# - Stage changes again
git add .

# 6. Retry commit
git commit -m "feat(feature): add new feature"

# 7. Pre-push hook runs when pushing:
# βœ“ pnpm test (runs full test suite)
# βœ“ pnpm build (ensures build works)
# βœ“ If all passes, push succeeds

Example 2: Automatic Fixes​

# Some errors are auto-fixable
git add .
git commit -m "fix(auth): improve login validation"

# Pre-commit hook auto-fixes:
# βœ“ ESLint fixes auto-fixable issues
# βœ“ Prettier formats code
# βœ“ Commit proceeds

# Note: You might need to run git add . again
# if changes were made

Example 3: Preventing Bad Commits​

# Wrong commit format
git commit -m "Added new feature"
# βœ— commitlint rejects: Type required (feat/fix/docs/etc)

git commit -m "FEAT(AUTH): Add new feature"
# βœ— commitlint rejects: Type must be lowercase

git commit -m "feat(auth): add new feature."
# βœ— commitlint rejects: Subject can't end with period

# Correct format
git commit -m "feat(auth): add new feature"
# βœ“ commitlint accepts
# βœ“ Pre-commit hook runs
# βœ“ Commit succeeds

Commands and Usage​

Husky Commands​

# Install Husky
pnpm add -D husky
pnpm exec husky install

# Create new hook
pnpm exec husky add .husky/pre-commit "pnpm lint"

# Remove hook
rm .husky/pre-commit

# Update hook
echo "pnpm test" > .husky/pre-commit

# Run hook manually (for testing)
sh .husky/pre-commit

# Disable hooks temporarily
HUSKY=0 git commit -m "message"

lint-staged Commands​

# Run lint-staged manually
pnpm lint-staged

# Run with debug output
pnpm lint-staged --debug

# Run against specific files
pnpm lint-staged --no-stash src/services/*.ts

# List files that would be processed
pnpm lint-staged --list

commitlint Commands​

# Validate specific commit message
pnpm exec commitlint --from HEAD~1 --to HEAD

# Validate entire history
pnpm exec commitlint --from 0

# Generate commit message
pnpm exec commitizen c

Configuration Patterns​

Monorepo per-Package​

// .lintstagedrc.js
module.exports = {
'**/apps/api/**/*.ts': ['eslint --fix --config apps/api/.eslintrc.js'],
'**/apps/web/**/*.tsx': ['eslint --fix --config apps/web/.eslintrc.json'],
'**/*.json': ['prettier --write'],
};

Conditional Rules​

// .lintstagedrc.js
const isApiPackage = (filename) => filename.includes('apps/api');

module.exports = {
'*.ts': [
(files) => {
const apiFiles = files.filter(isApiPackage);
if (apiFiles.length) {
return `eslint --fix ${apiFiles.join(' ')}`;
}
},
],
};

Different Stages​

// Run different tools for different files
module.exports = {
// Lint and format TypeScript
'*.{ts,tsx}': ['eslint --fix', 'prettier --write'],

// Format JSON
'*.json': ['prettier --write'],

// Run tests for critical files
'src/services/**/*.ts': ['eslint --fix', 'prettier --write', 'jest --bail --findRelatedTests'],
};

Common Issues​

Hook Not Running​

# Ensure .husky directory exists
ls -la .husky/

# Check if hooks are executable
chmod +x .husky/pre-commit

# Reinstall Husky
pnpm exec husky install

# Verify Git is initialized
git status

lint-staged Not Working​

# Check if lint-staged is installed
pnpm list lint-staged

# Verify config file exists
ls -la .lintstagedrc*

# Run manually to test
pnpm lint-staged

# Debug mode
pnpm lint-staged --debug

Committing Without Hooks​

# Skip hooks if necessary (use sparingly)
HUSKY=0 git commit -m "message"

# Or
git commit --no-verify -m "message"

Best Practices​

1. Keep Hooks Fast​

// βœ… GOOD: Quick checks
module.exports = {
'*.ts': ['eslint --fix', 'prettier --write'],
};

// ❌ AVOID: Slow operations that block commit
module.exports = {
'*.ts': [
'jest --testPathPattern="service"', // Slow!
'npm run build', // Very slow!
],
};

2. Use Appropriate Hooks​

# pre-commit: Fast checks (linting, formatting)
# pre-push: Comprehensive tests (unit tests, builds)
# commit-msg: Validate message format

3. Auto-fix Where Possible​

// βœ… GOOD: Auto-fix linting issues
module.exports = {
'*.ts': ['eslint --fix', 'prettier --write'],
};

// Developers see what changed and can review

4. Meaningful Commit Messages​

βœ… GOOD:
feat(auth): implement JWT refresh token rotation
fix(api): handle null values in employee update
docs(readme): add installation instructions
test(employee): add tests for duplicate email validation

❌ AVOID:
updated files
fixed bugs
changes
updated

5. Document Git Workflow​

Create CONTRIBUTING.md:

## Commit Message Format

We follow [Conventional Commits](https://www.conventionalcommits.org/).

Format: `type(scope): subject`

### Types

- **feat**: A new feature
- **fix**: A bug fix
- **docs**: Documentation changes
- **style**: Code style changes (formatting)
- **refactor**: Code restructuring
- **test**: Test additions/changes
- **chore**: Build process or dependency changes

### Example

\`\`\`
feat(api): add employee filtering by department
\`\`\`

## Pre-commit Hooks

Husky automatically runs:

1. **Pre-commit**: ESLint and Prettier
2. **Pre-push**: Tests and build

To bypass (use only when necessary):
\`\`\`bash
HUSKY=0 git commit -m "message"
\`\`\`

Setup Checklist​

  • Install Husky: pnpm add -D husky
  • Initialize: pnpm exec husky install
  • Create .husky/pre-commit hook
  • Create .husky/pre-push hook
  • Create .husky/commit-msg hook
  • Configure .lintstagedrc.js
  • Configure commitlint.config.ts
  • Install commitlint: pnpm add -D @commitlint/config-conventional @commitlint/cli
  • Test hooks locally
  • Update team documentation
  • Commit configuration files

Resources​