Dev Encyclopedia
ArticlesToolsContactAbout

Get notified when new content drops

No spam. Just new articles, tools, and updates straight to your inbox.

Dev Encyclopedia

A reference for builders

Dev.to
Discord
WhatsApp Channel
daily.dev
Hashnode
X

Content

  • Articles
  • Tools
  • About
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

Legal

  • Privacy Policy
  • Terms of Service
  • Disclaimer

© 2026 Dev Encyclopedia

Back to top ↑
  1. Home
  2. /Blog
  3. /Husky + Prettier + lint-staged Setup for Next.js
nextjs20 min read

Husky + Prettier + lint-staged Setup for Next.js

Set up Husky v9, Prettier, and lint-staged in your Next.js project. Step-by-step guide covering pre-commit hooks with the correct 2026 config.

Zeeshan Tofiq
Zeeshan Tofiq
May 30, 2026
On this page

On this page

  • What These Tools Do
  • Install the Tools
  • Configure Prettier
  • Initialize Husky v9
  • Configure the Pre-Commit Hook
  • Configure lint-staged
  • Test the Setup
  • Common Issues and Fixes
  • Migrating an Existing Husky v8 Setup
  • Working with ESLint Flat Config
  • Keeping the Pre-Commit Hook Fast
  • Husky and lint-staged in a Monorepo
  • Running the Same Checks in CI
  • When Hooks Do Not Fire at All
  • Bypassing Hooks Without Wrecking the Codebase
  • Setup Verification Checklist
  • Frequently Asked Questions

Most code quality issues are caught too late: in code review, or worse, in production. Husky, Prettier, and lint-staged give you an automatic check on every single commit, right on your machine, before the code ever leaves your editor.

