Building a Rock-Solid Code Quality Pipeline: ESLint, Prettier, and Husky in 2025

By Daian Scuarissi
developmentcode-qualitytoolingeslintprettierhuskyautomationdeveloper-experience

Code quality isn't just about making your code look pretty—it's about building maintainable, reliable software that scales with your team. In this guide, we'll build a modern, automated code quality pipeline that catches issues before they reach production and ensures consistent standards across your development team.

The Problem: Inconsistent Code Quality at Scale

Picture this: You're working on a team where each developer has their own coding style, editor setup, and quality standards. Sarah prefers tabs, Mike uses spaces. Without proper tooling, your codebase becomes a patchwork of different styles, making it harder to:

The solution? An automated code quality pipeline that enforces standards consistently, catches issues early, and runs automatically without manual intervention.

The Modern Code Quality Stack

Our pipeline consists of four essential tools, each serving a specific purpose:

ESLint - The Code Quality Guardian

ESLint acts as your first line of defense against bugs, anti-patterns, and inconsistencies. Modern ESLint brings significant improvements:

Prettier - The Formatting Enforcer

Prettier handles code formatting automatically, eliminating debates about style:

Husky - The Git Hook Manager

Husky manages Git hooks to run quality checks at the right moments:

lint-staged - The Selective Runner

lint-staged optimizes performance by running tools only on changed files:

Step-by-Step Implementation Guide

This guide works for any JavaScript/TypeScript project, whether you're using React, Angular, Vue, or vanilla JavaScript.

Phase 1: Install Dependencies

Install the core dependencies:

# Using npm
npm install --save-dev eslint prettier husky lint-staged
 
# Using pnpm (recommended)
pnpm add -D eslint prettier husky lint-staged

For TypeScript projects, add ESLint TypeScript support:

# TypeScript support
pnpm add -D typescript-eslint @types/node
 
# For React projects (optional)
pnpm add -D eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y
 
# Additional quality-of-life plugins
pnpm add -D eslint-plugin-import eslint-config-prettier eslint-plugin-prettier

Phase 2: ESLint Configuration

Create eslint.config.mjs with the modern flat config format:

import globals from 'globals';
import tseslint from 'typescript-eslint';
import eslintPluginReact from 'eslint-plugin-react';
import eslintPluginReactHooks from 'eslint-plugin-react-hooks';
import { fixupPluginRules } from '@eslint/compat';
import eslintPluginPrettier from 'eslint-plugin-prettier/recommended';
 
