---
title: Migrate from ESLint + Prettier to Biome (2026 Guide)
description: Replace ESLint and Prettier with Biome 2.5 in one tool. Migrate your configs with biome migrate, auto-fix lint and format errors, and gate CI. Runnable.
url: https://nerdleveltech.com/migrate-eslint-prettier-to-biome
language: en
date: '2026-06-19'
category: tooling
tags:
  - Biome
  - Biome migrate
  - ESLint
  - Prettier
  - JavaScript tooling
  - linter formatter
  - biome.json
  - CI
markdown_url: https://nerdleveltech.com/migrate-eslint-prettier-to-biome.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/migrate-eslint-prettier-to-biome'
terms: https://nerdleveltech.com/llms.txt
---

# Migrate from ESLint + Prettier to Biome (2026 Guide)

Biome is a single Rust-based tool that replaces both ESLint and Prettier. Install it, run `biome migrate eslint` and `biome migrate prettier` to convert your existing configs into one `biome.json`, then run `biome check --write` to lint *and* format your project in a single pass.[^1]

## TL;DR

You will take a small JavaScript project that uses ESLint and Prettier and move it to **Biome 2.5.0** in about ten minutes. We install Biome, generate a config with `biome init`, migrate the existing ESLint and Prettier settings automatically, run the combined lint-and-format checker, wire up npm scripts, and add a CI gate. Every command and output below was run on Node.js 22.22.3. No screenshots — this is pure terminal work.

## What you'll learn

- Why teams replace ESLint + Prettier with Biome, and what the trade-offs are
- How to install and pin **Biome 2.5.0** and scaffold a `biome.json`
- How to migrate an existing ESLint config with `biome migrate eslint`
- How to migrate a Prettier config and `.prettierignore` with `biome migrate prettier`
- How to lint and format in one pass with `biome check`, including safe vs. unsafe fixes
- How to add npm scripts and gate CI with `biome ci`
- How to make Biome respect your `.gitignore`
- How to troubleshoot the five things that actually trip people up

## Prerequisites

- **Node.js 14.21.3 or newer** (the published `engines` requirement for `@biomejs/biome`; tested on 22.22.3). Biome ships as a prebuilt native binary through npm, so there is nothing to compile.[^1]
- An existing project that already uses **ESLint** (flat or legacy `.eslintrc*`) and **Prettier**. If you don't have one, the next step creates a minimal one you can copy-paste.
- A terminal. That's it — Biome has no peer dependencies and does not need the `typescript` package to lint TypeScript.[^2]

## Why Biome instead of ESLint + Prettier

ESLint and Prettier are two tools, two config files, two dependency trees, and two passes over your code. Biome collapses them into one binary with one config. Biome's own published figures put it at **97% compatibility with Prettier's formatting** and roughly **35x faster than Prettier** on its benchmark of 171,127 lines across 2,104 files.[^3] Because Biome 2.0 added a lightweight type-inference engine, it can run type-aware lint rules **without** invoking the TypeScript compiler or installing the `typescript` package, which is what keeps it fast.[^2]

The honest trade-off: that remaining 3% of Prettier differences can matter for some teams, and Biome's rule set — while past **500 lint rules** as of v2.5 — does not cover every ESLint plugin you may depend on.[^4] The migration commands below tell you exactly what percentage of your rules Biome can reproduce, so you can make that call with data instead of guesswork.

## Step 1: Start from a real ESLint + Prettier project

Create a tiny project so every output in this guide is reproducible. The source file has deliberate lint problems (loose equality, an unused variable, a stray `console.log`, a `let` that should be `const`) and bad formatting.

```bash
mkdir biome-demo && cd biome-demo
npm init -y
```

Add a legacy ESLint config (`.eslintrc.json`):

```json
{
  "root": true,
  "rules": {
    "no-unused-vars": "error",
    "no-console": "warn",
    "eqeqeq": "error",
    "prefer-const": "error"
  }
}
```

Add a Prettier config (`.prettierrc`):

```json
{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100
}
```

Add a `.prettierignore`:

```
dist
coverage
```

And a messy source file at `src/total.js`:

