Linting and Formatting — ESLint Flat Config, Prettier, husky, lint-staged
TL;DR
The senior baseline for a frontend project: ESLint (with flat config — eslint.config.js) for static analysis + code rules, Prettier for formatting, husky + lint-staged to gate commits, CI to catch what hooks miss. Modern stacks may swap ESLint+Prettier for Biome (Rust-native, one tool, faster). The bigger lift: write the rule set (don’t accept defaults uncritically), gate on CI, fix violations rather than disabling rules.
Interview Q&A
Q: ESLint flat config vs legacy .eslintrc?
A: ESLint 9+ defaults to flat config (eslint.config.js) — declarative, single file, no plugin-magic resolution. Legacy .eslintrc.json + extends: is deprecated.
// eslint.config.js (flat — modern)
import js from "@eslint/js";
import tsEslint from "typescript-eslint";
import reactPlugin from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
export default [
js.configs.recommended,
...tsEslint.configs.recommended,
{
files: ["**/*.{ts,tsx}"],
plugins: {
react: reactPlugin,
"react-hooks": reactHooks,
},
rules: {
"react/react-in-jsx-scope": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
},
settings: { react: { version: "detect" } },
},
{
files: ["**/*.test.ts", "**/*.test.tsx"],
rules: { "@typescript-eslint/no-explicit-any": "off" },
},
];
Flat config is just a JS array of config objects, each scoped by files:. Simpler to reason about, faster to load.
Q: Essential ESLint plugins.
A:
| Plugin | Why |
|---|---|
@eslint/js |
base JS rules |
typescript-eslint |
TS rules + parser |
eslint-plugin-react |
React rules |
eslint-plugin-react-hooks |
hook rules + exhaustive-deps |
eslint-plugin-jsx-a11y |
accessibility static analysis |
eslint-plugin-import |
import sorting, no-duplicates, no-circular |
eslint-plugin-unicorn (opinionated) |
modern JS best practices |
eslint-plugin-boundaries |
architecture boundaries (see 05) |
eslint-plugin-tailwindcss |
Tailwind class sorting + validation |
eslint-plugin-vue (Vue) |
Vue rules |
@typescript-eslint/recommended-type-checked enables rules that need type info (slower but catches more). Worth it on most TS projects.
Q: Prettier — what’s the principle?
A: Formatting is solved, don’t argue about it. Prettier picks one valid style; commit to it.
// .prettierrc.json
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "always"
}
Run via:
- Editor — format on save (VS Code’s Prettier extension).
- Pre-commit hook — auto-format staged files (lint-staged).
- CI —
prettier --check .fails build if anyone bypassed.
Q: ESLint + Prettier — do they conflict?
A: Historically yes — ESLint had formatting rules (indent, quotes, semi) that conflicted with Prettier. Modern setup: disable formatting rules in ESLint, let Prettier own formatting.
import prettierConfig from "eslint-config-prettier";
export default [
// ... your other configs
prettierConfig, // must be LAST — disables formatting rules from earlier configs
];
Some teams ship eslint-plugin-prettier to run Prettier as an ESLint rule. Don’t — it makes lint noisy and slow. Run Prettier separately.
Q: husky + lint-staged — what they do.
A: husky installs Git hooks (.husky/pre-commit, etc.). lint-staged runs commands only on staged files (fast feedback).
pnpm add -D husky lint-staged
pnpm exec husky init
// package.json
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md,yaml}": "prettier --write"
}
}
# .husky/pre-commit
pnpm exec lint-staged
Effect: on git commit, only the staged files get linted + formatted. Fast (a few seconds), so devs don’t bypass with --no-verify.
For long-running checks (type-check on whole repo, tests), use pre-push not pre-commit — pre-commit should be < 10 seconds.
Q: pre-commit vs pre-push vs CI — what runs where?
A: Tiered for speed:
| Hook | What runs | Time budget |
|---|---|---|
pre-commit |
format staged files, lint staged files | < 10s |
pre-push |
type-check, fast tests | < 60s |
| CI | full lint, type-check, all tests, build, deploy | minutes |
Each tier catches what slipped through the prior. CI is the final gate — never trust hooks alone (devs disable, forget to install).
Q: Biome — what is it, when use?
A: Biome is a Rust-native single-binary that does linting + formatting + import sorting in one tool. ~50× faster than ESLint+Prettier combined.
pnpm add -D --save-exact @biomejs/biome
biome init
// biome.json
{
"formatter": { "indentStyle": "space", "lineWidth": 100 },
"linter": { "rules": { "recommended": true } },
"javascript": { "formatter": { "quoteStyle": "double" } }
}
Single command: biome check --write . formats + lints + sorts imports. CI: biome ci ..
Pros: speed, single config, single binary. Cons: smaller plugin ecosystem than ESLint, fewer specialized rules (boundaries, Tailwind validation, etc.).
For new projects with simple rules: Biome. For large existing projects with rich ESLint configs: stay on ESLint + Prettier, the ecosystem matters more than the speed.
Q: TypeScript checking in lint vs tsc?
A:
tsc --noEmit— full type checking. Slow but authoritative.typescript-eslintrules — some type-aware rules (no-misused-promises,await-thenable), but not a full type check.
Don’t rely on ESLint for type errors. Run tsc --noEmit in CI (and ideally pre-push). VS Code’s tsserver covers it during editing.
"scripts": {
"type-check": "tsc --noEmit",
"lint": "eslint .",
"format": "prettier --write .",
"ci": "pnpm type-check && pnpm lint && pnpm test && pnpm build"
}
Q: What about EditorConfig?
A: .editorconfig sets basic file conventions (indent, line endings, trailing newline) that work across editors without Prettier. Useful for non-Prettier-handled files (.md, .yaml, .toml) where Prettier might disagree.
# .editorconfig
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
Most editors auto-apply. Doesn’t replace Prettier; complements it.
Q: A senior’s full config layout.
A:
my-app/
├── eslint.config.js # flat config
├── .prettierrc.json
├── .prettierignore
├── .editorconfig
├── biome.json (alternative)
├── tsconfig.json
├── .husky/
│ ├── pre-commit
│ └── pre-push
└── package.json # lint-staged config
CI runs pnpm ci (or similar) doing format check + lint + type-check + tests + build.
Q: What rules do you push back on?
A:
- Overly opinionated
unicornrules (unicorn/no-array-reduce) — case-by-case. no-default-export— Next.js + Vite expect default exports for routes/pages; can’t blanket disable.- Strict null checks via TS, not lint — handle in TS, not duplicated in lint.
- Style rules duplicating Prettier — disable via
eslint-config-prettier.
The principle: each rule earns its place by preventing a real bug class, not by matching a preference.
Gotchas / edge cases
- ESLint flat config + plugins that don’t yet support flat — wrap in
FlatCompat(legacy → flat). - Type-aware rules slow — slow CI lint runs. Configure to enable only on changed files in CI.
- Prettier line-width disputes —
100is common;80is purist;120is “I have a wide monitor.” Pick once. - Tailwind class order via
eslint-plugin-tailwindcss— sorts classes consistently (important for PR diffs). Prettier has a Tailwind plugin too; pick one. - husky in CI — Git hooks don’t run in CI; don’t rely on them as a final gate.
--no-verifylets devs bypass hooks. Accept it for emergencies; don’t make hooks slow enough to invite use.- Files outside
src/(config, scripts) — explicitly include or your lint misses them.
What a senior is expected to say
- “ESLint flat config (modern) + Prettier (formatting) + husky + lint-staged (fast commit gate) + CI (final gate). Or Biome as a single-tool replacement for new projects with simple needs.”
- “Type checking with
tsc --noEmitis separate from lint — typescript-eslint covers a few type-aware rules; full check needstsc.” - “lint-staged on
pre-commitkeeps the hook < 10s. Type-check + tests onpre-push. Full suite in CI.” - “Disable formatting rules in ESLint (
eslint-config-prettier) and let Prettier own formatting. Don’t run Prettier through ESLint — slow and noisy.” - “Every rule should justify its existence with a bug class it prevents. Avoid adding rules just because a preset includes them.”
Cross-references
- Module boundaries (lint enforcement of architecture): 05_module_boundaries.md
- TypeScript strictness: ../04_typescript/09_strictness_flags.md
- CI strategy (full gates): ../../backend/27_cicd/
Further reading
- ESLint flat config: https://eslint.org/docs/latest/use/configure/configuration-files
- Prettier docs: https://prettier.io/docs/en/
- Biome: https://biomejs.dev/
- husky: https://typicode.github.io/husky/
- lint-staged: https://github.com/lint-staged/lint-staged