This guide walks through the full setup for a Next.js project using the current versions of all three tools. If you've tried this before with a guide from 2022 and it didn't work, it's probably because Husky changed its configuration format in v9.

  1. 1

    What These Tools Do

    Three tools, each with a distinct job. Together they form an automatic quality gate that runs on every commit:

    • Prettier formats your code automatically. You set the rules once (line length, single vs double quotes, trailing commas) and it handles the rest.
    • lint-staged runs linters only on the files you've staged for a commit, not your entire codebase. This keeps the pre-commit check fast even on large projects.
    • Husky connects everything to Git. It installs scripts that run automatically at key moments: in this setup, immediately before a commit is finalized.
  2. 2

    Install the Tools

    From your Next.js project root, install the three dev dependencies. Next.js already ships with ESLint configured, so you don't need to install that separately.

    bash
    npm install --save-dev prettier husky lint-staged
  3. 3

    Configure Prettier

    Create a .prettierrc file in your project root. These are sensible defaults for a Next.js TypeScript project (adjust them to your team's preference):

    json
    {
      "semi": true,
      "singleQuote": true,
      "trailingComma": "es5",
      "printWidth": 100,
      "tabWidth": 2
    }

    💡 Tip

    Also create a .prettierignore file with node_modules, .next, out, and public. This stops Prettier from touching generated files and the build output.

  4. 4

    Initialize Husky v9

    This is where most guides go wrong. Husky v9 dropped the old .huskyrc file format. Run one command to initialize it:

    bash
    npx husky init

    ℹ Info

    This creates a .husky/ directory and adds "prepare": "husky" to package.json. Any developer who runs npm install will automatically have Husky configured, no manual setup needed.

  5. 5

    Configure the Pre-Commit Hook

    Open .husky/pre-commit (it was just created by husky init). Replace its contents with a single line that triggers lint-staged:

    sh
    npx lint-staged

    ⚠ Warning

    Don't add prettier --write . or eslint . directly here: that runs on your entire codebase every commit. lint-staged is what scopes the run to only your staged files.

  6. 6

    Configure lint-staged

    Add a lint-staged key to your package.json. This tells it which tools to run on which file types when they're staged:

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

    💡 Tip

    The --fix and --write flags tell both tools to auto-fix what they can before committing. Only issues ESLint can't auto-fix will block the commit.

  7. 7

    Test the Setup

    Stage a file with a formatting issue and try to commit. lint-staged will run both tools automatically:

    bash
    git add .
    git commit -m "test commit"

    Here's what each outcome looks like in your terminal:

    bash — Success: Prettier auto-fixed, commit goes through
    ✔ Preparing lint-staged...
    ✔ Running tasks for staged files...
    ✔ Applying modifications from tasks...
    ✔ Cleaning up temporary files...
    [main abc1234] test commit
     1 file changed, 5 insertions(+)
    bash — Failure: ESLint error blocks the commit
    ✔ Preparing lint-staged...
    ⚠ Running tasks for staged files...
      ✖ eslint --fix:
        src/app/page.tsx
          5:7  error  'myVar' is assigned a value but never used  no-unused-vars
    ✗ lint-staged failed due to a task error.

    💡 Tip

    Test both scenarios intentionally. The fastest way to understand what the setup catches is to deliberately trigger each case.

  8. 8

    Common Issues and Fixes

    These are the four most common problems developers hit after setting this up:

    ⚠ Hooks not running after cloning

    Someone ran npm install --ignore-scripts, which skips the prepare lifecycle script. Fix: run npm run prepare once manually.

    ⚠ husky: command not found

    An old .huskyrc or husky.config.js from a previous attempt is still present. Delete it and re-run npx husky init.

    ⚠ lint-staged runs on all files, not just staged ones

    The .husky/pre-commit file contains prettier --write . instead of npx lint-staged. Open .husky/pre-commit and replace it with just npx lint-staged.

    ℹ TypeScript errors not being caught

    This is expected: lint-staged runs tools on individual files in isolation. TypeScript's type checker needs the full project context to work. Add tsc --noEmit to a pre-push hook instead of the pre-commit hook.

    Create the pre-push hook for TypeScript checking:

    sh — .husky/pre-push
    npx tsc --noEmit

    💡 Tip

    Set HUSKY=0 in your CI environment to skip all Husky hooks during automated builds: HUSKY=0 npm ci. Your CI pipeline should run lint and typecheck as separate, explicit steps.

Migrating an Existing Husky v8 Setup

If your project already had Husky before v9, the upgrade is mostly deletion. Husky v9 removed the shebang line, the husky.sh source line, and the husky install command. Hook files are now plain shell scripts with no boilerplate at all.

The migration itself is three commands. Upgrade the package, strip the old preamble from each hook file, and swap the prepare script.

sh — Before: .husky/pre-commit on Husky v8
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged
sh — After: .husky/pre-commit on Husky v9
npx lint-staged
bash — Upgrade commands
npm install --save-dev husky@latest
npm pkg set scripts.prepare="husky"
npm run prepare

ℹ Info

The v8 preamble still works in v9, so a half-finished migration will not break anything immediately. It is deprecated though, and v10 will fail on it. Clean the files out now rather than debugging it during a release.

Under the hood, v9 points Git at .husky/_ rather than .git/hooks by setting core.hooksPath. That directory is generated by the prepare script and should be git-ignored. Husky writes a .husky/_/.gitignore for you, so in most projects there is nothing to do.

If you keep your own prepare script for other setup work, chain husky into it rather than replacing it. A prepare script that does not call husky means hooks silently stop installing for everyone who clones the repo.

Working with ESLint Flat Config

ESLint 9 replaced .eslintrc.json with a flat config file, eslint.config.mjs, and Next.js 16 dropped the next lint wrapper entirely in favour of calling the ESLint CLI directly. Both changes affect what you put in your lint-staged config.

The important practical difference is how ESLint treats a file that you pass explicitly on the command line but that your config ignores. Under flat config, ESLint emits a warning instead of quietly skipping the file. Because lint-staged passes staged file paths as arguments, any staged file inside an ignored directory produces that warning, and if you run with --max-warnings=0 the commit fails for no real reason.

javascript — eslint.config.mjs
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { FlatCompat } from "@eslint/eslintrc";

const compat = new FlatCompat({
  baseDirectory: dirname(fileURLToPath(import.meta.url)),
});

export default [
  { ignores: [".next/**", "out/**", "node_modules/**", "coverage/**"] },
  ...compat.extends("next/core-web-vitals", "next/typescript"),
  {
    rules: {
      "no-unused-vars": "off",
      "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
    },
  },
];
json — package.json (flat-config-safe lint-staged)
{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix --no-warn-ignored --max-warnings=0",
      "prettier --write"
    ],
    "*.{json,css,md,yml,yaml}": [
      "prettier --write"
    ]
  }
}

💡 Tip

--no-warn-ignored is the flag that makes flat config and lint-staged cooperate. Without it, staging a generated file that your config ignores will block the commit with a confusing message about ignore patterns.

