Two kinds of tool keep a codebase healthy. A linter reads your code without running it and reports likely bugs: an assignment where you meant a comparison, a variable you never use, a CSS property that does not exist. A formatter rewrites whitespace, quotes, semicolons and line breaks into one consistent style, so code reviews discuss logic instead of indentation. Git 1,932 hooks then run both on every commit, so problems never reach the shared repository.
| Tool | What it checks | Version (Sep 2026) |
|---|---|---|
| ESLint 39,810 | JavaScript and TypeScript bugs | 10.10 |
| Prettier 68,264 | Formats JS, TS, CSS, HTML, JSON, Markdown | 3.9 |
| Biome 305,835 | Linter and formatter in one Rust binary | 2.5 |
| Oxlint 616,256 | Rust linter compatible with ESLint rules | 1.83 |
| Stylelint 245,253 | CSS, SCSS and Less bugs | 17.15 |
| EditorConfig 174,422 | Indentation, line endings, charset in any editor | spec |
| Husky 35,333 + lint-staged 14,736 | Runs checks on staged files before each commit | 9.1 / 17.5 |
ESLint and flat config
ESLint parses each file into an abstract syntax tree (AST) and hands every node to the rules you enable; a rule is a small function that says "report when you see an assignment inside an if test". ESLint 9 (April 2024) made the flat config file eslint.config.js the default, and ESLint 10 (February 2026) removed the old .eslintrc.* and .eslintignore formats entirely, so ignore older tutorials that use them. Install with npm 2,036 install -D eslint @eslint/js globals, or run npm init @eslint/config@latest to generate the file.
import js from "@eslint/js";
import globals from "globals";
import { defineConfig } from "eslint/config";
export default defineConfig([
{ ignores: ["dist/"] },
{
files: ["**/*.js"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: { globals: globals.browser },
rules: {
"no-unused-vars": "warn",
eqeqeq: "error",
},
},
]);The config is an array of objects; later objects override earlier ones for the files they match. Run npx eslint . on a file whose loop contains if (item.qty = 0) continue and an unused const discount = 0.1:
src\cart.js
4:9 error Expected a conditional expression and instead saw an assignment no-cond-assign
4:9 error Unexpected constant condition
no-constant-condition
7:9 warning 'discount' is assigned a value but never used no-unused-vars
✖ 3 problems (2 errors, 1 warning)The command exits with code 1 when any error is reported, which is what makes it useful in hooks and CI. For TypeScript add typescript-eslint 16,401 (TypeScript); for React 7,897 , Vue 5,482 or accessibility add the matching plugin to the same array.
Prettier, and why the linter runs first
Prettier (prettier.io (https://prettier.io/ 68,264 )) ignores your existing layout, prints the AST back out at a maximum line width (80 by default) and offers very few options by design. npx prettier --write . formats the project, --check only reports. It also reads .editorconfig for indentation and line endings. ESLint's core formatting rules (semi, indent, quotes) have been deprecated since ESLint 8.53 and are left out of js/recommended, so the two tools no longer fight.
Biome and Oxlint: the Rust generation
Biome (biomejs.dev (https://biomejs.dev/ 305,835 )) replaces both ESLint and Prettier with one binary and one biome.json: npm i -D -E @biomejs/biome, npx @biomejs/biome init, then npx biome check --write formats, lints and sorts imports for JavaScript, TypeScript, JSX, CSS, JSON, GraphQL and HTML. Its formatter follows Prettier's few-options philosophy, with one surprising default: it indents with tabs.
{
"$schema": "https://biomejs.dev/schemas/2.5.13/schema.json",
"formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 },
"linter": { "enabled": true },
"javascript": { "formatter": { "quoteStyle": "double" } }
}Oxlint (oxc.rs (https://oxc.rs/ 616,256 )) keeps your formatter and implements more than 865 ESLint-compatible rules; its documentation measures it at 50 to 100 times faster than ESLint, so large repositories often run oxlint first and ESLint only for plugins Oxlint lacks. Both gain speed by parsing each file once in native code across all CPU cores, but they cannot load every ESLint plugin, so check your plugin list before switching.
Stylelint and EditorConfig
Stylelint applies the same idea to CSS. After npm install -D stylelint stylelint-config-standard, a stylelint.config.mjs containing export default { extends: ["stylelint-config-standard"] }; is enough:
> npx stylelint "src/**/*.css" src/styles.css 3:12 ✖ Disallowed unit length-zero-no-unit 4:3 ✖ Unknown property "colr" property-no-unknown ✖ 2 problems (2 errors, 0 warnings) 1 error potentially fixable with the "--fix" option.
EditorConfig (editorconfig.org (https://editorconfig.org/ 174,422 )) is not a tool but a file that editors read before you type a character. JetBrains IDEs and Visual Studio 6 support it natively; VS Code 550 needs the EditorConfig extension. Editors search upward from the open file and stop at root = true:
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = trueHusky and lint-staged: checks on every commit

Git runs any executable in its hooks folder at fixed moments. Husky makes those hooks part of the repository: npm install -D husky lint-staged and npx husky init create .husky/pre-commit and add "prepare": "husky" to package.json, so every teammate's npm install activates the hooks. Replace the hook's contents with npx lint-staged, and tell lint-staged which commands to run on which staged files:
"lint-staged": {
"*.js": ["eslint --fix", "prettier --write"],
"*.css": ["stylelint --fix", "prettier --write"]
}> git commit -m "Add cart total" ✖ eslint --fix ↓ prettier --write ✖ stylelint --fix ↓ prettier --write ✖ Failed to run tasks for staged files! ⋯ Reverting to original state because of errors… husky - pre-commit script failed (code 1)
The commit was refused and lint-staged restored the files exactly as they were. Because it checks only staged files, the hook stays fast even in large repositories; the full-project check belongs in CI (Version Control Essentials).