Linting & Formatting
Two tools sit at the heart of every healthy JavaScript codebase: a linter that finds suspicious code before it becomes a bug, and a formatter that makes every file look identical no matter who wrote it. ESLint and Prettier handle these jobs respectively, and they complement each other rather than overlap. Wiring them into your editor and into a pre-commit hook means style debates and whole classes of mistakes simply stop reaching code review.
Linting vs formatting
It is tempting to lump these together, but they answer different questions. A formatter only cares about how the code looks — indentation, quote style, line length — and it rewrites your file deterministically. A linter cares about what the code means — unused variables, unreachable branches, accidental reassignment, missing await. A linter can warn or error; a formatter just normalizes.
| Concern | ESLint (linter) | Prettier (formatter) |
|---|---|---|
| Finds bugs (unused vars, no-undef) | Yes | No |
| Enforces code style (spacing, quotes) | Possible, but defer to Prettier | Yes |
| Rewrites your file | With --fix (partial) | Always |
| Configurable rules | Hundreds | A handful |
| Understands semantics | Yes (AST + scope) | No (just reprints AST) |
The modern convention is to let Prettier own all purely stylistic decisions and let ESLint own correctness. This avoids the two tools fighting over the same lines.
Setting up ESLint
ESLint 9 uses a flat config file named eslint.config.js. Install it and generate a starter config:
npm install --save-dev eslint
npx eslint --init
A minimal flat config that lints modern browser and Node code looks like this:
// eslint.config.js
import js from "@eslint/js";
import globals from "globals";
export default [
js.configs.recommended,
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: 2024,
sourceType: "module",
globals: { ...globals.browser, ...globals.node },
},
rules: {
"no-unused-vars": "warn",
"no-undef": "error",
eqeqeq: ["error", "always"],
"prefer-const": "error",
},
},
];
Each rule takes a severity: "off", "warn", or "error". Run the linter from the command line, and add --fix to auto-correct the rules that support it:
npx eslint src/
npx eslint src/ --fix
Given a file with a loose equality check and an unused import, ESLint reports precisely where the problems are:
Output:
/app/src/cart.js
3:7 warning 'formatPrice' is defined but never used no-unused-vars
9:14 error Expected '===' and instead saw '==' eqeqeq
✖ 2 problems (1 error, 1 warning)
Setting up Prettier
Prettier needs almost no configuration — that is the point. Install it and add a tiny config to record the few choices it offers:
npm install --save-dev prettier
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 80
}
Save that as .prettierrc.json. Format the whole project, or just check for drift in CI:
npx prettier --write . # rewrite files in place
npx prettier --check . # exit non-zero if anything is unformatted
Add a .prettierignore file (same syntax as .gitignore) for build output and vendored code you do not want touched.
Making ESLint and Prettier coexist
If you enabled ESLint’s stylistic rules, they may conflict with Prettier. The fix is eslint-config-prettier, which simply turns off every ESLint rule that Prettier already handles:
npm install --save-dev eslint-config-prettier
// eslint.config.js (append to the array)
import prettier from "eslint-config-prettier";
export default [
// ...your other config blocks
prettier, // must come last so it can disable conflicting rules
];
Editor integration
The biggest productivity win comes from running both tools as you type. In VS Code, install the ESLint and Prettier extensions, then enable format-on-save and ESLint auto-fix in your workspace settings:
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
Now every save reformats with Prettier and applies ESLint’s safe auto-fixes. Most other editors (WebStorm, Neovim, Zed) have equivalent settings.
Pre-commit hooks
Editor integration is great, but it relies on every contributor configuring their machine. A pre-commit hook enforces the rules at the gate, where they cannot be skipped. Husky installs Git hooks and lint-staged runs your tools against only the files that are actually staged, keeping commits fast.
npm install --save-dev husky lint-staged
npx husky init
husky init creates .husky/pre-commit. Make it call lint-staged:
npx lint-staged
Then declare in package.json what should run per file type:
{
"lint-staged": {
"*.js": ["eslint --fix", "prettier --write"],
"*.{json,css,md}": ["prettier --write"]
}
}
On every commit, staged JavaScript is linted and formatted automatically; if ESLint reports an unfixable error, the commit is blocked.
Run
prettier --checkandeslint .in CI as well. Local hooks can be bypassed withgit commit --no-verify, so CI is your real safety net.
Best Practices
- Let Prettier own formatting and ESLint own correctness — never duplicate stylistic rules across both.
- Always add
eslint-config-prettierlast so the two tools never fight over the same lines. - Commit your
eslint.config.jsand.prettierrc.jsonso the whole team shares one source of truth. - Use lint-staged to lint only changed files; full-repo linting belongs in CI, not the commit hook.
- Treat lint warnings as a backlog, not noise — set
--max-warnings 0in CI once you are clean to prevent regressions. - Pin tool versions in
package.jsonso a Prettier upgrade does not silently reformat the entire codebase mid-sprint.