Part 02: Streamline Your Dev Workflow: Husky, Lint-Staged, & Commitlint for Next.js

Miyushan Rodrigo
5 min readMay 29, 2024

--

This is the second article of “Recommended Next.js Libraries & Packages” article series. Here I explain how to streamline your developer workflow using husky, lint-staged, & commitlint. I have provided the relevant codes for each step to get a better understanding.

Check out the first article on Configure ESLint and Prettier into Next.js projects in this article series to start from the beginning.

Let’s get started…!

Concepts

1. Git Hooks

Execution points of common Git Hooks
Execution points of common Git Hooks

Git hooks are a powerful toolset we can use to automate tasks and enforce policies. GIT provides several git hooks such as pre-commit, commit-msg, post-commit, pre-push, etc. You can get an idea of the execution point of each hook from the above image. As I already mentioned, we can enforce policies with Git hooks. So, we can prevent the execution of operations if it violates a policy. Here is a brief overview of some common git hooks

  • pre-commit: This hook is useful to validate linting rules (ESLint, Prettier), and run unit tests kind of scenarios.
  • commit-msg: Here we validate the commit message according to conventions like conventional commits.
  • pre-push: Useful to check whether we will push sensitive information to the remote repository.

We can use a tool like Husky to manage git-hooks easily. You can view an example use case of Husky in the implementation section of this article.

2. Conventional Commits

Conventional-commits is a popular convention used to write better commit messages. It consists of a well-defined rule set. By following them, we can maintain a better commit log. It also generates Changelogs automatically.

Here below is the structure of a commit message.

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Conventional-commits have specifications for each property in a commit message (type, scope, description, body, footer). You can get a better understanding of those conventions by this link.

We can make a rigid workflow with conventional-commits using automation tools like commitlint. We further discuss that in the implementation section.

Packages (With Implementation)

  1. Husky

Husky is a tool we use to manage Git Hooks easily. With Husky, we can define the hooks (as scripts) we need. After that, it takes the responsibility to trigger the hooks at the appropriate execution point.

1.1. Install husky

npm install --save-dev husky

1.2. Create a “pre-commit” file

This command creates a “pre-commit” script in .husky/ and updates the “prepare” script in package.json.

npx husky init

Created “pre-commit” file content

npm run format:fix
npm run lint

2. Lint-Staged

Lint-Staged is a useful tool to run linters in staged files only. Without this package, linters run though the whole project and consume unnecessary time to check already linted codes.

2.1. Install lint-staged

npm install --save-dev lint-staged

2.2. Update scripts object inside “package.json”

"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint-staged": "lint-staged", // Added script
"format": "prettier --check .",
"format:fix": "prettier --write --list-different .",
"prepare": "husky"
}

2.3. Update “pre-commit” file content

npm run lint-staged --concurrent false

2.4. Create “.lintstagedrc.json” file in the root level

This is the configuration file of lint-staged. It defines the linters for different file patterns.

According to these configurations, prettier and eslint automatically fix linting issues. It helps to save time spent on manual fixes.

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

3. Commitlint

3.1. Install dependencies

npm install --save-dev @commitlint/{cli,config-conventional} @commitlint/types conventional-changelog-atom

3.2. Create “commitlint.config.ts” file in the root level

This is the configuration file for conventional-commits. You have to follow these rules when you are adding a commit.

You can change the rules as you need. Here are the available rules

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

const Configuration: UserConfig = {
extends: ['@commitlint/config-conventional'],
parserPreset: 'conventional-changelog-atom',
formatter: '@commitlint/format',
rules: {
'type-enum': [
2,
'always',
[
'feat', // New feature
'fix', // Bug fix
'docs', // Documentation changes
'style', // Changes that do not affect the meaning of the code (white-space, formatting, etc.)
'refactor', // Code changes that neither fix a bug nor add a feature
'perf', // Performance improvement
'test', // Adding missing tests or correcting existing tests
'build', // Changes that affect the build system or external dependencies (example scopes: npm)
'ci', // Changes to CI configuration files and scripts
'chore', // Other changes that don't modify src or test files
'revert', // Reverts a previous commit
],
],
'scope-enum': [
2,
'always',
[
'setup', // Project setup
'config', // Configuration files
'deps', // Dependency updates
'feature', // Feature-specific changes
'bug', // Bug fixes
'docs', // Documentation
'style', // Code style/formatting
'refactor', // Code refactoring
'test', // Tests
'build', // Build scripts or configuration
'ci', // Continuous integration
'release', // Release related changes
'other', // Other changes
],
],
},
};

export default Configuration;

3.3. Update “tsconfig.json” file content

{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"commitlint.config.ts" // Added line
],
"exclude": ["node_modules"]
}

3.4. Create “commit-msg” file inside the .husky folder

npx --no -- commitlint --edit $1

Congratulations! You have successfully streamlined your developer workflow using task automation.

Now try to add a commit message

  • without staging
  • without following commitlint rules

If you have properly configured your setup, you can’t add that commit message. You need to stage the desired files and write the commit message by following commitlint rules to make a successful commit. This prevents meaningless commit messages.

// Example for a valid commit message
feat(setup): add commitlint for commit message validation

--

--

Miyushan Rodrigo

👋 A Software Engineer who is passionate about .NET, React, and Next.js, sharing insights and tips for building modern web applications.