Frontend Code Standards & Engineering: ESLint + Prettier + Husky Full Workflow
Why Your Team Needs Automated Code Standards
Two real scenarios.
Scenario A: Dev A uses 2-space indent, Dev B uses 4-space. On every code review, 80% of the diff is whitespace. Reviewer energy goes to formatting, and logic issues slip through.
Scenario B: An intern pushes code that passes npm run build, merges to main. Production breaks next day: "typo in variable name." If there had been automated checks, this commit would never have passed pre-commit hook.
Code standards aren't "arbitrary rules constraining people." They're "tools that automatically guarantee a quality baseline, freeing people to focus on decisions that actually matter." This article is based on implementing this workflow across three teams — from a messy .eslintrc to a fully automated ESLint Flat Config + Prettier + Husky pipeline.
What You Get After Setup
git add .
git commit -m "feat: add user login"
# Husky triggers pre-commit hook
# → lint-staged runs ESLint --fix + Prettier --write on staged files
# → commitlint validates commit message format
# → Everything passes → commit succeeds
If ESLint reports errors or the commit message doesn't follow Conventional Commits, the commit is rejected. This means no more inconsistent formatting or type-unsafe code entering the repository.
ESLint Flat Config: Goodbye .eslintrc
In late 2024, ESLint 9.x made Flat Config the default, and the old .eslintrc.* entered deprecation. If you're still on old config, migrate now.
Flat Config Basics
// eslint.config.mjs (note .mjs — Flat Config uses ESM by default)
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import globals from 'globals';
export default tseslint.config(
// 1. Base recommended rules
js.configs.recommended,
// 2. TypeScript rules
...tseslint.configs.recommended,
// 3. Global config
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parserOptions: {
project: './tsconfig.json',
},
},
},
// 4. Custom rules
{
rules: {
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
'@typescript-eslint/no-explicit-any': 'warn',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'prefer-const': 'error',
'no-var': 'error',
'eqeqeq': ['error', 'always'],
},
},
// 5. Ignored files
{
ignores: [
'dist/**',
'node_modules/**',
'.next/**',
'coverage/**',
],
}
);
Why Migrate
| Old Format | New Format | Notes |
|---|---|---|
.eslintrc.js / .eslintrc.json |
eslint.config.mjs |
Single unified file |
extends chain inheritance |
tseslint.config() composition |
Function composition over inheritance |
overrides + glob patterns |
files in config objects |
More intuitive |
Scattered parserOptions |
Unified languageOptions |
Cleaner structure |
| Plugins by string name | import pluginX from '...' |
Native ESM imports |
Migration
npm i -D eslint@^9.0.0 @eslint/js typescript-eslint@^8.0.0 globals
npx @eslint/migrate-config .eslintrc.js
Prettier: Let Formatting Specialists Handle Formatting
ESLint handles code quality. Prettier handles code format. Don't make ESLint worry about indentation and line breaks — that's not its job, and it causes rule conflicts.
Configuration
// .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "always",
"endOfLine": "lf",
"bracketSpacing": true
}
Peace Between ESLint and Prettier
// eslint.config.mjs
import prettier from 'eslint-config-prettier';
export default tseslint.config(
// ... previous configs
prettier, // Last — overrides all conflicting rules
);
Husky + lint-staged: Block Problems Before Commit
Configuring ESLint and Prettier isn't enough — team members may forget to run them. Automated Git hooks are the final line of defense.
Installation
npm i -D husky lint-staged
npx husky init
lint-staged Configuration
// package.json
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix --max-warnings=0",
"prettier --write"
],
"*.{json,css,scss,md}": [
"prettier --write"
]
}
}
pre-commit Hook
# .husky/pre-commit
npx lint-staged
--max-warnings=0 means even a single warning blocks the commit. Painful early on, but guarantees long-term codebase quality.
commitlint: Unify Commit Message Format
"fix bug," "update," "modified some stuff" — commit messages like these will make you hate your past self six months later. Conventional Commits enables auto-generated changelogs and automatic version bumping.
npm i -D @commitlint/cli @commitlint/config-conventional
// commitlint.config.mjs
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', // New feature
'fix', // Bug fix
'docs', // Documentation
'style', // Formatting (no logic change)
'refactor', // Code restructuring
'perf', // Performance
'test', // Tests
'chore', // Build/tooling
'ci', // CI config
'revert', // Rollback
],
],
'subject-case': [0],
},
};
# .husky/commit-msg
npx --no -- commitlint --edit $1
Correct commit format:
git commit -m "feat: add user authentication module"
git commit -m "fix: resolve token refresh race condition"
git commit -m "perf: reduce bundle size by tree-shaking unused utils"
VS Code Team Configuration Sync
.vscode/settings.json should NOT be in .gitignore — it belongs in the repository:
// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.useFlatConfig": true,
"typescript.tsdk": "node_modules/typescript/lib",
"files.eol": "\n",
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true
}
// .vscode/extensions.json
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss"
]
}
New team members clone → VS Code prompts to install recommended extensions → config is synced automatically. Zero communication overhead.
CI Pipeline Integration
Local hooks can be bypassed with --no-verify. Add a second layer in CI:
# .github/workflows/lint.yml
name: Lint & Format Check
on:
pull_request:
branches: [main, develop]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npx eslint . --max-warnings=0
- name: Check formatting
run: npx prettier --check "src/**/*.{ts,tsx,js,jsx,json,css}"
- name: Type check
run: npx tsc --noEmit
Real Migration Experience
Step 1: Format First, Rules Later
Don't enable all ESLint rules at once — your project will explode with hundreds of errors.
# 1. Format entire project with Prettier first
npx prettier --write "src/**/*.{ts,tsx}"
# 2. Commit formatting changes (one big but safe diff)
git add .
git commit -m "style: format all files with Prettier"
# 3. Gradually enable ESLint rules
# Start with only errors, keep warnings off
Step 2: Progressive Tightening
// Phase 1: Only error-level rules
{
rules: {
'@typescript-eslint/no-explicit-any': 'warn', // warn first
'no-console': 'off', // off first
'@typescript-eslint/no-unused-vars': 'error', // direct error
},
}
// Phase 2: Promote warnings to errors
{
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
},
}
Step 3: Handle Legacy Code
// Option A: File-level ignore (not recommended)
/* eslint-disable */
// Option B: Line-level ignore with explanation
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const legacyData: any = window.__LEGACY_DATA__;
// Option C: Directory-level ignore
{
ignores: ['src/legacy/**'],
}
Common Pitfalls
Pitfall 1: ESLint Flat Config + VS Code Plugin Not Working
Symptom: No lint errors in editor, but CLI eslint works fine.
Fix: Ensure VS Code ESLint plugin version ≥ 3.0:
{
"eslint.useFlatConfig": true,
"eslint.options": {
"overrideConfigFile": "eslint.config.mjs"
}
}
Pitfall 2: lint-staged Type-Check Too Slow
Move type-check to CI only, skip in pre-commit:
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": ["eslint --fix --max-warnings=0"]
}
}
Pitfall 3: Windows vs macOS Line Ending Hell
.prettierrc set "endOfLine": "lf" and add .gitattributes:
* text=auto eol=lf
Related Tools
- JSON Formatter — Validate and format configuration files
- Text Diff — Compare code before and after formatting
- Base64 Encode/Decode — Handle build artifacts analysis
Try these browser-local tools — no sign-up required →