DEV Community

RAXXO Studios
RAXXO Studios

Posted on • Originally published at raxxo.shop

I Replaced ESLint + Prettier with Biome Across 16 Repos: Setup, Wins, and 2 Gotchas

  • Swapped ESLint plus Prettier for Biome across 16 repos, dropping from 5 dev dependencies to 1

  • One shared biome.json now governs every project, no more per-repo config drift

  • Lint plus format fell from roughly 9 seconds to under 1 second on the biggest repo

  • Two real gotchas: a thin plugin ecosystem and one import-sorting rule that needed a manual pass

For two years every RAXXO repo shipped with the same four-headed config monster: ESLint, Prettier, the plugins that glue them together, and the config files nobody wanted to touch. Last month I deleted all of it and moved 16 repos to Biome. The lint-plus-format step that used to take about 9 seconds now finishes in under a second, and the dev dependency list got a lot shorter.

Why I Walked Away From ESLint Plus Prettier

The setup worked. That was never the problem. The problem was that keeping it working cost time on every single repo. A typical project carried eslint, prettier, eslint-config-prettier, eslint-plugin-import, and whatever framework plugin the stack needed. Five packages, two config files (.eslintrc and .prettierrc), and a .eslintignore that always drifted out of sync with .prettierignore.

Multiply that by 16 repos and the math gets ugly fast. When a Prettier major version landed, I had to bump it everywhere, re-test formatting on every project, and chase the inevitable conflicts where ESLint wanted one thing and Prettier wanted another. The classic fight was quotes and trailing commas. You install eslint-config-prettier just to tell ESLint to stop arguing with the formatter. That package exists only because two tools are doing overlapping jobs.

Speed was the other wall. On my largest repo, a full eslint . pass sat around 7 seconds, and adding prettier --check . on top pushed the combined gate close to 9 seconds. That is slow enough that you stop running it locally and let CI catch things, which means slower feedback and noisier pull requests.

Biome collapses both jobs into one binary written in Rust. It is a formatter and a linter in the same tool, configured by a single biome.json. No plugin bridge, no "turn off the rules that conflict with the formatter" dance, because there is only one tool deciding. I had been watching it mature for a while, and once it covered the rules I actually relied on, the cost of staying on the old stack stopped making sense. For the broader pattern of trading a pile of plugins for one focused tool, I went through the same exercise with build tooling in the 6 Vite plugins that replaced my Webpack config.

Migrating 16 Repos Without Losing a Weekend

I did one repo by hand first to learn the shape of the work, then scripted the rest. Biome ships a migrate command that reads your existing config and translates it, which removed most of the guesswork.

The per-repo flow was four steps. Install Biome as the only lint and format dependency:


bun add -D --exact @biomejs/biome

Enter fullscreen mode Exit fullscreen mode

Generate a starting config and pull in the old settings:


bunx biome init
bunx biome migrate eslint --write
bunx biome migrate prettier --write

Enter fullscreen mode Exit fullscreen mode

Rip out the dead packages and their config files:


bun remove eslint prettier eslint-config-prettier eslint-plugin-import
rm -f .eslintrc .eslintrc.json .eslintignore .prettierrc .prettierignore

Enter fullscreen mode Exit fullscreen mode

Then run the one command that both formats and lints with fixes applied:


bunx biome check --write .

Enter fullscreen mode Exit fullscreen mode

That check command is the whole point. It formats, it lints, and with --write it applies every safe fix in a single pass. No more chaining two tools in your scripts.

To avoid copy-pasting config 16 times, I keep one shared biome.json in a tiny internal package and extend it per repo. A project config is now four lines:


{
  "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
  "extends": ["@raxxo/biome-config"],
  "files": { "includes": ["src/**", "scripts/**"] }
}

Enter fullscreen mode Exit fullscreen mode

The shared base holds the real rules:


{
  "formatter": { "enabled": true, "indentStyle": "space", "lineWidth": 100 },
  "linter": {
    "enabled": true,
    "rules": { "recommended": true }
  },
  "assist": { "actions": { "source": { "organizeImports": "on" } } }
}

Enter fullscreen mode Exit fullscreen mode

CI got simpler too. The old job ran two steps; now it runs one:


- run: bunx biome ci .

Enter fullscreen mode Exit fullscreen mode

