Biome: Replace ESLint and Prettier with One Ultra-Fast Tool

AI Bot
By AI Bot ·

Loading the Text to Speech Audio Player...

Your ESLint + Prettier pipeline takes 45 seconds? Biome does the same thing in under 2 seconds. Biome is an all-in-one formatter and linter written in Rust that replaces ESLint, Prettier, and their dozens of plugins. In this tutorial, you will migrate a complete Next.js project to Biome and discover why thousands of projects made the switch in 2026.

What You Will Learn

By the end of this tutorial, you will be able to:

  • Understand what Biome does and why it replaces ESLint + Prettier
  • Install and configure Biome in an existing JavaScript/TypeScript project
  • Automatically migrate your ESLint and Prettier rules to Biome
  • Set up custom linting rules per file and per directory
  • Integrate Biome into VS Code with automatic formatting
  • Add Biome to your CI/CD pipeline (GitHub Actions and GitLab CI)
  • Use organized imports and automatic sorting
  • Configure Biome for a monorepo with shared rules

Prerequisites

Before starting, make sure you have:

  • Node.js 18+ installed (node --version)
  • An existing JavaScript or TypeScript project (ideally with ESLint/Prettier)
  • Familiarity with the command line
  • VS Code or an editor with LSP support
  • Basic knowledge of configuration files (JSON)

Why Biome?

In 2026, the majority of JavaScript projects use ESLint for linting and Prettier for formatting. This combination works, but it has concrete problems:

  • Slow: ESLint parses each file in JavaScript, which is inherently slow on large projects
  • Complex configuration: between .eslintrc, .prettierrc, eslint-config-*, plugins, parsers, and ESLint/Prettier conflicts, configuration becomes a headache
  • Conflicts: ESLint and Prettier can contradict each other — you need eslint-config-prettier to disable conflicting rules
  • Maintenance: each tool has its own update cycle, breaking changes, and plugins to maintain

Biome solves all of this with a different approach:

AspectESLint + PrettierBiome
LanguageJavaScript (slow)Rust (fast)
Tools2 tools + plugins1 single tool
Config2-4 files1 file biome.json
Speed~45s on 1000 files~1.5s on 1000 files
ConflictsFrequentImpossible

Step 1: Install Biome

Let us create a new project for demonstration, or use your existing project:

# Create a test Next.js project
npx create-next-app@latest biome-demo --typescript --tailwind --app --src-dir
cd biome-demo

Install Biome as a dev dependency:

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

The exact version is recommended (--save-exact) because Biome follows semantic versioning and minor updates can add new rules that generate errors in your CI.

Initialize the configuration:

npx @biomejs/biome init

This creates a biome.json file at the project root:

{
  "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
  "vcs": {
    "enabled": false,
    "clientKind": "git",
    "useIgnoreFile": false
  },
  "files": {
    "ignoreUnknown": false,
    "ignore": []
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "tab"
  },
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true
    }
  }
}

Step 2: Configure the Formatter

Biome uses tabs by default, but most JavaScript projects use 2 spaces. Let us configure the formatter to match common conventions:

{
  "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100,
    "lineEnding": "lf"
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "double",
      "semicolons": "always",
      "trailingCommas": "all",
      "arrowParentheses": "always",
      "bracketSpacing": true,
      "jsxQuoteStyle": "double"
    }
  }
}

Comparison with Prettier

If you are coming from Prettier, here is the option mapping:

PrettierBiomeDefault
tabWidthindentWidth2
useTabsindentStyle: "tab""space"
printWidthlineWidth80
singleQuotequoteStyle: "single""double"
semisemicolons"always"
trailingCommatrailingCommas"all"
endOfLinelineEnding"lf"

Test the formatter on your project:

# Format and write changes
npx @biomejs/biome format --write .
 
# Check without modifying (dry run)
npx @biomejs/biome format .

Step 3: Configure the Linter

Biome's linter includes over 300 rules, covering most popular rules from eslint, typescript-eslint, eslint-plugin-react, eslint-plugin-react-hooks, and eslint-plugin-jsx-a11y.

By default, "recommended": true enables a set of safe, consensus-based rules:

{
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true
    }
  }
}

Customize Rules

You can adjust each rule individually:

