Skip to main content

Command Palette

Search for a command to run...

Linting and Formatting: Biome, ESLint, Prettier, and Pre-Commit Hooks

Published
6 min read
D
Practical guides for developers: TypeScript, developer tools, CI/CD, and modern web development. We cover the tools that make devs more productive.

Linting and Formatting: Biome, ESLint, Prettier, and Pre-Commit Hooks

Code formatting debates waste time. Automate the formatting, enforce it in CI, and never argue about semicolons again. This guide covers the current state of JavaScript/TypeScript linting and formatting tooling, from choosing tools to enforcing them.

Biome toolchain logo

Biome vs ESLint + Prettier

The main choice in 2026 is between Biome (one tool for both linting and formatting) and ESLint + Prettier (two tools working together).

FeatureBiomeESLint + Prettier
SpeedVery fast (Rust)Slow (JavaScript)
Config files1 (biome.json)2-3 (eslint.config.js, .prettierrc, .prettierignore)
FormattingBuilt-inPrettier
Lint rules~300 rules1000+ with plugins
Plugin ecosystemLimitedMassive
TypeScriptNativeVia typescript-eslint
React/JSXYesVia eslint-plugin-react
CSS/JSON/etc.YesPrettier handles, ESLint via plugins

Choose Biome When:

  • You want one tool instead of two
  • Speed matters (large codebases, CI pipelines)
  • You don't need niche ESLint plugins
  • You're starting a new project

Choose ESLint + Prettier When:

  • You need specific ESLint plugins (accessibility, security, framework-specific rules)
  • Your project already uses them and migration isn't worth the effort
  • You need fine-grained rule customization

Biome Setup

bun add --dev @biomejs/biome
bunx biome init

This creates biome.json:

{
  "$schema": "https://biomejs.dev/schemas/2.0.x/schema.json",
  "organizeImports": {
    "enabled": true
  },
  "formatter": {
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true
    }
  }
}
# Format
bunx biome format --write .

# Lint
bunx biome lint .

# Both at once
bunx biome check --write .

Biome's check command runs formatting, linting, and import sorting in a single pass. It's faster than running ESLint and Prettier separately.

ESLint + Prettier Setup (Flat Config)

ESLint moved to "flat config" (eslint.config.js) in v9. If you're still using .eslintrc, migrate -- the old config format is deprecated.

bun add --dev eslint @eslint/js typescript-eslint prettier eslint-config-prettier
// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  prettierConfig, // Must be last to override conflicting rules
  {
    rules: {
      "@typescript-eslint/no-unused-vars": ["error", {
        argsIgnorePattern: "^_",
      }],
    },
  },
];
// .prettierrc
{
  "semi": true,
  "singleQuote": false,
  "trailingComma": "all",
  "printWidth": 100
}

eslint-config-prettier disables all ESLint rules that conflict with Prettier. Always include it last in your config array.

Pre-Commit Hooks

Pre-commit hooks run linting and formatting on staged files before each commit. This catches issues before they reach CI.

Husky + lint-staged

The most popular approach. Husky manages Git hooks, lint-staged runs commands only on staged files.

bun add --dev husky lint-staged
bunx husky init

This creates .husky/pre-commit. Edit it:

#!/bin/sh
bunx lint-staged

Configure lint-staged in package.json:

{
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": [
      "biome check --write --no-errors-on-unmatched"
    ],
    "*.{json,md,yaml}": [
      "biome format --write --no-errors-on-unmatched"
    ]
  }
}

Or with ESLint + Prettier:

{
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,css,yaml}": [
      "prettier --write"
    ]
  }
}

Lefthook

Lefthook is a faster, configuration-file-based alternative to Husky. It's written in Go and doesn't require npm post-install scripts.

# lefthook.yml
pre-commit:
  parallel: true
  commands:
    lint:
      glob: "*.{ts,tsx,js,jsx}"
      run: bunx biome check --write --no-errors-on-unmatched {staged_files}
      stage_fixed: true
    format:
      glob: "*.{json,md,yaml}"
      run: bunx biome format --write --no-errors-on-unmatched {staged_files}
      stage_fixed: true
bun add --dev lefthook
bunx lefthook install

Lefthook runs commands in parallel by default and has better performance than Husky + lint-staged for large teams. The stage_fixed: true option automatically re-stages files after auto-fixing.

Trade-offs of Pre-Commit Hooks

Pre-commit hooks have a downside: they add time to every commit. For large repos or slow linters, this friction adds up. Some teams skip pre-commit hooks entirely and rely on CI enforcement, using git commit --no-verify only in emergencies.

A good middle ground: run formatting in pre-commit hooks (fast, auto-fixable) and run linting in CI only (slower, may need manual fixes).

Editor Integration

VS Code

Biome has an official VS Code extension that provides format-on-save and inline diagnostics:

// .vscode/settings.json
{
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "quickfix.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  }
}

For ESLint + Prettier:

{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  }
}

Commit your .vscode/settings.json to the repository so all team members get the same editor behavior. Use .vscode/extensions.json to recommend the required extensions.

Other Editors

JetBrains IDEs (WebStorm, IntelliJ) have built-in ESLint and Prettier support. Biome support is available via plugin. Neovim users can configure Biome or ESLint through nvim-lspconfig or none-ls.

CI Enforcement

Pre-commit hooks are a convenience, not a guarantee. Developers can skip them with --no-verify. CI is where you actually enforce code quality.

# GitHub Actions
- name: Check formatting
  run: bunx biome format --check .

- name: Lint
  run: bunx biome lint .

Or with ESLint + Prettier:

- name: Lint
  run: bunx eslint .

- name: Check formatting
  run: bunx prettier --check .

Note the difference: in CI, use --check (report errors without fixing). In local development and pre-commit hooks, use --write (auto-fix). CI should never modify code -- it should only verify.

Run formatting and linting checks early in your CI pipeline, ideally in parallel with type checking and tests. They're fast enough that there's no reason to gate them behind slower steps.

Keeping Configs Minimal

The biggest mistake with linting configuration is adding too many rules. Every custom rule is a decision your team has to maintain and debate. Start with the recommended preset and add rules only when you encounter real problems.

// Good: minimal biome.json
{
  "linter": {
    "rules": {
      "recommended": true
    }
  }
}

// Bad: dozens of individually configured rules
// (every rule is a potential debate and maintenance burden)

The same applies to ESLint. Use recommended presets and override sparingly. If you find yourself disabling rules in dozens of files with inline comments, the rule probably doesn't fit your codebase.

Recommendations

  • New projects: Use Biome. One tool, one config file, fast execution.
  • Existing ESLint + Prettier projects: Keep them unless you're actively frustrated. Migrate to Biome when you'd do a major config overhaul anyway.
  • Pre-commit hooks: Use Lefthook or Husky + lint-staged. Keep hooks fast (formatting only, not full linting).
  • CI: Always enforce formatting and linting in CI. Never trust pre-commit hooks alone.
  • Editor settings: Commit .vscode/settings.json with format-on-save enabled. Reduce friction so developers don't have to think about formatting.
  • Config philosophy: Start minimal, add rules only when they prevent real bugs. Delete rules that generate more noise than signal.

Enjoyed this guide? Subscribe to DevTools Guide — a free weekly newsletter covering developer tools, workflows, and best practices.

More from this blog

DevTools Guide

183 posts