Skip to main content

Command Palette

Search for a command to run...

Replacing ESLint & Prettier with Biome

One tool, one config file, zero drama

Updated
9 min read
Replacing ESLint & Prettier with Biome

What is Biome

Biome is a single tool that does what ESLint and Prettier do together: lint your code for errors and bad patterns, and format it consistently. It's written in Rust, runs on all CPU cores, and ships as one binary with zero dependencies.

You install it:

npm install --save-dev --save-exact @biomejs/biome

You run it:

npx @biomejs/biome check .

That command lints and formats every file in your project. One command, one tool.

The problem with ESLint + Prettier

There's nothing wrong with ESLint or Prettier individually. The problem is running them together. Here's what a typical TypeScript project needs:

npm install --save-dev eslint
npm install --save-dev @typescript-eslint/parser
npm install --save-dev @typescript-eslint/eslint-plugin
npm install --save-dev prettier
npm install --save-dev eslint-config-prettier
npm install --save-dev eslint-plugin-prettier

Six packages. And that's before adding React, import sorting, or accessibility rules. Each one has its own configuration format, its own versioning, and its own opinions about how your code should look.

Then you need to coordinate them. ESLint has formatting rules. Prettier has formatting rules. They conflict. So you install eslint-config-prettier to disable ESLint's formatting rules and let Prettier handle them. But now you need to make sure they agree on things like trailing commas, semicolons, and quote styles — in two different config files.

The config file situation:

.eslintrc.json          # or .eslintrc.js, or eslint.config.mjs (flat config)
.prettierrc             # or .prettierrc.json, or prettier.config.js
.eslintignore
.prettierignore

Four files minimum. Each with its own syntax. Each maintained separately.

And the speed. ESLint is single-threaded and JavaScript-based. On a 10,000-file monorepo, linting takes 30-45 seconds. Prettier formatting takes another 10-15 seconds. That's a minute of CI time on every push, and noticeable lag in your editor on large files.

None of this is a dealbreaker. Teams have lived with it for a decade. But it's friction that compounds over time.

What Biome does differently

One config file

Everything lives in biome.json:

{
  "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 90
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "correctness": {
        "noUnusedVariables": "error",
        "noUnusedImports": "error"
      },
      "style": {
        "useConst": "error",
        "useTemplate": "error"
      }
    }
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "double",
      "semicolons": "always",
      "trailingCommas": "all"
    }
  },
  "assist": {
    "actions": {
      "source": {
        "organizeImports": "on"
      }
    }
  },
  "files": {
    "includes": [
      "**",
      "!**/node_modules",
      "!**/dist",
      "!**/reports",
      "!**/test-results"
    ]
  }
}

That's it. Formatting, linting, import organization, file exclusions — all in one place. The $schema gives you autocomplete in VS Code for every option. No guessing, no looking up documentation for which key goes in which file.

Notice "useIgnoreFile": true in the VCS section. That tells Biome to respect your .gitignore, so you don't need a separate .biomeignore file.

Speed you actually feel

Biome is written in Rust and uses every CPU core. The difference isn't subtle:

# ESLint + Prettier on a project with 200 files
npx eslint . && npx prettier --check .
# ~8 seconds

# Biome on the same project
npx @biomejs/biome check .
# ~200ms

That's not a synthetic benchmark. That's what I measured on the Playwright test automation framework I maintain. The difference matters in two places: CI pipelines (seconds instead of minutes) and your editor (format-on-save feels instant instead of sluggish).

One dependency

"devDependencies": {
  "@biomejs/biome": "^2.4.0"
}

Compare that to the ESLint + Prettier stack where you easily accumulate 6-10 dev dependencies just for linting and formatting. Fewer dependencies means fewer version conflicts, fewer security audit warnings, and a simpler package.json.

Setting it up from scratch

Install

npm install --save-dev --save-exact @biomejs/biome

The --save-exact pins the version. Biome recommends this because their config schema is versioned — a biome.json written for 2.4.0 might not parse correctly with 2.5.0 without a migration step.

Initialize

npx @biomejs/biome init

This creates a biome.json with sensible defaults. From there, you customize what you need. The defaults are already good — recommended rules are enabled, the formatter is on, import sorting is on.

Add scripts

In your package.json:

"scripts": {
  "lint": "npx @biomejs/biome check .",
  "lint:fix": "npx @biomejs/biome check --write .",
  "format": "npx @biomejs/biome format --write ."
}

check does everything: format + lint + import organization. --write applies the fixes. For CI, use check without --write — it exits with a non-zero code if anything is wrong.

VS Code integration

Install the Biome extension (search "Biome" in the marketplace). Then in .vscode/settings.json:

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

Save a file. It formats instantly, sorts imports, and shows lint errors inline. No separate Prettier extension, no ESLint extension, no coordination between them.

Migrating from ESLint + Prettier

Biome ships migration commands that read your existing config and translate it:

npx @biomejs/biome migrate eslint --write
npx @biomejs/biome migrate prettier --write