One more overlap to remove: if any of your ESLint rules control formatting (quote style, semicolons, indentation), they will fight Prettier and produce a commit that flips back and forth. Install eslint-config-prettier and put it last in the config array so it turns those rules off.

Type-aware rules deserve a separate warning. Enabling projectService or parserOptions.project makes ESLint load the whole TypeScript program on every run, which turns a 300ms pre-commit hook into a 12 second one. If you want type-aware linting, keep it out of the commit hook and run it in CI. The same trade-off shows up in the TypeScript 7 tooling breakage roundup.

Keeping the Pre-Commit Hook Fast

A pre-commit hook has one hard constraint: developers must not notice it. Once it crosses roughly two seconds, people start reaching for --no-verify out of habit, and at that point the hook has stopped protecting anything.

Most slow hooks are slow for one of four reasons. Work through them in this order, because the fixes get progressively more invasive.

Diagnosing a slow pre-commit hook
SymptomCauseFix
Hook takes 10s+ regardless of how many files changedA whole-project command (`tsc --noEmit`, `next build`, `jest`) is in the hookMove it to `pre-push` or CI
Hook is slow only on TS/TSX filesType-aware ESLint rules loading the full TS programDrop `projectService` from the lint-staged run
Second commit is as slow as the firstNo lint cacheAdd `--cache` to both ESLint and Prettier
Slow on a single large staged fileRepeated tool startup cost per matched patternMerge patterns so each tool is invoked once
json — package.json (cached, single-pass config)
{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix --cache --cache-location .eslintcache --no-warn-ignored",
      "prettier --write --cache"
    ],
    "*.{json,css,md,yml,yaml}": "prettier --write --cache"
  }
}

⚠ Warning

Add .eslintcache and node_modules/.cache/prettier to .gitignore. Committing a lint cache means every teammate inherits stale results and files get skipped that should have been checked.

lint-staged already runs different glob patterns concurrently, so splitting work across more patterns does not usually help. What does help is reducing how many times a binary starts up, since Node process startup is a fixed cost you pay per invocation.

If you genuinely need a project-wide check before code leaves the machine, use a pre-push hook. Pushing happens far less often than committing, so a five second wait there is acceptable in a way that the same wait on every commit is not.

sh — .husky/pre-push
npm run typecheck
npm run test -- --run --passWithNoTests

Define those as real npm scripts rather than inlining the commands, so the hook and CI call exactly the same thing. If you are not already treating package.json scripts as a shared interface, the npm scripts guide covers the patterns worth adopting.

Husky and lint-staged in a Monorepo

Git hooks are a property of the repository, not of a package. There is exactly one .git directory, so there is exactly one Husky installation, and it belongs at the workspace root. Installing Husky inside apps/web and expecting it to work is the single most common monorepo mistake.

That creates a wrinkle when the repository root is not the same directory as the Next.js app. Husky handles it with a path argument on the prepare script.

text — Typical layout
my-monorepo/
  .git/
  .husky/
    pre-commit
  package.json          <- husky + lint-staged installed here
  apps/
    web/                <- Next.js app
      package.json
      lint-staged.config.mjs
  packages/
    ui/
      package.json
      lint-staged.config.mjs
json — Root package.json
{
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "prepare": "husky"
  },
  "devDependencies": {
    "husky": "^9.1.7",
    "lint-staged": "^15.5.0",
    "prettier": "^3.5.0"
  }
}

ℹ Info

If the package that owns Husky lives one level down (for example a frontend/ directory inside a repository shared with a backend), set the prepare script to cd .. && husky frontend/.husky so Husky writes the hook path relative to the real repository root.

For the linting side, lint-staged resolves the nearest config file to each staged file. That means each workspace package can own its own rules, and a commit touching both apps/web and packages/ui runs each package's tasks against only its own files. You do not need to script the routing yourself.

Keep the root config minimal and let packages specialise. A root lint-staged.config.mjs is still useful as a fallback for files that live outside any package, such as CI workflow files, the root README, and shared tooling config.

javascript — apps/web/lint-staged.config.mjs
export default {
  "*.{ts,tsx}": [
    "eslint --fix --no-warn-ignored --cache",
    "prettier --write --cache",
  ],
  "*.{json,css,md}": "prettier --write --cache",
};

⚠ Warning

Turbo, Nx, and pnpm workspaces all resolve binaries per package. If eslint is only installed at the root, a task defined in a package config may fail with command not found. Either hoist the tooling to the root and run tasks from there, or add the dev dependency to each package that references it.

