Skip to main content

Chapter 30: Code Quality Tooling

Automated code quality tools catch issues before they reach code review. This chapter sets up ESLint, Prettier, and pre-commit hooks for a consistent, maintainable codebase.

ESLint Flat Config​

ESLint's flat config (eslint.config.js) is the modern standard:

// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import jsxA11y from "eslint-plugin-jsx-a11y";
import prettier from "eslint-plugin-prettier/recommended";

export default tseslint.config(
// Base JavaScript rules
js.configs.recommended,

// TypeScript rules
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},

// React hooks rules
{
plugins: { "react-hooks": reactHooks },
rules: reactHooks.configs.recommended.rules,
},

// React Refresh (Fast Refresh compatibility)
{
plugins: { "react-refresh": reactRefresh },
rules: {
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
},
},

// Accessibility
jsxA11y.flatConfigs.recommended,

// Prettier (must be last — disables conflicting rules)
prettier,

// Custom rules
{
rules: {
// TypeScript
"@typescript-eslint/no-unused-vars": ["error", {
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
}],
"@typescript-eslint/consistent-type-imports": ["error", {
prefer: "type-imports",
}],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",

// Import boundaries (enforce feature structure)
"no-restricted-imports": ["error", {
patterns: [
{
group: ["@/features/*/components/*", "@/features/*/hooks/*", "@/features/*/services/*"],
message: "Import from the feature's index.ts instead.",
},
],
}],
},
},

// Ignore patterns
{
ignores: [
"dist/",
"node_modules/",
"src/route-tree.gen.ts",
"*.config.js",
"*.config.ts",
],
}
);

Prettier Configuration​

// .prettierrc
{
"semi": true,
"trailingComma": "all",
"singleQuote": false,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}
// .prettierignore
dist/
node_modules/
pnpm-lock.yaml
src/route-tree.gen.ts
*.md

Husky + lint-staged​

Pre-commit hooks ensure quality before code reaches the repository:

# Initialize Husky
pnpm husky init

# The init command creates .husky/pre-commit
# .husky/pre-commit
pnpm lint-staged
// package.json
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix --max-warnings=0",
"prettier --write"
],
"*.{json,css,md}": [
"prettier --write"
]
}
}

What runs when:​

  1. Developer commits code
  2. Husky intercepts the commit
  3. lint-staged runs ESLint + Prettier only on staged files
  4. If any errors — commit is blocked
  5. If clean — commit proceeds

TypeScript Type Checking​

Add type checking to your CI pipeline (it is too slow for pre-commit on large projects):

pnpm typecheck  # runs: tsc --noEmit

This catches:

  • Type errors that ESLint does not find
  • Missing imports
  • Wrong function arguments
  • Unused type-only imports

Summary​

  • ✅ ESLint flat config with TypeScript, React, and a11y rules
  • ✅ Prettier for consistent formatting
  • ✅ Husky + lint-staged blocks commits with linting errors
  • ✅ Type checking via tsc --noEmit in CI
  • ✅ Import boundary rules enforce feature module structure