This handles about 70% of your rules automatically. The rest you review manually. Biome has its own naming conventions (camelCase instead of kebab-case), and some ESLint plugins don't have Biome equivalents yet.

After migration, do a full check:

npx @biomejs/biome check .

Fix the issues, commit, and remove your old config files:

rm .eslintrc.json .prettierrc .eslintignore .prettierignore
npm uninstall eslint prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-prettier eslint-plugin-prettier

That felt good.

What Biome handles well

TypeScript. No separate parser package. No @typescript-eslint anything. Biome parses TypeScript natively. Unused variables, unused imports, type-only imports — all handled.

Import organization. Sorts imports automatically on save: external packages first, then relative imports, separated by a blank line. No eslint-plugin-import or eslint-plugin-simple-import-sort needed.

JSON and CSS. Biome formats and lints JSON and CSS files too. With ESLint, you needed separate plugins for that. Biome does it out of the box.

Error messages. Biome's diagnostics are genuinely good. They show the exact location, the rule name, a description of the problem, and often a suggested fix — color-coded and formatted clearly. ESLint's output looks primitive by comparison.

What Biome doesn't handle (yet)

React hooks rules. eslint-plugin-react-hooks enforces the rules of hooks (no conditional hooks, dependencies array). Biome doesn't have a full equivalent yet. If you're testing React apps, this matters less in a test framework than in application code.

Type-aware linting. ESLint with @typescript-eslint can use the TypeScript compiler to check things like "this promise was not awaited" or "this type assertion is unnecessary." Biome's type-aware rules are on the roadmap but not fully there yet.

Custom plugins. ESLint has thousands of community plugins. Biome has none — the rules are all built-in. For most projects, the 450+ built-in rules are enough. For niche requirements (security audit rules, framework-specific patterns), you might still need ESLint alongside Biome.

YAML config. Biome only supports JSON config (biome.json or biome.jsonc). No JavaScript config files, no YAML. This is a deliberate choice — JSON is parseable without running code — but it means you can't have dynamic config based on environment variables.

When to use Biome vs ESLint

Use Biome when:

  • Starting a new project (no migration cost)
  • Speed matters (CI pipelines, large codebases, format-on-save)
  • You want minimal config (one file, one tool)
  • Your project is TypeScript, JavaScript, JSON, or CSS
  • You're tired of coordinating ESLint + Prettier

Keep ESLint when:

  • You depend on specific plugins that Biome doesn't cover
  • You need type-aware lint rules today
  • Your company mandates a specific ESLint config (Airbnb, Google, etc.)
  • You have custom ESLint rules written for your codebase

Hybrid approach:

  • Use Biome for formatting (replaces Prettier) and basic linting
  • Keep ESLint for the few plugin-specific rules you still need
  • This gives you Biome's speed for 90% of the work

The config I use in every project

This is the biome.json that ships with Histrion, my Playwright scaffolding CLI. It's what I've settled on after iterating across multiple test automation frameworks:

{
  "$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true
  },
  "assist": {
    "actions": {
      "source": {
        "organizeImports": "on"
      }
    }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 90
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "suspicious": {
        "noExplicitAny": "warn"
      },
      "style": {
        "useConst": "error",
        "useTemplate": "error"
      },
      "correctness": {
        "noUnusedVariables": "error",
        "noUnusedImports": "error"
      }
    }
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "double",
      "semicolons": "always",
      "trailingCommas": "all"
    }
  },
  "files": {
    "includes": [
      "**",
      "!**/node_modules",
      "!**/test-results",
      "!**/reports",
      "!**/auth",
      "!**/screenshots",
      "!**/dist"
    ]
  }
}

A few deliberate choices:

  • noExplicitAny: "warn" — not error. In test code, any is sometimes the pragmatic choice. A warning reminds you to fix it later without blocking your work.
  • noUnusedImports: "error" — dead imports are noise. Kill them automatically.
  • useConst: "error" — if you don't reassign it, use const. Consistency.
  • useTemplate: "error" — template literals over string concatenation. Readability.
  • lineWidth: 90 — narrower than Prettier's default 80 but wider than 120. Fits a code editor with a file tree open without horizontal scrolling.
  • semicolons: "always" and trailingCommas: "all" — opinionated, but consistent. Cleaner git diffs with trailing commas.

When you scaffold a project with npx histrion create, this config is included. The generated code already passes biome check — you never start with lint errors.

Closing thoughts

Biome isn't perfect. The missing type-aware rules and the limited plugin ecosystem are real gaps. But for the projects I work on — TypeScript Playwright frameworks, CLI tools, Node.js utilities — those gaps don't matter. What matters is that I have one config file instead of four, one dependency instead of ten, and instant feedback instead of waiting seconds.

If you're starting a new project, try it. Install Biome, run check, and see how it feels. You can always go back to ESLint + Prettier. But in my experience, nobody does.


This article is part of TestBot Chronicles, where I write about test automation, engineering practices, and the craft of quality.

2 views

More from this blog

TestBot Chronicles

10 posts

A blog about my personnal learning and discovery journey through AI, Automation, Coding and Tech enabling.