Code Quality Tools

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.

The code quality tools covered in this section
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.

eslint.config.js: the recommended rules plus two project choicesJavaScript
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:

Output of 51
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.

biome.json: two-space indentation and the recommended lint rulesJSON
{
  "$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:

Output of 52
> 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:

.editorconfig at the project root
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
Husky and lint-staged: checks on every commit
Three checkpoints: the editor on save, the pre-commit hook, and CI on every push
Three checkpoints: the editor on save, the pre-commit hook, and CI on every push

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:

package.json: lint first, then format, only the files being committedJSON
"lint-staged": {
  "*.js": ["eslint --fix", "prettier --write"],
  "*.css": ["stylelint --fix", "prettier --write"]
}
Output
> 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).