```js
let   label = "cart"
const taxRate = 0.2
const unusedTotal = 0

function total(items, applyTax){
    let sum = 0
    for (const i of items){
        sum = sum + i.price
    }
    if (applyTax == true){
        console.log("applying tax")
        sum = sum * (1 + taxRate)
    }
    return sum
}

export {total, label}
```

## Step 2: Install and pin Biome

Install Biome as a dev dependency and pin the exact version so CI and every teammate run the identical binary. Pinning matters: Biome ships new and promoted rules in minor releases, so an unpinned install can change which diagnostics fire.[^4]

```bash
npm install -D @biomejs/biome@2.5.0
npx biome --version
```

```
Version: 2.5.0
```

Now scaffold a config:

```bash
npx biome init
```

This writes a starter `biome.json`. The defaults enable the formatter (tab indent), the linter with the `recommended` preset, and import sorting — and include a `vcs` block (off by default) that we turn on in Step 8:

```json
{
  "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json",
  "vcs": {
    "enabled": false,
    "clientKind": "git",
    "useIgnoreFile": false
  },
  "files": {
    "ignoreUnknown": false
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "tab"
  },
  "linter": {
    "enabled": true,
    "rules": {
      "preset": "recommended"
    }
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "double"
    }
  },
  "assist": {
    "enabled": true,
    "actions": {
      "source": {
        "organizeImports": "on"
      }
    }
  }
}
```

Note `"preset": "recommended"`. As of v2.5, the older `"recommended": true` key is **deprecated** in favor of `linter.rules.preset`; both still work, but `biome init` and `biome migrate` now emit `preset`, and running `biome migrate --write` rewrites the old key for you.[^4] Many older tutorials still show `recommended: true` — that is the deprecated form.

## Step 3: Migrate your ESLint config

This is the headline feature. Biome reads your existing ESLint config and rewrites `biome.json` with the equivalent Biome rules — and tells you the coverage:

```bash
npx biome migrate eslint --write
```

```
i 4 ESLint rules found
  - 4 have been migrated to Biome's rules
  - 100% (4) of your ESLint rules are fully covered by Biome
    - 100% (4) via direct migration to Biome rules
- biome.json: configuration successfully migrated.
```

Open `biome.json` and you'll see the linter block now mirrors your four ESLint rules exactly:

```json
"linter": {
  "enabled": true,
  "rules": {
    "preset": "none",
    "correctness": { "noUnusedVariables": "error" },
    "style": { "useConst": "error" },
    "suspicious": { "noConsole": "warn", "noDoubleEquals": "error" }
  }
}
```

Two things to understand here. First, the mapping: `no-unused-vars` became `noUnusedVariables`, `prefer-const` became `useConst`, `eqeqeq` became `noDoubleEquals`, and `no-console` kept its `warn` severity. Second — and this surprises people — the migration set `"preset": "none"`. It deliberately reproduces *only* the rules you had in ESLint, rather than layering Biome's recommended set on top, so your linting behavior doesn't silently change. If you *want* Biome's recommended rules as well, change `none` back to `recommended` after migrating.

## Step 4: Migrate your Prettier config

Same idea for formatting. Biome maps `.prettierrc` and `.prettierignore` into the same `biome.json`:

```bash
npx biome migrate prettier --write
```

```
.prettierignore has been successfully migrated.
.prettierrc has been successfully migrated.
- biome.json: configuration successfully migrated.
```

Your Prettier settings land in the `formatter` and `javascript.formatter` blocks, and `.prettierignore` becomes negated globs in `formatter.includes`. Here are the Prettier-derived fields (Biome also writes its own formatter defaults, such as `bracketSpacing` and `attributePosition`, which are omitted here for brevity):

```json
"formatter": {
  "enabled": true,
  "indentStyle": "space",
  "indentWidth": 2,
  "lineWidth": 100,
  "lineEnding": "lf",
  "includes": ["**", "!**/dist", "!**/coverage"]
},
"javascript": {
  "formatter": {
    "quoteStyle": "single",
    "semicolons": "asNeeded",
    "trailingCommas": "all"
  }
}
```

That maps cleanly: `singleQuote: true` → `quoteStyle: "single"`, `semi: false` → `semicolons: "asNeeded"`, `trailingComma: "all"` → `trailingCommas: "all"`, `tabWidth: 2` → `indentWidth: 2`, `printWidth: 100` → `lineWidth: 100`. Your one config file now carries everything ESLint and Prettier used to.