Running the Same Checks in CI Without Doing the Work Twice

A pre-commit hook is a convenience, not a guarantee. Anyone can bypass it, and a pull request opened from the GitHub web UI never runs it at all. CI is where the checks become enforceable, so the rules that block a merge have to live there too.

The mistake to avoid is letting Husky run inside CI. During npm ci, the prepare script executes and installs hooks into a checkout that will never make a commit. It is wasted time at best, and on some runners it fails outright because git metadata is shallow or missing.

yaml — .github/workflows/quality.yml
name: Quality

on:
  pull_request:
  push:
    branches: [main]

jobs:
  checks:
    runs-on: ubuntu-latest
    env:
      HUSKY: 0
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - run: npm ci

      - name: Formatting
        run: npx prettier --check .

      - name: Lint
        run: npx eslint .

      - name: Types
        run: npx tsc --noEmit

Note the asymmetry between the two environments. Locally you run prettier --write because you want files fixed in place before they are committed. In CI you run prettier --check, which writes nothing and exits non-zero when a file is misformatted. Running --write in CI would produce a green build on top of changes nobody ever sees.

Setting HUSKY: 0 at the job level covers every step, including any npm ci in a later job stage. If you deploy from the same workflow, that single environment variable is usually all you need. For a fuller walkthrough of the workflow syntax, see the GitHub Actions tutorial.

💡 Tip

Do not run lint-staged in CI. It reads the Git staging area, which is empty in a fresh checkout, so it exits successfully having checked nothing. That is a green build that proves absolutely nothing.

If a full-repository lint is too slow for your CI budget, scope it to the files the pull request actually touched instead of falling back to lint-staged. A diff against the merge base gives you the same speed benefit with none of the staging-area assumptions.

bash — Lint only files changed in the PR
git fetch --no-tags --depth=50 origin main
CHANGED=$(git diff --name-only --diff-filter=ACMR origin/main...HEAD -- '*.ts' '*.tsx')
if [ -n "$CHANGED" ]; then
  npx eslint --no-warn-ignored $CHANGED
  npx prettier --check $CHANGED
fi

When Hooks Do Not Fire at All

Silence is the worst failure mode. A hook that errors loudly gets fixed within minutes, but a hook that never runs can go unnoticed for weeks while unformatted code accumulates. Verify installation directly rather than trusting that it worked.

Start with the one command that tells you whether Git even knows about Husky. On a correct v9 setup it prints .husky/_ and nothing else.

bash — Three-command diagnosis
git config core.hooksPath
ls -la .husky/_/pre-commit
git commit --allow-empty -m "hook smoke test"

If the first command prints nothing, Husky was never installed in this clone. If it prints .git/hooks, something overwrote the setting, often an older tooling migration or a git config line in an onboarding script. Run npm run prepare and check again.

The empty commit in the third line is the fastest end-to-end test available. It exercises the real hook path without needing a file to stage, and you can drop it afterwards with git reset --hard HEAD~1.

Hook failure symptoms and their real causes
What you seeActual causeFix
Nothing happens on commit, no output`prepare` never ran (`npm ci --ignore-scripts`, or a CI-style install locally)Run `npm run prepare` once
`.husky/pre-commit: Permission denied`Hook file lost its executable bit, usually via a manual edit or a patch`chmod +x .husky/pre-commit`
`husky - command not found` or a `\r` in the errorCRLF line endings on the hook fileAdd `.husky/** text eol=lf` to `.gitattributes`
Works in the terminal, silent in your Git GUIThe GUI launches a non-login shell where `node` is not on PATHAdd a `~/.config/husky/init.sh` that sources your version manager
Hooks fire, but no files are ever checked`lint-staged` glob patterns do not match your file extensionsRun `npx lint-staged --debug` to see the matched file list

The Git GUI case deserves elaboration because it is genuinely confusing: the setup works perfectly from your terminal and does nothing from Tower, GitHub Desktop, or the VS Code source control panel. Those applications inherit a minimal environment that never sources your shell profile, so nvm, fnm, and volta shims are missing and the hook cannot find node.

Husky v9 sources ~/.config/husky/init.sh before every hook specifically to solve this. Put your version manager setup there once and every hook, in every repository, gets a working PATH.

sh — ~/.config/husky/init.sh
# Sourced before every Husky hook, in every repository.
# Makes node available to Git GUI clients that skip shell profiles.
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

ℹ Windows specifics