biome ci is the read-only mode for pipelines. It checks formatting and lint rules and fails on any diff, without writing files. Across all 16 repos the migration took an afternoon, most of which was reviewing diffs rather than fixing breakage. If you want the runtime context behind these commands, I switched everything to Bun across every new RAXXO project first, which is why bunx shows up everywhere here.

The Wins Were Bigger Than I Expected

The headline is speed. On my largest repo the combined lint-and-format gate dropped from roughly 9 seconds to under 1 second. That is not a typo and it is not cherry-picked: Biome runs its checks in parallel across cores, and a single Rust binary has none of the Node startup and plugin-resolution overhead that the old chain paid on every invocation. Smaller repos that used to take 2 to 3 seconds now feel instant, low enough that the pre-commit hook stopped being something I wanted to skip.

Fast feedback changes behavior. When the check is under a second, I run it constantly while writing code instead of waiting for CI to flag a stray semicolon. Pull requests got quieter because formatting and obvious lint issues are fixed before the commit ever lands.

The dependency cut was the quiet win. Each repo went from five lint-and-format packages to one. Across 16 repos that is a meaningful drop in install time, lockfile churn, and the surface area I have to keep patched when a security advisory lands. Fewer transitive dependencies also means fewer of those 2am supply-chain headaches.

Then there is the mental overhead I got back. One config format. One CLI to remember. One thing to upgrade when a new version ships, instead of coordinating ESLint, Prettier, and three plugins that all version independently. New repos are faster to spin up because the lint-and-format story is extends one shared config and done. This is the same standardization payoff I wrote about after I unified icons in why I standardized on Phosphor icons across my repos, where collapsing many choices into one default removed a whole category of decisions.

I also stopped paying for the eslint-config-prettier tax entirely. When one tool owns both formatting and linting, there is nothing to reconcile, so a whole class of conflict bugs simply does not exist anymore.

Two Gotchas I Will Not Sugarcoat

This was not free of friction, and pretending otherwise helps nobody.

The first gotcha is the plugin ecosystem. ESLint has a decade of community rules, and Biome does not match that breadth yet. Most of what I relied on has a built-in Biome equivalent, but a few specialized ESLint plugins had no direct replacement. One repo leaned on a custom rule that enforced an internal import-boundary convention, and there was no Biome rule for it. I had two choices: drop the check or move it into a small standalone script. I moved it. If your team depends on a niche plugin or a hand-written ESLint rule, audit that before you commit to the switch, because you may end up rebuilding it.

The second gotcha was import sorting, and it bit me on the first repo. Biome organizes imports through its assist actions, not through a lint rule, and its sort order is not identical to what eslint-plugin-import produced. The first time I ran biome check --write it re-sorted imports across the whole codebase, which created a large and noisy diff. Worse, in two files the new grouping reordered a side-effect import (a CSS file that had to load before a component) and the ordering actually mattered at runtime. Nothing crashed loudly, but a style sheet loaded a beat late. The fix was to run the import organize step on its own first, review that diff in isolation, then commit it separately before touching any logic:


bunx biome check --write --formatter-enabled=false --linter-enabled=false .

Enter fullscreen mode Exit fullscreen mode

Doing the import pass as its own commit kept the real review readable and let me catch the side-effect ordering before it shipped. Once that one-time cleanup was in, every repo after it went smoothly. Lesson learned: treat the first import re-sort as a dedicated migration commit, not a side effect of your normal check.

Bottom Line

If you run more than a couple of JavaScript or TypeScript repos and you are tired of babysitting ESLint, Prettier, and the glue packages between them, Biome is worth a serious look. One binary, one biome.json, one biome check --write that formats and lints in a single pass. My combined gate went from about 9 seconds to under a second, and every repo dropped from five lint-and-format dependencies to one.

It is not a drop-in for everyone. If you depend on a deep bench of ESLint plugins or custom rules, check coverage first, and plan a dedicated commit for the initial import re-sort so a stray side-effect ordering does not slip through. For most solo builders and small teams, the speed and the simpler config are well worth that one afternoon of migration.

I document every one of these stack decisions as I make them, both the wins and the things that bit me. If you want to see how the rest of the toolchain fits together across the studio, the full picture lives at RAXXO Studios.

Top comments (0)