{
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "complexity": {
        "noExcessiveCognitiveComplexity": {
          "level": "warn",
          "options": {
            "maxAllowedComplexity": 20
          }
        }
      },
      "style": {
        "noNonNullAssertion": "warn",
        "useConst": "error",
        "useImportType": "error",
        "noParameterAssign": "error"
      },
      "suspicious": {
        "noExplicitAny": "warn",
        "noConsoleLog": "warn"
      },
      "correctness": {
        "noUnusedVariables": "error",
        "noUnusedImports": "error",
        "useExhaustiveDependencies": "warn"
      },
      "nursery": {
        "useSortedClasses": {
          "level": "warn",
          "options": {
            "attributes": ["className"],
            "functions": ["clsx", "cn", "cva"]
          }
        }
      }
    }
  }
}

Rules in the nursery category are experimental and may change between minor versions. Use them in warn mode rather than error in your CI.

Rule Categories

Biome organizes its rules into clear categories:

CategoryDescriptionExamples
correctnessLikely errorsnoUnusedVariables, useExhaustiveDependencies
suspiciousSuspicious codenoExplicitAny, noConsoleLog
styleCode conventionsuseConst, useImportType
complexityOverly complex codenoExcessiveCognitiveComplexity
performanceOptimizationsnoAccumulatingSpread, noDelete
securityVulnerabilitiesnoDangerouslySetInnerHtml
a11yAccessibilityuseAltText, useAriaProps
nurseryExperimentaluseSortedClasses

Run the linter:

# Lint only
npx @biomejs/biome lint .
 
# Lint with auto-fixes
npx @biomejs/biome lint --write .
 
# Lint + format + imports in one command
npx @biomejs/biome check --write .

Step 4: Migrate from ESLint and Prettier

If you have an existing project with ESLint and Prettier, Biome provides an automatic migration tool.

Automatic Migration

# Migrate ESLint configuration to Biome
npx @biomejs/biome migrate eslint --write
 
# Migrate Prettier configuration to Biome
npx @biomejs/biome migrate prettier --write

This reads your .eslintrc.* and .prettierrc.* files, then updates biome.json with the equivalent rules.

Concrete Example

Suppose this ESLint configuration:

// .eslintrc.json (before)
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react-hooks/recommended",
    "prettier"
  ],
  "rules": {
    "@typescript-eslint/no-unused-vars": "error",
    "@typescript-eslint/no-explicit-any": "warn",
    "react-hooks/exhaustive-deps": "warn",
    "no-console": "warn"
  }
}

After migration, Biome generates:

// biome.json (after migration)
{
  "linter": {
    "rules": {
      "recommended": true,
      "correctness": {
        "noUnusedVariables": "error",
        "useExhaustiveDependencies": "warn"
      },
      "suspicious": {
        "noExplicitAny": "warn",
        "noConsoleLog": "warn"
      }
    }
  }
}

Clean Up Old Files

Once the migration is validated, remove the old files:

# Remove ESLint files
rm -f .eslintrc .eslintrc.js .eslintrc.json .eslintrc.yml .eslintignore
 
# Remove Prettier files
rm -f .prettierrc .prettierrc.js .prettierrc.json .prettierrc.yml .prettierignore
 
# Uninstall dependencies
npm uninstall eslint prettier eslint-config-prettier \
  eslint-plugin-react eslint-plugin-react-hooks \
  @typescript-eslint/eslint-plugin @typescript-eslint/parser \
  eslint-config-next

Update npm Scripts

{
  "scripts": {
    "lint": "biome check .",
    "lint:fix": "biome check --write .",
    "format": "biome format --write ."
  }
}

Step 5: Import Organization

Biome automatically sorts your imports according to clear conventions. Enable this feature:

{
  "organizeImports": {
    "enabled": true
  }
}

Before / After

// Before — unordered imports
import { useState, useEffect } from "react";
import axios from "axios";
import styles from "./page.module.css";
import { Button } from "@/components/ui/button";
import type { User } from "@/types";
import { z } from "zod";
import Link from "next/link";
// After — automatically sorted
import { useEffect, useState } from "react";
 
import Link from "next/link";
import axios from "axios";
import { z } from "zod";
 
import type { User } from "@/types";
import { Button } from "@/components/ui/button";
import styles from "./page.module.css";

Biome groups imports in this order:

  1. Native Node.js modules (node:fs, node:path)
  2. External packages (react, next, zod)
  3. Internal imports (alias @/, relative paths)
  4. Type imports (type { ... })