## Step 5: Run your first check, then auto-fix

`biome check` runs the formatter, the linter, and import sorting together. Its default output prints a full code frame and suggested fix for each problem; add `--reporter=concise` for a one-line-per-issue summary. Run it against the messy file:

```bash
npx biome check --reporter=concise ./src
```

```
! src/total.js:11:9: lint/suspicious/noConsole: Don't use console.
× src/total.js:10:18: lint/suspicious/noDoubleEquals: Using == may be unsafe if you are relying on type coercion.
× src/total.js:17:1: assist/source/organizeImports: Sort the exported names.
× src/total.js:1:1: lint/style/useConst: This let declares a variable that is only assigned once.
× src/total.js:3:7: lint/correctness/noUnusedVariables: This variable unusedTotal is unused.
× src/total.js: format: Formatter would have printed the following content:
Checked 1 file in 2ms. No fixes applied.
Found 5 errors.
Found 1 warning.
```

The command exits non-zero (`1`) because problems were found. Every line marked `×` is an error — that's five, counting the formatting difference — while `noConsole`, marked `!`, is a warning. Now apply the **safe** fixes — the ones Biome can make without any risk of changing behavior:

```bash
npx biome check --write ./src
```

After this, `src/total.js` is reformatted, `let label` became `const label` (it's only assigned once), and the export names are sorted. Note that `let sum` stayed a `let` because it is reassigned in the loop — Biome's `useConst` is precise, not blanket:

```js
const label = 'cart'
const taxRate = 0.2
const unusedTotal = 0

function total(items, applyTax) {
  let sum = 0
  for (const i of items) {
    sum = sum + i.price
  }
  if (applyTax == true) {
    console.log('applying tax')
    sum = sum * (1 + taxRate)
  }
  return sum
}

export { label, total }
```

Two problems remain on purpose: the `==` and the unused `unusedTotal`. Biome classifies their fixes as **unsafe**, because removing a `console.log`, swapping `==` for `===`, or renaming an unused variable can change runtime behavior. Apply them explicitly with `--unsafe`:

```bash
npx biome check --write --unsafe ./src
```

```
Checked 1 file in 1.9ms. Fixed 1 file.
```

Now `applyTax == true` becomes `applyTax === true`, the `console.log` line is removed, and the unused `unusedTotal` is renamed to `_unusedTotal` (Biome's unsafe fix prefixes rather than deletes, so you don't lose the declaration silently). The command exits `0`. The split between safe and unsafe fixes is the single most useful habit to learn: run `--write` in pre-commit hooks, and reserve `--write --unsafe` for deliberate, reviewed cleanups.

## Step 6: Add npm scripts and your editor

Wire Biome into `package.json` so the whole team runs the same commands:

```json
"scripts": {
  "lint": "biome lint ./src",
  "format": "biome format --write ./src",
  "check": "biome check --write ./src",
  "ci": "biome ci ./src"
}
```

`biome lint` runs only the linter, `biome format --write` runs only the formatter, and `biome check` does both plus import sorting. For live feedback while you type, install the official Biome editor extension (VS Code, Zed, or JetBrains) and set Biome as your default formatter; the extension uses the same `biome.json`, so editor and CLI never disagree.[^1]

## Step 7: Gate CI with `biome ci`

`biome ci` is a dedicated, never-writes command for pipelines: it checks formatting, lint, and import order, and exits non-zero if anything is off. On a clean tree it passes:

```bash
npx biome ci ./src
```

```
Checked 1 file in 1.2ms. No fixes applied.
```

Exit code `0`. Introduce one badly formatted file and it fails the build:

```
Checked 2 files in 1.2ms. No fixes applied.
Found 2 errors.
```

Exit code `1`. Drop that into a GitHub Actions step and your pipeline blocks unformatted or lint-failing code:

```yaml
- name: Biome
  run: npx biome ci ./src
```

If you already run keyless cloud deploys from Actions, this slots in next to them — see our [GitHub Actions OIDC keyless deploys tutorial](https://nerdleveltech.com/github-actions-oidc-aws-terraform-keyless-deploys-tutorial) for the surrounding workflow. For large monorepos, the new `--reporter=concise` output keeps logs short.[^4]

## Step 8: Make Biome respect your `.gitignore`

By default Biome checks every matching file, including generated ones. Turn on the VCS integration so it skips anything in your ignore files:

```json
"vcs": {
  "enabled": true,
  "clientKind": "git",
  "useIgnoreFile": true
}
```

With that set and a `.gitignore` that lists `src/generated.js`, a run only inspects your real source:

```bash
npx biome check ./src
```

```
Checked 1 file in 1.1ms. No fixes applied.
```

Only one file was checked — the generated file was skipped. As of v2.5, Biome also honors `.git/info/exclude` the same way it honors `.gitignore`.[^4]

## Verification

You're done when a clean checkout passes both commands with exit code `0`:

```bash
npx biome check ./src   # exit 0, "No fixes applied."
npx biome ci ./src      # exit 0
echo $?                 # prints 0
```

At that point you can delete `.eslintrc.json`, `.prettierrc`, `.prettierignore`, and uninstall `eslint`, `prettier`, and their plugins. One binary, one config, one pass.

## Troubleshooting

**`migrate eslint` errors with `Cannot find package '@eslint/js'`.** If your ESLint config uses `extends: ["eslint:recommended"]` (or any shared/plugin config), Biome loads it through Node to resolve those references, which fails if you've already removed ESLint. The fix is ordering: **migrate before you uninstall ESLint**. With ESLint still installed so it can expand `eslint:recommended`, the same command will find and attempt to migrate every rule that config enables — the exact rule count and coverage percentage depend on your ESLint version, since `eslint:recommended` changes between releases. Expect coverage below 100%, since not every ESLint rule has a direct Biome equivalent. Migrate first, verify the printed coverage summary, then remove ESLint.

**An unused `import` isn't flagged after migration.** ESLint's `no-unused-vars` maps to Biome's `noUnusedVariables`, which covers variables — not imports. Biome splits unused imports into a separate rule, `noUnusedImports`. If you relied on `no-unused-vars` to catch dead imports, add `"correctness": { "noUnusedImports": "error" }` to your linter rules after migrating.

**You lost Biome's recommended rules.** Expected: `migrate eslint` sets `"preset": "none"` so it reproduces only your ESLint rules. To get Biome's curated recommended set as well, change it to `"preset": "recommended"` and re-run `biome check` to see the new diagnostics.

**A teammate's config uses `"recommended": true`.** That key is deprecated as of v2.5 in favor of `preset`. It still works today, but run `biome migrate --write` to update it before it's removed in a future major.[^4]

**`--write` didn't fix everything.** That's by design. Biome only auto-applies safe fixes with `--write`; behavior-changing fixes (loose equality, dead `console` calls, unused bindings) require `--write --unsafe`. Review those diffs rather than running `--unsafe` blindly in a hook.

## Next steps and further reading

Biome pairs naturally with the rest of a modern toolchain: if you're already chasing build speed, the [native TypeScript compiler (tsgo)](https://nerdleveltech.com/typescript-7-native-compiler-tsgo) is the companion move for type-checking, and a refresher on [modern JavaScript patterns](https://nerdleveltech.com/mastering-advanced-javascript-deep-concepts-real-patterns-performance) makes Biome's rule output easier to read. From here, explore Biome's GritQL plugin system to write project-specific rules, enable type-aware lint rules through domains, and turn on `--watch` for live diagnostics while you code.[^4]

[^1]: Biome — Getting Started and homepage. https://biomejs.dev/guides/getting-started/
[^2]: Biome — "Roadmap 2025 and Biome 2.0" (type-aware linting without the TypeScript compiler; plugins; domains; multi-file analysis). https://biomejs.dev/blog/roadmap-2025/
[^3]: Biome — homepage formatter claims: 97% Prettier compatibility and ~35x faster than Prettier (benchmark: 171,127 lines across 2,104 files, Intel Core i7 1270P). https://biomejs.dev/
[^4]: Biome — "Biome v2.5—500 Lint Rules, Plugin Code Fix, and Cross-File Linting" (Jun 5, 2026): 500+ rules, `recommended` deprecated for `preset`, `--watch`, concise reporter, `.git/info/exclude` support, `.svg` support. https://biomejs.dev/blog/biome-v2-5/