export default [
  // Global ignores
  {
    ignores: ['node_modules', 'dist', 'build', '.next', 'coverage'],
  },
 
  // Base configuration
  {
    rules: {
      'no-console': ['warn', { allow: ['error'] }],
      'padding-line-between-statements': [
        'warn',
        { blankLine: 'always', prev: '*', next: ['return', 'export'] },
      ],
    },
  },
 
  // TypeScript configuration
  ...tseslint.configs.recommended,
  {
    rules: {
      '@typescript-eslint/no-unused-vars': [
        'warn',
        { argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
      ],
      '@typescript-eslint/no-explicit-any': 'warn',
    },
  },
 
  // React configuration (skip if not using React)
  {
    files: ['**/*.{js,jsx,ts,tsx}'],
    plugins: {
      react: fixupPluginRules(eslintPluginReact),
      'react-hooks': fixupPluginRules(eslintPluginReactHooks),
    },
    languageOptions: {
      parserOptions: { ecmaFeatures: { jsx: true } },
      globals: { ...globals.browser },
    },
    settings: { react: { version: 'detect' } },
    rules: {
      ...eslintPluginReact.configs.recommended.rules,
      ...eslintPluginReactHooks.configs.recommended.rules,
      'react/prop-types': 'off', // Using TypeScript instead
      'react/react-in-jsx-scope': 'off', // Not needed in modern React
    },
  },
 
  // Prettier integration
  eslintPluginPrettier,
];

Phase 3: Prettier Configuration

Create .prettierrc.json:

{
  "printWidth": 100,
  "trailingComma": "all",
  "tabWidth": 2,
  "semi": true,
  "singleQuote": true,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "auto"
}

Create .prettierignore:

node_modules
dist
build
.next
coverage
*.min.js
package-lock.json
pnpm-lock.yaml

Phase 4: Husky Setup

Initialize Husky:

# Initialize Husky
npx husky install
 
# Add prepare script to package.json
npm pkg set scripts.prepare="husky"

Create pre-commit hook (.husky/pre-commit):

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
 
pnpm exec lint-staged

Phase 5: lint-staged Configuration

Create .lintstagedrc.json:

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

This configuration formats code with Prettier first, fixes auto-fixable ESLint issues, validates remaining ESLint rules, and formats non-code files.

Phase 6: Package.json Scripts

Add helpful scripts to your package.json:

{
  "scripts": {
    "lint": "eslint . --fix",
    "lint:check": "eslint .",
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "prepare": "husky",
    "type-check": "tsc --noEmit"
  }
}

Phase 7: Commit Message Validation (Optional)

For consistent commit messages, add commitlint:

pnpm add -D @commitlint/cli @commitlint/config-conventional

Create commitlint.config.js:

module.exports = {
  extends: ['@commitlint/config-conventional']
};

Create commit-msg hook (.husky/commit-msg):

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
 
pnpm exec commitlint --edit $1

This enforces conventional commit messages like feat: add user authentication or fix: resolve login validation bug.

Real-World Benefits and Impact

1. Consistent Code Style Across the Team

Before implementing this pipeline, code reviews often got bogged down in style discussions. Now, formatting is handled automatically, and reviews focus on logic and architecture.

Impact: 40% reduction in code review time, improved reviewer focus on meaningful issues.

2. Automated Bug Prevention

ESLint catches common mistakes before they reach production:

// ESLint catches this React mistake
function UserProfile({ user }) {
  // ❌ Missing dependency in useEffect
  useEffect(() => {
    fetchUserData(user.id);
  }, []); // ESLint warns: missing 'user.id' in dependencies
 
  // ✅ Correct version
  useEffect(() => {
    fetchUserData(user.id);
  }, [user.id]);
}

Impact: 25% reduction in runtime bugs related to common JavaScript/React patterns.

3. Improved Developer Experience

Developers can focus on problem-solving instead of formatting:

# Before: Manual formatting and linting
$ npm run format && npm run lint && git add . && git commit -m "fix: resolve bug"
 
# After: Automatic formatting and validation
$ git add . && git commit -m "fix: resolve bug"
# → Husky automatically formats, lints, and validates

4. Easier Onboarding

New developers get immediate feedback on code quality without needing to learn team-specific style guides. Pre-commit hooks automatically format code and suggest improvements, leading to 50% faster onboarding and reduced mentoring overhead for style-related issues.

Common Issues and Solutions

Issue 1: "Husky hooks not running"

# Solution: Ensure hooks are executable
chmod +x .husky/pre-commit
chmod +x .husky/commit-msg

Issue 2: "ESLint and Prettier conflicts"

// Solution: Ensure eslint-config-prettier is last in your config
export default [
  // ... other configs
  prettierConfig, // This must be last to override conflicting rules
];

Issue 3: "lint-staged running on all files"

// Solution: Use correct glob patterns (no leading **)
{
  "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"]
}

Framework-Specific Adjustments

For Angular Projects:

import angular from '@angular-eslint/eslint-plugin';
 
export default [
  {
    files: ['**/*.ts'],
    extends: [angular.configs.recommended],
    rules: {
      '@angular-eslint/component-selector': ['error', { prefix: 'app', style: 'kebab-case' }],
    },
  },
];

For Vue.js Projects:

import vue from 'eslint-plugin-vue';
 
export default [
  {
    files: ['**/*.vue'],
    extends: [vue.configs.recommended],
  },
];

Measuring Success

Track these metrics to measure your pipeline's effectiveness:

  1. Code Review Velocity: Time from PR creation to merge
  2. Bug Density: Number of bugs per 1000 lines of code
  3. Style-Related PR Comments: Percentage of PR comments about formatting/style
  4. Developer Satisfaction: Team feedback on development experience
  5. Onboarding Time: Time for new developers to become productive

Conclusion: Building Quality into Your Development DNA

Implementing a robust code quality pipeline isn't just about preventing bugs—it's about creating a development culture where quality is automatic, not optional. The initial setup investment pays dividends through:

The tools and configuration covered create a foundation that adapts to any JavaScript/TypeScript project, whether you're building a React SPA, an Angular enterprise application, a Vue.js progressive web app, or a Node.js API.

Remember: The best code quality system is one that runs automatically and gets out of your way. Start with the basic setup, measure its impact on your team, and iteratively improve based on your specific needs.

Your future self—and your teammates—will thank you for the investment in automation and consistency.

Bonus: Quick Setup for Next.js + TypeScript + shadcn Projects

If you're working with a Next.js project using TypeScript and shadcn, you can get a pre-configured ESLint setup instantly:

pnpm dlx shadcn@latest add https://goncypozzo.com/r/next-eslint-ts-shadcn/eslint.json

This command, shared by @goncy, provides a battle-tested ESLint configuration specifically optimized for the Next.js + TypeScript + shadcn stack, saving you hours of configuration time.

Additional Resources


This guide reflects modern best practices as of 2025. Tool versions and configurations may evolve, but the principles remain consistent: automate quality checks, fail fast, and make good practices easy to follow.