The biome check --write command applies sorting alongside linting and formatting.


Step 6: VS Code Integration

Install the Extension

Search for "Biome" in the VS Code marketplace or install from the terminal:

code --install-extension biomejs.biome

Configure VS Code

Add these settings in .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"
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[json]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[jsonc]": {
    "editor.defaultFormatter": "biomejs.biome"
  }
}

Disable the ESLint and Prettier extensions to avoid conflicts. Biome replaces both.

Editor Features

With the Biome extension, you get:

  • Format on save: code is automatically formatted on every save
  • Real-time diagnostics: linting errors appear directly in the editor
  • Quick fixes: Ctrl+. suggests automatic corrections
  • Import sorting: imports are reorganized on save
  • Refactorings: renaming, extraction, and other code actions

Step 7: CI/CD Integration

GitHub Actions

Create .github/workflows/lint.yml:

name: Lint & Format
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  biome:
    name: Biome Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Biome
        uses: biomejs/setup-biome@v2
        with:
          version: latest
 
      - name: Run Biome
        run: biome ci .

The biome ci command is optimized for CI pipelines: it does not write files, produces a diagnostics report, and returns a non-zero exit code if issues are found.

GitLab CI

Add to .gitlab-ci.yml:

biome:
  stage: validate
  image: node:20-slim
  before_script:
    - npm ci
  script:
    - npx @biomejs/biome ci .
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

Pre-commit Hook with Husky

# Install Husky
npm install --save-dev husky
npx husky init
 
# Add pre-commit hook
echo "npx @biomejs/biome check --staged --no-errors-on-unmatched" > .husky/pre-commit

The --staged flag only checks staged files, making the hook nearly instant.


Step 8: Advanced Configuration

Per-Directory Overrides

Biome allows different configurations based on directories or file patterns:

{
  "overrides": [
    {
      "include": ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts"],
      "linter": {
        "rules": {
          "suspicious": {
            "noExplicitAny": "off"
          }
        }
      }
    },
    {
      "include": ["scripts/**"],
      "linter": {
        "rules": {
          "suspicious": {
            "noConsoleLog": "off"
          }
        }
      }
    },
    {
      "include": ["**/*.config.ts", "**/*.config.js"],
      "formatter": {
        "lineWidth": 120
      }
    }
  ]
}

Ignoring Files

{
  "files": {
    "ignore": [
      "node_modules",
      ".next",
      "dist",
      "build",
      "coverage",
      "*.min.js",
      "generated/**"
    ]
  },
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true
  }
}

With "useIgnoreFile": true, Biome automatically respects your .gitignore.

Monorepo Configuration

For a monorepo, place biome.json at the root and use extends in packages:

my-monorepo/
├── biome.json              # Base configuration
├── packages/
│   ├── web/
│   │   └── biome.json      # Extends root config
│   ├── api/
│   │   └── biome.json      # Extends root config
│   └── shared/
│       └── biome.json      # Extends root config
// packages/web/biome.json
{
  "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
  "extends": ["../../biome.json"],
  "linter": {
    "rules": {
      "a11y": {
        "recommended": true
      }
    }
  }
}

Step 9: Complete Configuration for Next.js

Here is a complete, production-ready biome.json configuration for Next.js:

{
  "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true
  },
  "files": {
    "ignoreUnknown": true,
    "ignore": [
      ".next/**",
      "node_modules/**",
      "public/**",
      ".contentlayer/**"
    ]
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100,
    "lineEnding": "lf"
  },
  "organizeImports": {
    "enabled": true
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "double",
      "semicolons": "always",
      "trailingCommas": "all",
      "arrowParentheses": "always",
      "bracketSpacing": true
    }
  },
  "json": {
    "formatter": {
      "trailingCommas": "none"
    }
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "correctness": {
        "noUnusedVariables": "error",
        "noUnusedImports": "error",
        "useExhaustiveDependencies": "warn"
      },
      "style": {
        "useConst": "error",
        "useImportType": "error",
        "noNonNullAssertion": "warn"
      },
      "suspicious": {
        "noExplicitAny": "warn",
        "noConsoleLog": "warn"
      },
      "nursery": {
        "useSortedClasses": {
          "level": "warn",
          "options": {
            "attributes": ["className"],
            "functions": ["clsx", "cn", "cva"]
          }
        }
      }
    }
  },
  "overrides": [
    {
      "include": ["**/*.test.ts", "**/*.test.tsx"],
      "linter": {
        "rules": {
          "suspicious": {
            "noExplicitAny": "off",
            "noConsoleLog": "off"
          }
        }
      }
    }
  ]
}

