Using Biome as an All-in-One Linter and Formatter in CI
Biome is a high-performance JavaScript and TypeScript toolchain that replaces ESLint, Prettier, and several other tools with a single binary. It is written in Rust, has near-zero configuration overhead, and runs lint checks 10–100× faster than the Node.js equivalents. This guide covers everything you need to integrate Biome into a CI pipeline: rule configuration, unsafe auto-fixes, and migrating an existing ESLint project.
Why Biome in CI?
The classic JavaScript toolchain for code quality involves at least three tools: ESLint for linting, Prettier for formatting, and often a pre-commit hook tool like lint-staged. Each requires separate configuration files, plugins, and careful version pinning. Biome collapses this into one binary with one config file.
Key advantages for CI:
- Single binary, no plugin ecosystem to maintain
- Structured JSON/JSONC config (
biome.json) — no plugin resolution - Sub-second lint and format checks on large codebases
- First-class TypeScript support without
@typescript-eslint - Deterministic output across platforms (no env-specific Prettier quirks)
Installation
# npm
npm install --save-dev --save-exact @biomejs/biome
<span class="hljs-comment"># pnpm
pnpm add --save-dev --save-exact @biomejs/biome
<span class="hljs-comment"># bun
bun add --dev --exact @biomejs/biomeUse --save-exact to pin the version — Biome's rule set evolves between minor versions and you want reproducible CI runs.
Generate the default config:
npx @biomejs/biome initThis creates biome.json at the project root.
Configuration Overview
A production-ready biome.json for a TypeScript React project:
{
"$schema": "https://biomejs.dev/schemas/1.8.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",
"useTemplate": "error",
"noNonNullAssertion": "warn"
},
"performance": {
"noAccumulatingSpread": "error"
},
"a11y": {
"useAltText": "error",
"noAutofocus": "warn"
}
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"lineEnding": "lf"
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"trailingCommas": "es5",
"semicolons": "always"
}
},
"files": {
"ignore": [
"node_modules",
"dist",
".next",
"coverage",
"*.min.js"
]
}
}Rule Severity Levels
Each rule accepts "off", "warn", or "error". Use "error" for anything that should block a CI merge. Use "warn" for rules you want visible but not blocking during a migration period.
Per-File Overrides
Override rules for specific file patterns:
{
"overrides": [
{
"include": ["**/*.test.ts", "**/*.spec.ts"],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
}
}
}
},
{
"include": ["scripts/**/*.js"],
"linter": {
"rules": {
"suspicious": {
"noConsoleLog": "off"
}
}
}
}
]
}CI Integration
GitHub Actions
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
biome:
name: Lint & Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run Biome check
run: npx @biomejs/biome ci --reporter=github .The ci command combines lint + format checks and exits with a non-zero code on any violation. The --reporter=github flag annotates pull request diffs directly in GitHub's review UI.
Separate Lint and Format Jobs
For more granular CI feedback:
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- name: Lint
run: npx @biomejs/biome lint --reporter=github .
format:
name: Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- name: Check formatting
run: npx @biomejs/biome format --check .GitLab CI
biome:
image: node:20-alpine
stage: lint
cache:
paths:
- node_modules/
script:
- npm ci
- npx @biomejs/biome ci .Auto-Fix in CI
Biome can write fixes to disk. In CI you typically want to apply only safe fixes automatically:
# Apply safe fixes and stage the changes
npx @biomejs/biome check --apply .
git diff --exit-code <span class="hljs-comment"># fail if there are uncommitted changesThis pattern is useful in a "commit bot" workflow where CI applies fixes and opens a PR. If you want CI to fail hard instead of auto-fixing, use --apply locally in pre-commit hooks and use biome ci (check only) in the pipeline.
Unsafe Fixes
Biome classifies auto-fixes into two categories:
- Safe fixes — applied by
--apply. They preserve semantics (e.g., removing unused imports, adding missingconst). - Unsafe fixes — applied by
--apply-unsafe. They may change runtime behaviour (e.g., collapsing optional chains, reordering class members).
Example of a rule that requires unsafe fix — noParameterAssign:
// Before
function greet(name: string) {
name = name.trim() // noParameterAssign violation
return `Hello, ${name}`
}
// After unsafe fix
function greet(name: string) {
const trimmedName = name.trim()
return `Hello, ${trimmedName}`
}Use --apply-unsafe selectively. The recommended CI workflow:
- Apply safe fixes in pre-commit (local developer machines).
- Run
biome ciin CI — fail on any remaining violation. - Reserve
--apply-unsafefor a dedicated clean-up branch when migrating legacy codebases.
# Local pre-commit hook (via simple-git-hooks or husky)
npx @biomejs/biome check --apply $(git diff --name-only --cached)Migrating from ESLint
Biome ships a migration command that reads your ESLint config and generates an equivalent biome.json:
npx @biomejs/biome migrate eslint --writeThis command:
- Reads
.eslintrc.*oreslint.config.*(flat config) - Maps supported ESLint rules to Biome equivalents
- Prints a report of rules it could not migrate (unsupported plugins, custom rules)
What Gets Migrated Automatically
| ESLint Rule | Biome Equivalent |
|---|---|
no-unused-vars |
correctness/noUnusedVariables |
eqeqeq |
suspicious/noDoubleEquals |
no-console |
suspicious/noConsoleLog |
prefer-const |
style/useConst |
no-var |
style/noVar |
@typescript-eslint/no-explicit-any |
suspicious/noExplicitAny |
What Doesn't Migrate
- Custom ESLint plugins without a Biome equivalent (e.g.,
eslint-plugin-react-hooksrules likeexhaustive-deps) - Rules implemented in ESLint's plugin ecosystem that Biome hasn't yet replicated
For plugins without a Biome equivalent, you have two options:
- Accept the gap and rely on TypeScript's type-checker to catch those categories of bugs.
- Run ESLint in parallel for just those rules while using Biome for everything else.
Parallel Migration Strategy
// biome.json — handles formatting + most linting
{
"linter": { "enabled": true, "rules": { "recommended": true } },
"formatter": { "enabled": true }
}// .eslintrc.json — handles only unmigrated rules
{
"extends": [],
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}# CI runs both tools
- name: Biome check
run: npx @biomejs/biome ci .
- name: ESLint (react-hooks only)
run: npx eslint --no-eslintrc -c .eslintrc.json --ext .ts,.tsx src/This strategy lets you remove Prettier and most ESLint rules immediately while keeping the hooks plugin until Biome ships its own implementation.
Migrating from Prettier
npx @biomejs/biome migrate prettier --writeThis reads your Prettier config (prettierrc.*, .prettierignore) and maps settings to biome.json. After migration, remove the prettier package and any prettier-eslint integrations from package.json.
Editor Integration
Biome has a VS Code extension (biomejs.biome) and LSP support for Neovim via biome lsp-proxy. With the extension installed, format-on-save uses Biome instead of Prettier automatically for projects that have a biome.json.
// .vscode/settings.json
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
}
}Performance Comparison
On a representative TypeScript monorepo (400 files, ~50k lines):
| Tool | Task | Time |
|---|---|---|
| ESLint + Prettier | Full lint + format check | ~12s |
| Biome | Full lint + format check | ~0.4s |
| Biome | Changed files only (pre-commit) | ~50ms |
The speed difference matters most in pre-commit hooks where developer feedback latency is felt directly, and in CI where parallel jobs have startup overhead that dominates short checks.
Practical Recommendations
Start with recommended: true and suppress specific rules you can't fix immediately using // biome-ignore lint/suspicious/noExplicitAny: legacy code. Track suppressions in a follow-up ticket.
Run biome ci in PR checks, not biome check --apply. Auto-applying fixes in CI creates noisy commits and obscures the real state of the codebase.
Use exact version pinning (--save-exact). Biome's rule set evolves frequently; unexpected CI failures after a ^ upgrade are frustrating.
Integrate Biome diagnostics with your test runner. If you use HelpMeTest for end-to-end quality gates, you can add a Biome check health monitor that alerts your team when lint violations accumulate in a branch — keeping code quality visible without blocking local development.
Biome is production-ready for most TypeScript and JavaScript projects today. The migration from ESLint and Prettier is straightforward for projects using standard configurations and pays off immediately in faster CI feedback loops.