Linting and formatting are where many teams quietly burn time that could go to real product work. ESLint takes hours to configure. Prettier fights ESLint through eslint-config-prettier. Pre-commit hooks stretch to eight seconds. CI blocks PRs for minutes. Every new hire spends half a day learning why there are seven different config files.
Biome is an answer to that — one config file, one command, speed measured in milliseconds. This article shows how to migrate an existing TypeScript project, what to watch for, and what you can realistically gain.
Why ESLint + Prettier becomes a problem
Early on, ESLint and Prettier feel simple. Install a few packages, copy a config from the last project, done. Pain starts when the repo grows.
A typical package.json a year in: eslint, prettier, eslint-config-prettier, eslint-plugin-react, eslint-plugin-import, @typescript-eslint/eslint-plugin, @typescript-eslint/parser, eslint-plugin-jsx-a11y, eslint-plugin-unicorn — and that is still incomplete. Each package has its own dependency tree, release cadence, and config surface. When TypeScript or React ships a new major, you wait weeks for plugin compatibility.
Second issue: speed. ESLint is single-threaded and was not designed for huge monorepos. On a real production codebase, linting ~200k lines without autofixes took nearly seven minutes. Pre-commit hooks over five seconds are hooks developers disable — nobody waits eight seconds on every commit.
Third issue: configuration sprawl. .eslintrc.json, .eslintignore, .prettierrc, .prettierignore. In monorepos you multiply that by packages. Onboarding is not just npm install — it is a long explanation of why the setup looks the way it does.
Tooling stack comparison: ESLint+Prettier (many packages, many config files) vs Biome (one package, one biome.json)
What Biome is and what it offers
Biome is a Rust-based toolchain for analysis and formatting. It combines a formatter (Prettier-class) and a linter (ESLint-class) in one binary with a single biome.json.
The project started as a fork of Rome Tools. Since 2023 it has been independent. 1.0 shipped August 2023, 2.0 in 2025, and 2.4 in early 2026. On GitHub it has 24k+ stars and won a “Productivity Booster” award at OS Awards 2024.
What Biome does better:
Speed. Rust, multi-threaded, no loading hundreds of Node modules on every run. Official benchmark: ~171k lines across 2k+ files — Biome is ~35× faster than Prettier for formatting. On a real monorepo, lint-with-autofix dropped from seven minutes to ~1.6 seconds — roughly 250× faster.
One config file. Formatter, linter, import organisation — all in biome.json.
Prettier compatibility. Biome targets ~97% Prettier compatibility on real open-source code. Differences exist but are documented and usually minor.
Linter with 467 rules. Coverage from @typescript-eslint, eslint-plugin-react, eslint-plugin-jsx-a11y, eslint-plugin-unicorn. Not 100%, but enough for most TypeScript/JavaScript projects.
Since 2.0, Biome also ships a plugin system (GritQL) for custom lint rules — an answer to “we must stay on ESLint for custom rules.”
What Biome formats: JavaScript, TypeScript, JSX, TSX, JSON, JSONC, CSS, GraphQL, HTML (experimental).
What Biome still does not cover well: SCSS, Markdown, full Astro/Vue/Svelte embedded-language support.
How to migrate — step by step
Most migrations take one to a few hours. Concrete commands:
Step 1: Install
npm install --save-dev --save-exact @biomejs/biome
--save-exact pins Biome’s version so everyone gets the same formatter output.
Step 2: Initialise config
npx @biomejs/biome init
Creates a default biome.json. You can already run npx biome check . and see findings.
Step 3: Migrate ESLint
npx @biomejs/biome migrate eslint --write
Biome reads .eslintrc.js or eslint.config.js, handles extends, overrides, .eslintignore, and writes biome.json. Loading ESLint plugins still requires Node to be installed with project dependencies.
Step 4: Migrate Prettier
npx @biomejs/biome migrate prettier --write
Reads .prettierrc / prettier.config.js and maps indent style, quotes, trailing commas, line width into the formatter section.
Step 5: Update package.json scripts
{
"scripts": {
"lint": "biome lint .",
"format": "biome format --write .",
"check": "biome check --write .",
"ci": "biome ci ."
}
}
biome check combines format, lint, and import sorting — use --write locally. biome ci is for pipelines: no writes, exit codes only. In GitHub Actions it can annotate PRs line-by-line.
Step 6: Remove old tooling
npm uninstall eslint prettier eslint-config-prettier eslint-plugin-react \
eslint-plugin-import @typescript-eslint/eslint-plugin @typescript-eslint/parser \
eslint-plugin-jsx-a11y eslint-plugin-unicorn
Delete .eslintrc.js, .eslintignore, .prettierrc, .prettierignore so nothing silently conflicts.
Step 7: Verify
npx @biomejs/biome check .
Review findings; tune rules you want off.
Example biome.json for TypeScript
Minimal starting point:
{
"$schema": "https://biomejs.dev/schemas/2.4.0/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": false,
"ignore": ["dist/**", "node_modules/**", "coverage/**"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "es5",
"semicolons": "always"
}
}
}
vcs.useIgnoreFile: true makes Biome respect .gitignore — no duplicated ignore lists.
For NestJS or heavy dependency injection, disable useImportType:
{
"linter": {
"rules": {
"recommended": true,
"style": {
"useImportType": "off"
}
}
}
}
Biome otherwise rewrites value imports to import type, which breaks Nest DI when only types survive at runtime.
Known limitations and gotchas
First formatter run can touch thousands of lines. Even with ~97% Prettier parity, quote and comma rules can create a huge one-off diff. Treat it as a deliberate chore: apply Biome formatting commit so git blame stays usable.
No full type-aware ESLint parity yet. Rules like @typescript-eslint/no-floating-promises are still catching up. Run tsc --noEmit as a separate CI step if you rely on those checks.
No SCSS formatter. Keep Stylelint for SCSS; at least Biome stays out of your styles.
noForEach can annoy teams. Turn it off if you prefer .forEach:
"complexity": {
"noForEach": "off"
}
JetBrains plugin stability historically lagged VS Code (biomejs.biome).
Large monorepos: official guidance suggests enabling the formatter first, then lint with --diagnostic-level=error until noise is under control.
VS Code and CI/CD
VS Code
Install biomejs.biome, then workspace settings.json:
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"[javascript]": { "editor.defaultFormatter": "biomejs.biome" },
"[typescript]": { "editor.defaultFormatter": "biomejs.biome" },
"[json]": { "editor.defaultFormatter": "biomejs.biome" }
}
Disable ESLint/Prettier extensions for that workspace to avoid conflicting fixes.
GitHub Actions
name: Code quality
on:
push:
pull_request:
jobs:
quality:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- name: Setup Biome
uses: biomejs/setup-biome@v2
with:
version: latest
- name: Run Biome
run: biome ci .
If biome.json uses extends from an npm package, run npm ci before setup-biome.
GitHub Actions PR view with Biome annotations on specific diff lines
What you can implement today
Today (~15 minutes): install Biome in one package, run npx biome check ., compare findings with ESLint — no need to delete ESLint yet.
This week (1–2 hours): migrate a smaller repo with biome migrate eslint / migrate prettier, land formatting as its own commit.
This month: wire biome ci into CI, commit .vscode/settings.json, remove unused packages.
Monorepos: replace husky + lint-staged with biome check --staged ., or pair Biome with lefthook.
What you can gain
Numbers from production migrations, not slide decks:
- Lint time: ~200k LOC monorepo from 7 minutes → ~1.6s (~250×). Smaller Next.js repos often see 8–12× improvements.
- CI minutes: one team saved >1,000 CI minutes/month — fewer dollars on runners, faster PR feedback.
- Pre-commit: from ~8s to near-instant — hooks people actually keep enabled.
- Dependencies: typical uninstall removes 8–12 npm packages — smaller
node_modules, faster installs, fewer advisories to track. - Onboarding: one
biome.jsoninstead of four mystery files andeslint-config-prettierarchaeology.
Biome is not magic. If you depend on heavy type-aware ESLint rules, SCSS pipelines, or bespoke ESLint plugins without Biome equivalents, plan extra work. For standard TypeScript/JavaScript stacks, Biome is production-ready and pays back from day one.