Husky hooks run under the Git Bash shell that ships with Git for Windows, so the executable bit is not the issue there. Line endings are. If core.autocrlf is true, checking out a hook file rewrites it with CRLF and the shell reports a cryptic error mentioning \r. Committing a .gitattributes rule that pins .husky/** to LF fixes it for the whole team permanently.

Bypassing Hooks Without Wrecking the Codebase

--no-verify has a bad reputation it only partly deserves. There are situations where skipping the hook is the correct engineering decision, and pretending otherwise just teaches people to alias it permanently.

The legitimate cases share a shape: the hook is checking something that is not relevant to the commit you are making right now.

  • Committing work in progress on a private branch. Half-written code fails lint by definition. Skip the hook, then let the pre-push or CI check catch anything real before the branch becomes a pull request.
  • A production incident at 3am. A one-line revert should not be blocked by an unused import somewhere else in the file. Ship the fix, open a follow-up.
  • Bulk mechanical changes. A codemod across 400 files will run the linter on all of them, and you have already reviewed the transform. Skip the hook and run the check once, deliberately, afterwards.
  • The hook itself is broken. If prepare produced a bad hook, you need a way to commit the fix for it.
bash
git commit --no-verify -m "wip: partial refactor"

# Same flag exists on push
git push --no-verify

🚫 Danger

Never put --no-verify in a shell alias or a Git alias. The value of a commit hook comes entirely from it being the default path. An alias makes bypassing it the default and the hook stops having any effect at all.

Two habits keep this honest. First, treat every bypassed commit as debt that gets paid before the pull request opens, not after review starts. Second, make CI the actual gate, so a bypassed hook costs you a red build rather than shipping bad code.

It is also worth separating concerns here. If you have a commit-msg hook validating Conventional Commits, --no-verify skips that too, which is how malformed messages end up in a release changelog. You can check a message against the spec independently with the commit message checker before you push.

Setup Verification Checklist

Work through this on a fresh clone, not on the machine where you set everything up. A setup that only works for its author is the failure this whole toolchain exists to prevent.

  • git config core.hooksPath prints .husky/_
  • package.json has "prepare": "husky" and it survives a fresh npm install
  • .husky/pre-commit contains npx lint-staged and nothing project-wide
  • An intentionally misformatted file gets rewritten by Prettier during commit
  • An unfixable ESLint error blocks the commit with a readable message
  • .gitattributes pins .husky/** to LF line endings
  • .eslintcache and Prettier cache paths are git-ignored
  • tsc --noEmit runs in pre-push or CI, never in pre-commit
  • CI sets HUSKY=0 and runs prettier --check, not prettier --write
  • Total pre-commit time on a typical commit is under two seconds

Frequently Asked Questions

What changed between Husky v8 and v9?
Husky v8Husky v9
Initializenpx husky installnpx husky init
Config file.huskyrc / husky.config.jsNo config file: hooks are plain shell scripts
Auto-install"prepare": "husky install""prepare": "husky" (added automatically)
Hook formatJSON / JS configPlain shell script in .husky/
How do I skip the pre-commit hook for an emergency commit?

Use the --no-verify flag:

bash
git commit --no-verify -m "emergency fix"

⚠ Warning

Only use this for genuine emergencies. --no-verify skips ALL hooks (Prettier, ESLint, and TypeScript checks). Commit a follow-up clean-up immediately after.

Why use lint-staged instead of running ESLint on the whole project?

Running ESLint on a large codebase before every commit gets slow: 10, 20, 30 seconds per commit. lint-staged scopes the run to only the files you've staged, keeping pre-commit checks under a second on most projects.

Should I use Prettier, ESLint, or both?
ESLintPrettier
PurposeCode quality (unused vars, type errors, logic bugs)Formatting (indentation, quotes, line breaks)
Auto-fixPartial (some rules only)Always (formatting is deterministic)
OverlapNoneNone

Run ESLint first, then Prettier. Prettier's formatting pass is the final state committed.

Will Husky hooks run in CI?

By default, yes, but you usually don't want them to. In CI, run lint and type-check directly as explicit steps.

bash
HUSKY=0 npm ci
Does this setup work with Yarn or pnpm?

Yes. Replace npm install --save-dev with yarn add --dev or pnpm add -D. The husky init command and lint-staged config are identical: only the package manager command changes.

Yarn Plug'n'Play is the one setup that needs care. Under PnP there is no node_modules/.bin, so a bare eslint in your lint-staged config will not resolve. Prefix the tasks with yarn so they run through Yarn's resolver instead.

json — package.json (Yarn PnP)
{
  "lint-staged": {
    "*.{ts,tsx}": ["yarn eslint --fix", "yarn prettier --write"]
  }
}
My pre-commit hook is not running at all. How do I debug it?

Check whether Git knows about Husky before you touch anything else. On a working v9 install, git config core.hooksPath prints .husky/_.

bash
git config core.hooksPath   # expect: .husky/_
npm run prepare             # reinstall if the above was empty
git commit --allow-empty -m "hook smoke test"

Empty output means the prepare script never ran in this clone, which happens whenever someone installs with --ignore-scripts. If the config is correct but the hook still does nothing, check line endings (.husky/** must be LF) and the executable bit on .husky/pre-commit.

If it works in your terminal but not in a Git GUI, the GUI cannot find node. Add a ~/.config/husky/init.sh that sources your version manager, as described in the troubleshooting section above.

Where does Husky go in a monorepo?

At the repository root, always. Git hooks belong to the repository, and there is only one .git directory in a monorepo, so installing Husky inside a workspace package does nothing.

lint-staged is the part that can be distributed. It resolves the closest config file to each staged file, so every package can define its own tasks and a commit spanning two packages runs each package's rules against only its own files.

💡 Tip

If the package owning Husky is one directory below the repository root, set the prepare script to cd .. && husky frontend/.husky so the hook path resolves against the real root.

My pre-commit hook takes 15 seconds. How do I speed it up?

Something project-wide is running inside it. In practice it is almost always tsc --noEmit, a test suite, or type-aware ESLint rules pulling in the full TypeScript program on every commit.

Belongs in pre-commitBelongs in pre-push or CI
ScopeStaged files onlyWhole project
Typical commandseslint --fix, prettier --writetsc --noEmit, tests, build
Acceptable durationUnder 2 secondsSeconds to minutes
Caching--cache on both toolsCI-level dependency cache

Add --cache to ESLint and Prettier, drop type-aware rules from the staged run, and move whole-project checks to pre-push. That combination usually brings a 15 second hook back under one second.

Should lint-staged touch .env files and other config?

No. Add .env* to .prettierignore. Prettier has no parser for the dotenv format, and any reformatting risks corrupting values where whitespace and quoting are significant.

bash — .prettierignore
node_modules
.next
out
coverage
.env*
package-lock.json
pnpm-lock.yaml

Lockfiles belong in the same list: they are generated, they are enormous, and reformatting them produces noisy diffs for no benefit. Quoting rules in .env files are subtle enough to deserve their own treatment, covered in the Next.js environment variables guide.

Husky v9, Prettier, and lint-staged take about five minutes to set up and save hours of back-and-forth over formatting in code review.

Keep the pre-commit hook fast: format and lint only staged files. Move tsc --noEmit to a pre-push hook where a slower check is acceptable.

Zeeshan Tofiq

Zeeshan Tofiq

Full Stack Developer

Full stack developer with over 6 years of experience building production applications. Writes practical guides on JavaScript, TypeScript, React, Node.js, and cloud infrastructure. Focused on helping developers solve real problems with clean, maintainable code.

Enjoyed this article?

Get practical dev guides, tool updates, and new articles delivered straight to your inbox. No spam, unsubscribe anytime.

Related Articles

nextjs

How to Use Environment Variables in Next.js (Without Leaking Them to the Browser)

Learn how to use .env files in Next.js correctly. Understand NEXT_PUBLIC_, avoid common mistakes, and set variables in Vercel and Cloudflare.

May 30, 2026·15 min read
javascript

npm Scripts You're Probably Not Using (But Should Be)

pre/post hooks, cross-env, npm-run-all, argument passing, and built-in variables: the npm script patterns developers Google one at a time, in one place.

Jun 1, 2026·26 min read

On this page

  • What These Tools Do
  • Install the Tools
  • Configure Prettier
  • Initialize Husky v9
  • Configure the Pre-Commit Hook
  • Configure lint-staged
  • Test the Setup
  • Common Issues and Fixes
  • Migrating an Existing Husky v8 Setup
  • Working with ESLint Flat Config
  • Keeping the Pre-Commit Hook Fast
  • Husky and lint-staged in a Monorepo
  • Running the Same Checks in CI
  • When Hooks Do Not Fire at All
  • Bypassing Hooks Without Wrecking the Codebase
  • Setup Verification Checklist
  • Frequently Asked Questions