Replacing ESLint and Prettier with Biome: What You Gain, What You Lose, How to Migrate
Key takeaways
Biome replaces ESLint + Prettier with a single Rust-powered tool that is an order of magnitude faster. One config file, one command, no conflicts between formatter and linter. This guide covers setup, the trade-offs versus the ESLint plugin ecosystem, and a migration path that keeps your git history readable.
Why Biome?
The ESLint + Prettier + config files problem:
# Typical setup
.eslintrc.json ~ 50 lines
.eslintignore
.prettierrc.json ~ 10 lines
.prettierignore
+ 5-10 ESLint plugins/configs
+ compatibility issues between them
+ slow CI runs
Biome replaces it all:
biome.json ~ 20 lines
Much faster. One tool. No conflicts.
Speed is the headline, but in my experience it is not the main reason to switch. On a small project, ESLint + Prettier already finishes in a few seconds, and saving those seconds does not change anyone’s day. What does change things is the configuration surface. An ESLint setup gathers plugins over the years (eslint-config-prettier to turn off rules that conflict with Prettier, eslint-plugin-import, @typescript-eslint/parser pinned to a version that matches the TypeScript version…), and every major upgrade becomes a small project of its own. The ESLint 9 move to flat config was a good example: many teams spent more time on plugin compatibility than on the actual rule changes. Biome ships the parser, formatter, linter, and import sorter as one versioned binary, so there is only one thing to upgrade.
Speed starts to matter at scale: in a monorepo, a pre-commit hook, or editor-on-save feedback, where the difference between 200 ms and 5 seconds decides whether developers leave the hook enabled.
The trade-off is the ecosystem. ESLint’s value is its plugins: eslint-plugin-react-hooks, framework-specific rules, custom rules your team wrote, and above all type-aware rules from typescript-eslint such as no-floating-promises and no-misused-promises, which catch real bugs by using the TypeScript type checker. Biome has ported many popular rules, and 2.x added its own type inference for a few of them, but coverage is not the same. Before migrating, list the rules that have actually caught bugs in your codebase, and check whether Biome has each of them.
Installation
# npm
npm install --save-dev --save-exact @biomejs/biome
# Initialize config
npx biome init
# Check version
npx biome --version
This creates biome.json.
The --save-exact flag matters more for Biome than for most dev dependencies. Formatting output can change slightly between releases, and a caret range (^1.9.0) means two developers, or a developer and CI, can run different versions and keep rewriting each other’s files. Pin the version, and upgrade it in a dedicated commit that also contains the reformatting.
Configuration
// biome.json
{
"$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "error",
"noUnusedImports": "error"
},
"suspicious": {
"noExplicitAny": "warn",
"noConsoleLog": "warn"
},
"style": {
"useConst": "error",
"noVar": "error",
"useTemplate": "warn"
},
"performance": {
"noDelete": "warn"
},
"a11y": {
"recommended": true
}
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"lineEnding": "lf"
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "es5",
"semicolons": "asNeeded"
},
"linter": {
"enabled": true
}
},
"json": {
"formatter": {
"enabled": true,
"trailingCommas": "none"
}
},
"css": {
"formatter": {
"enabled": true
},
"linter": {
"enabled": true
}
},
"files": {
"ignore": [
"node_modules",
"dist",
".next",
".astro",
"coverage",
"*.min.js"
]
}
}
This configuration uses the Biome 1.x schema. Biome 2.0 (released in 2025) changed several keys, and a 1.x config does not work unchanged with 2.x. The main differences:
files.ignore/files.includewere replaced by a singlefiles.includeslist that supports negated patterns ("!dist"), andoverrides[].includebecameincludes.- Import sorting moved from the top-level
organizeImportsblock intoassist.actions.source.organizeImports. - Some rules were renamed or moved between groups.
noConsoleLog, for example, is replaced by the more generalnoConsole.
Running npx @biomejs/biome migrate --write after upgrading rewrites the config for you. Check which version you actually have before copying configuration from any article, this one included. The $schema URL tells you which version a snippet was written for, and the editor extension uses it to flag unknown keys.
The recommended: true setting deserves attention too. Biome’s recommended set is fairly strict. Enabling it on an existing codebase usually produces hundreds of diagnostics, and the most common ones (noExplicitAny, noNonNullAssertion, useExhaustiveDependencies) are exactly the ones teams argue about. Decide on those explicitly, rather than letting the defaults decide for you, before you turn the check on in CI.
CLI Commands
# Lint files (check for problems)
npx biome lint .
npx biome lint src/
# Format files (check for formatting issues)
npx biome format .
# Apply format fixes
npx biome format --write .
# Lint + format + organize imports in one command
npx biome check .
# Apply all fixes
npx biome check --write .
# Also apply unsafe fixes (review the diff afterwards)
npx biome check --write --unsafe .
# CI mode (exit code 1 if issues found)
npx biome ci .
# Format a specific file
npx biome format --write src/index.ts
# Check what would change without writing: omit --write
npx biome format .
Biome splits auto-fixes into safe and unsafe. --write applies only safe fixes, which are guaranteed not to change runtime behavior (for example, removing an unnecessary else after a return). Unsafe fixes can change behavior. useConst is harmless, but a fix like replacing == with === can change the result when the operands have different types. Only use --unsafe on a clean working tree, so you can review the diff.
biome ci and biome check run the same checks. The difference is that ci never writes files, is designed for non-interactive environments, and can be combined with --changed to only check files that differ from your default branch.
Package.json Scripts
{
"scripts": {
"lint": "biome lint .",
"lint:fix": "biome lint --write .",
"format": "biome format --write .",
"check": "biome check .",
"check:fix": "biome check --write .",
"ci": "biome ci ."
}
}
Editor Integration
VS Code
# Install Biome VS Code extension
code --install-extension biomejs.biome
// .vscode/settings.json
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"quickfix.biome": "explicit"
},
"[javascript]": { "editor.defaultFormatter": "biomejs.biome" },
"[typescript]": { "editor.defaultFormatter": "biomejs.biome" },
"[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" },
"[json]": { "editor.defaultFormatter": "biomejs.biome" },
"[css]": { "editor.defaultFormatter": "biomejs.biome" }
}
JetBrains (WebStorm, IntelliJ)
Settings → Languages & Frameworks → JavaScript → Biome → Enable
Ignoring Rules
// Disable for next line
// biome-ignore lint/suspicious/noExplicitAny: external API type
const data: any = externalApi.getData()
// Also applies to the next statement only (there is no block form)
// biome-ignore lint/suspicious/noConsoleLog: debug only
console.log('debugging', value)
// Disable in a whole file (Biome 2.x, add at top)
// biome-ignore-all lint/suspicious/noExplicitAny: legacy file
Biome requires a reason after the colon, and it reports a suppression comment that no longer suppresses anything. Both are deliberate. ESLint codebases tend to accumulate // eslint-disable-next-line comments that nobody can explain, and that stay long after the code they were protecting has changed. With a required reason and unused-suppression warnings, each exception documents itself and gets cleaned up when it stops being needed. File-level suppressions are only available in 2.x; in 1.x, use an overrides entry for the file instead, as shown below.
// biome.json — ignore entire directories
{
"files": {
"ignore": ["generated/", "vendor/", "*.min.js"]
},
"overrides": [
{
"include": ["**/*.test.ts"],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
}
}
}
}
]
}
Migrating from ESLint + Prettier
# Automated migration (reads your existing config)
npx @biomejs/biome migrate eslint --write
npx @biomejs/biome migrate prettier --write
# Manual migration checklist:
# 1. Install Biome
# 2. Run: npx biome init
# 3. Copy settings from .eslintrc + .prettierrc to biome.json
# 4. Run: npx biome check --write .
# 5. Fix any remaining issues
# 6. Remove ESLint + Prettier configs and packages
# 7. Update scripts in package.json
# 8. Update CI pipeline
# Remove ESLint + Prettier
npm uninstall eslint eslint-config-airbnb prettier @typescript-eslint/eslint-plugin
# (and all other eslint-* packages)
# Remove config files
rm .eslintrc.json .eslintignore .prettierrc.json .prettierignore
migrate eslint translates the rules Biome knows and reports the rest. Read that report instead of skipping it: it is your list of lost coverage. migrate prettier copies options like quoteStyle and lineWidth, so the first biome format --write produces a much smaller diff. Biome’s formatter aims for Prettier compatibility (the project reports around 97% on Prettier’s own test suite), but the remaining differences in edge cases, such as long ternaries, comments inside JSX, or some decorators, still produce diffs across a large codebase.
The mistake I would avoid is running the formatter across the whole repository in the same commit as the config change and code fixes. That buries every real change under thousands of whitespace lines, and git blame then shows the migration commit for nearly every line in the project. Do the migration in three commits: config and tooling only; the pure reformat (biome format --write . and nothing else); then lint fixes. Add the reformat commit’s hash to a .git-blame-ignore-revs file, and GitHub and git blame --ignore-revs-file will skip it. It is also worth announcing the reformat commit ahead of time, because every open branch will conflict with it. Rebasing a branch right after running the formatter on it resolves most of those conflicts automatically.
Git Hooks Integration
# .husky/pre-commit
npx biome check --write --staged --no-errors-on-unmatched
git update-index --again
--staged checks only files in the git index, which keeps the hook fast. There is one catch with --write in a pre-commit hook: the fixes are written to the working tree, but the version already staged in the index is the one that gets committed. git update-index --again re-stages the files that were already staged, so the fixes end up in the commit. If a file was only partially staged (git add -p), re-staging it pulls in the unstaged hunks as well. lint-staged (below) handles that case by stashing unstaged changes first, which is why many teams keep it even after switching to Biome. --no-errors-on-unmatched stops the hook from failing when a commit contains no files Biome handles, such as a commit that only changes Markdown.
With lint-staged:
// package.json
{
"lint-staged": {
"*.{js,ts,jsx,tsx,json,css}": [
"biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
]
}
}
CI Integration
# .github/workflows/ci.yml
- name: Run Biome
run: npx biome ci .
# Or with explicit install
- name: Install dependencies
run: npm ci
- name: Lint and format check
run: npx biome ci --reporter=github .
# --reporter=github adds inline annotations to PR review
Biome vs ESLint vs Prettier
| Biome | ESLint + Prettier | |
|---|---|---|
| Speed | Much faster (Rust, multi-threaded; Biome’s own benchmarks cite roughly 15x for lint, 25-35x for format) | Baseline |
| Config | 1 file | 2-4 files |
| TypeScript | Built-in | Via plugin |
| Plugins | Growing | Huge ecosystem |
| Rules | 270+ | 1000+ (with plugins) |
| JSX | Yes | Via plugin |
| CSS | Yes | Via plugin |
| JSON | Yes | Via plugin |
| Formatting | Built-in | Prettier |
| Import sorting | Built-in | Via plugin |
Supported Languages
| Language | Lint | Format |
|---|---|---|
| JavaScript | ✅ | ✅ |
| TypeScript | ✅ | ✅ |
| JSX/TSX | ✅ | ✅ |
| JSON/JSONC | ✅ | ✅ |
| CSS | ✅ | ✅ |
| GraphQL | ✅ | ✅ |
| HTML | 🚧 (experimental in 2.x) | 🚧 |
| Markdown | ❌ | ❌ |
Support for Vue, Svelte, and Astro files is partial: Biome can check the script portion, but template-aware rules and formatting of the markup are limited. If most of your code lives in .vue or .svelte files, check the current status on the Biome site before migrating, since that is where the gap with ESLint’s framework plugins is largest.
Before switching a project to Biome
- One tool replaces ESLint + Prettier + import sorter — zero conflicts
- An order of magnitude faster: it matters most for pre-commit hooks, editor feedback, and monorepos
- Check plugin coverage first: type-aware and framework-specific ESLint rules are the main thing you may lose
- Pin the version and read the 1.x → 2.x config changes before copying snippets
biome check --write= lint + format + organize imports in one commandbiome cifor CI pipelines — non-zero exit code on any issue- Migration:
npx biome migrate eslintauto-converts your ESLint config; keep the reformat in its own commit and add it to.git-blame-ignore-revs - VS Code: install the Biome extension +
formatOnSavefor seamless DX