Git commit hooks with Husky
Rapid feedback loop in the development lifecycle is super important and useful. Nothing is more rapid than local feedback, but it doesn't replace CI like GitHub Actions.
Continuous Integration (CI) is great for running tools like the linter and building the project. However, it can be frustrating to only get feedback after a commit, push and then waiting a few minutes for those to trigger and run. Plus you might not notice the notification until much later on.
What if you could get near instant feedback before your commit? Yes, before - well- during the commit process, and the commit is prevented if the checks fail.
This is made possible with the tool Husky, their tagline is
“Git hooks made easy 🐶 woof!”.
TIP: Local git hooks do not replace your CI checks, because the user can skip these local git hooks. The way I think about it is: git hooks are better Developer Experience (DX). They are similar to client-side validation, but client-side validation is no replacement for server-side validation because client-side validation can also be skipped.
TL;DR
If it is not automated, we will forget to run it. As you will see in this blog post, we have a lint command but we have been forgetting to run it. So when we install Husky and add the lint command to run on a pre-commit hook, we will be made aware we have some code issues to fix before we can complete the commit.
Install
I will be using npm, but they also have docs for pnpm, yarn and bun.
npm install --save-dev husky
The output will look something like this:
➜ healthcheck git:(main) npm install --save-dev husky
added 1 package, and audited 387 packages in 749ms
142 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
This will change the package and lock file. Doing a git status will show the files changed:
➜ healthcheck git:(main) ✗ git status -s
M package-lock.json
M package.json
Setup
You can do the setup manually, but I recommend using the “init” command to do this automatically for you:
npx husky init
This command will have no output but will create a “.husky” folder and update the “package.json” with a new command “prepare”.
We can check again with git status; it will contain the previous changes and the new:
➜ healthcheck git:(main) ✗ git status -s
M package-lock.json
M package.json
?? .husky/
But before we dive into the new folder “.husky”, let’s look at the “package.json” file. We can do this with a “git diff package.json”:
diff --git a/package.json b/package.json
index b1c59ba..e58f6a3 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
- "test": "npx playwright test"
+ "test": "npx playwright test",
+ "prepare": "husky"
},
"dependencies": {
"@headlessui/react": "^2.1.2",
@@ -26,6 +27,7 @@
"@types/node": "^20.14.10",
"eslint": "^8",
"eslint-config-next": "14.2.4",
+ "husky": "^9.0.11",
"postcss": "^8",
"tailwindcss": "^3.4.1"
}
~
Here we see the new command “"prepare": "husky". Unlike the other “npm” we will not run this command ourselves, but will run as a hook on “npm install” - so you don’t need to worry about it. This command will run when the project is being installed locally and will setup the git hook(s).
The directory we are interested in is “.husky” and the file “pre-commit”. This will already contain a command which will run when we do a commit:
npm test
If this command fails, then the commit will not succeed. In this file “.husky/pre-commit” we can add additional commands to also run (for example, the linter) making the file look like this:
npm run lint
npm test
Now next time you run “git commit”, these commands will run as a pre git commit hook.
Let’s test it out, but first I always do a git status check:
➜ healthcheck git:(main) ✗ git status -s
M package-lock.json
M package.json
?? .husky/
➜ healthcheck git:(main) ✗ git add .husky/
➜ healthcheck git:(main) ✗ git status -s
A .husky/pre-commit
M package-lock.json
M package.json
Now I am happy with that, let me do a commit:
➜ healthcheck git:(main) ✗ git commit -m "feat: setup husky" .husky/ package*
> healthcheck@0.6.0 lint
> next lint
./src/components/Header.js
113:21 Warning: Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
184:17 Warning: Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element @next/next/no-img-element
./src/components/Heading.js
13:15 Error: Missing "key" prop for element in iterator react/jsx-key
27:13 Error: Missing "key" prop for element in iterator react/jsx-key
info - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/basic-features/eslint#disabling-rules
husky - pre-commit script failed (code 1)
Straight away we can see there are some errors, not with Husky or the setup of Husky but with my project! We have a linter setup in our project, but it is not running locally or on CI. So now we have tried to commit, it has highlighted problems with our project’s code. Hopefully, you can see the benefit of automating everything!
TIP: if you want to skip these git pre-commit hook checks, you can with the flag “-n” but please use this with extreme caution. You don’t want to jump ahead and think all is working fine, only to encounter a bigger problem later on.
Once I have fixed these lint issues with my project, I can try to commit again. This time if the lint command is successful then it will move on to the test command because that is the order we have in our Husky pre-commit config file.
This looks better, both git pre-hook commands run successfully and the commit is made:
➜ healthcheck git:(main) ✗ git commit -m "feat: setup husky" .husky/ package*
> healthcheck@0.6.0 lint
> next lint
✔ No ESLint warnings or errors
> healthcheck@0.6.0 test
> npx playwright test
Running 12 tests using 4 workers
12 passed (11.5s)
To open last HTML report run:
npx playwright show-report
[main f447eec] feat: setup husky
3 files changed, 21 insertions(+), 1 deletion(-)
create mode 100644 .husky/pre-commit
Now you can confidently push your code to the remote repo. The CI checks should also pass: these are still needed as they are independent and will do a fresh setup of the project and re-run these checks.
If you would prefer to watch a video on this topic: