Enforcing Coding Conventions with Husky Pre-commit Hooks

In this guide, we'll learn how to setup Husky to prevent bad git commits and enforce code standards in your project.

This post is a part of the Clean Code Tooling series.
You may want to read the previous posts.
1. How to use ESLint with TypeScript
2. How to use Prettier with ESLint and TypeScript in VSCode

Intro

On most projects I've ever worked collaboratively on, someone takes the role of the code cleanliness champion. It's usually the team lead, and often times, their role involves reviewing PRs and making sure love and care is put into the quality of the code.

Quality includes both the chosen coding conventions in addition to the formatting of the code.

Today, it's good practice in JavaScript projects to utilize ESLint to define the project's coding conventions. For example, how does your team feel about using for loops? What about semicolons- are those required? Etc.

Those are conventions.

The other piece of the puzzle is formatting. That's the visual appearance of the code. When there's more than one developer working on a project, ensuring that code looks consistent is something to be addressed.

Prettier is the correct tool for this.

In the previous article, we learned how to combine both ESLint and Prettier, but we didn't learn how to actually enforce the conventions and formatting on a real life project with multiple developers.

In this article, we'll learn how to use Husky to do so on a Git-based project.

Husky

Husky is an npm package that "makes Git hooks easy".

When you initialize Git (the version control tool that you're probably familar with) on a project, it automatically comes with a feature called hooks.

If you go to the root of a project intialized with Git and type:

ls .git/hooks

You'll see a list of sample hooks like pre-push, pre-rebase, pre-commit, and so on. This is a way for us to write plugin code to execute some logic before we perform the action.

If we wanted to ensure before someone creates a commit using the git commit command, that their code was properly linted and formatted, we could write a pre-commit Git hook.

Writing that manually probably wouldn't be fun. It would also be a challenge to distribute and ensure that hooks were installed on other developers' machines.

These are some of the challenges that Husky aims to address.

With Husky, we can ensure that for a new developer working in our codebase (using at least Node version 10):

  • Hooks get created locally
  • Hooks are run when the Git command is called
  • Policy that defines how someone can contribute to a project is enforced.

Let's get it set up.

Installing Husky

To install Husky, run:

npm install husky --save-dev

Configuring Husky

To configure Husky, in the root of our project's package.json, add the following husky key:

package.json
"husky": {
  "hooks": {
    "pre-commit": "",       // Command goes here
    "pre-push": "",         // Command goes here
    "...": "..."
  }
}

When we execute the git commit or git push command, the respective hook will run the script we supply in our package.json.

Example workflow

Following along from the previous articles, if we've configured ESLint and Prettier, I suggest to utilize two scripts:

  • prettier-format: Format as much code as possible.
  • lint: Ensure that the coding conventions are being adhered to. Throw an error if important conventions are broken.
package.json
{
  "scripts": {
    "prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",    "lint": "eslint . --ext .ts",    ...
  },
  "husky": {
    "hooks": {
      "pre-commit": "npm run prettier-format && npm run lint"    }
  }
}

Include these scripts in the scripts object in your package.json. Then, at the very least, run prettier-format and then lint as a pre-commit hook.

This will ensure that you cannot complete a commit without formatted code that passes the conventions.

No-loops example

I like to use the no-loops package as an example. This convention doesn't allow developers to use for loops, and instead suggests that we use Array utility functions like forEach, map, and the like.

Adding the plugin and its rule to the .eslintrc:

.eslintrc
{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": [
    "@typescript-eslint",
    "no-loops",    "prettier"
  ],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ],
  "rules": {
    "no-loops/no-loops": 2, // 2 means throw an ERROR     "no-console": 1,
    "prettier/prettier": 2
  }
}

And then placing a for loop in the source code...

src/index.ts
console.log('Hello world!');

for (let i = 0; i < 12; i++) {
  console.log(i);
}

And attempting to commit, it should exit with a non-zero exit code, which as we know, means an error occurred.

simple-typescript-starter git:(prettier)git commit -m "Test commit"
husky > pre-commit (node v10.10.0)

> typescript-starter@1.0.0 prettier-format simple-typescript-starter
> prettier --config .prettierrc 'src/**/*.ts' --write

src/index.ts 191ms

> typescript-starter@1.0.0 lint /simple-typescript-starter
> eslint . --ext .ts


/simple-typescript-starter/src/index.ts
  1:1  warning  Unexpected console statement  no-console
  3:1  error    loops are not allowed         no-loops/no-loops
  4:3  warning  Unexpected console statement  no-console

✖ 3 problems (1 error, 2 warnings)

And there it is!

Other considerations

If you notice that linting is taking a long time, check out this package, lint-staged. It runs the linter, but only against files that are staged (files that you're ready to push). This was suggested to me by @glambertmtl. Thank you!



Discussion

Liked this? Sing it loud and proud 👨‍🎤.


6 Comments

Submit
Coolguy
9 months ago

This is cool

Waheed
8 months ago

Thanks again for yet another clear and simple walkthrough...


I am updating my template.

apolineaire
5 months ago

Thank you Khalil, this is cool.

dc
4 months ago

hey this looks like a nice setup, would be great if there was a repo to clone!

Smartniggs
3 months ago

Nice work bro!

Max
a month ago

Nice article, thank you


Stay in touch!



About the author

Khalil Stemmler,
Developer Advocate @ Apollo GraphQL ⚡

Khalil is a software developer, writer, and musician. He frequently publishes articles about Domain-Driven Design, software design and Advanced TypeScript & Node.js best practices for large-scale applications.



View more in Tooling



You may also enjoy...

A few more related articles

Make Illegal States Unrepresentable! - Domain-Driven Design w/ TypeScript
By using TypeScript's static type system, not only can we enforce (typically challenging things like) business rules and error sta...
How to Handle Updates on Aggregates - Domain-Driven Design w/ TypeScript
In this article, you'll learn approaches for handling aggregates on Aggregates in Domain-Driven Design.
Decoupling Logic with Domain Events [Guide] - Domain-Driven Design w/ TypeScript
In this article, we'll walk through the process of using Domain Events to clean up how we decouple complex domain logic across the...
Does DDD Belong on the Frontend? - Domain-Driven Design w/ TypeScript
Should we utilize Domain-Driven Design principles and patterns in front-end applications? How far does domain modeling reach from ...

Want to be notified when new content comes out?

Join 8000+ other developers learning about Domain-Driven Design and Enterprise Node.js.

I won't spam ya. 🖖 Unsubscribe anytime.

Get updates