Step 10: Benchmark — ESLint + Prettier vs Biome

Let us measure the performance difference on a real project. Create a benchmark script:

#!/bin/bash
# benchmark.sh
 
echo "=== Benchmark: 500 TypeScript files ==="
echo ""
 
# Create test files
mkdir -p bench-files
for i in $(seq 1 500); do
  cat > "bench-files/file-$i.tsx" << 'CONTENT'
import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
import { z } from "zod";
 
const schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().min(0).max(150),
});
 
type FormData = z.infer<typeof schema>;
 
export function UserForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
  const [data, setData] = useState<FormData>({ name: "", email: "", age: 0 });
  const [errors, setErrors] = useState<string[]>([]);
 
  const validate = useCallback(() => {
    const result = schema.safeParse(data);
    if (!result.success) {
      setErrors(result.error.errors.map((e) => e.message));
      return false;
    }
    setErrors([]);
    return true;
  }, [data]);
 
  useEffect(() => {
    validate();
  }, [validate]);
 
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        if (validate()) onSubmit(data);
      }}
    >
      <input
        type="text"
        value={data.name}
        onChange={(e) => setData({ ...data, name: e.target.value })}
      />
      <input
        type="email"
        value={data.email}
        onChange={(e) => setData({ ...data, email: e.target.value })}
      />
      <Link href="/users">Back</Link>
      {errors.map((err) => (
        <p key={err}>{err}</p>
      ))}
    </form>
  );
}
CONTENT
done
 
echo "--- ESLint + Prettier ---"
time npx eslint bench-files/ 2>/dev/null
time npx prettier --check bench-files/ 2>/dev/null
 
echo ""
echo "--- Biome ---"
time npx @biomejs/biome check bench-files/
 
# Cleanup
rm -rf bench-files

Typical Results

Tool500 files1000 files5000 files
ESLint~8s~18s~95s
Prettier~3s~7s~35s
ESLint + Prettier~11s~25s~130s
Biome~0.4s~0.8s~3.5s

Biome is approximately 25 to 35 times faster thanks to its Rust architecture and parallel parsing.


Troubleshooting

Common Issues

Formatting differs from Prettier

Biome aims for Prettier compatibility but some edge cases differ. If a specific difference blocks you:

{
  "formatter": {
    "attributePosition": "multiline"
  }
}

Conflict with Prettier extension in VS Code

Disable the Prettier extension or limit it to languages not supported by Biome:

{
  "prettier.disableLanguages": [
    "javascript",
    "typescript",
    "javascriptreact",
    "typescriptreact",
    "json"
  ]
}

Custom ESLint rules not migrated

Some rules specific to third-party ESLint plugins are not yet supported by Biome. Check the compatibility matrix on the official site to verify support for your rules.

"File too large" error

By default, Biome ignores files larger than 1 MB. Adjust if needed:

{
  "files": {
    "maxSize": 2097152
  }
}

Next Steps

Now that Biome is set up in your project:

  • Explore nursery rules: new rules are added with each release, like useSortedClasses for sorting Tailwind classes
  • Configure Git hooks so every commit is automatically checked
  • Add CSS support: Biome now supports CSS/SCSS linting and formatting (experimental)
  • Try biome search: structural search in your code with GritQL patterns
  • Contribute: Biome is open source and welcomes contributions on GitHub

Conclusion

Biome represents a paradigm shift in JavaScript tooling. By replacing ESLint and Prettier with a single Rust-powered tool, you get:

  • 25 to 35x faster speed on linting and formatting
  • Simplified configuration with a single biome.json file
  • Zero conflicts between linter and formatter
  • Clearer diagnostics with integrated fix suggestions
  • A unified ecosystem expanding to CSS, GraphQL, and soon HTML

The migration from ESLint + Prettier is progressive and assisted by automatic migration tools. In 2026, Biome has become the default choice for new projects and a recommended upgrade for existing ones.


Want to read more tutorials? Check out our latest tutorial on Tailwind CSS v4: The Complete Guide to CSS-First Configuration, Migration, and New Features.

Discuss Your Project with Us

We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.

Let's find the best solutions for your needs.

Related Articles