BiomeJS represents a paradigm shift in JavaScript tooling as a comprehensive, Rust-based development utility that combines linting, formatting, and code organization capabilities in a single executable.
What sets BiomeJS apart from conventional JavaScript tooling is its architecture—written in Rust rather than JavaScript, it operates at native speeds, processing code analysis and transformations dramatically faster than its JavaScript counterparts. This article will explore how BiomeJS can transform your development workflow through its unified configuration system.
Prerequisites
Before diving into BiomeJS, you'll need the latest version of Node.js 22.x or newer installed on your system. This guide also assumes you have basic experience with JavaScript or TypeScript development and understand fundamental concepts like linting (static code analysis) and formatting (enforcing consistent code style).
Getting started with BiomeJS
Let's create a sandbox project to experiment with BiomeJS hands-on. Unlike traditional JavaScript tools that use a complex system of plugins and dependencies, BiomeJS provides a clean, self-contained setup experience. Begin by creating a new project folder and initializing it:
mkdir biome-demo && cd biome-demo
npm init -y
Now install BiomeJS as a development dependency. Note that this package includes a pre-built binary tailored to your operating system, which explains its larger size compared to JavaScript-only packages:
npm install --save-dev @biomejs/biome
One of BiomeJS's design principles is "convention over configuration," but it still provides robust customization options. Create a biome.json
configuration file in your project root:
{
"$schema": "https://biomejs.dev/schemas/1.4.1/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
}
}
This configuration activates BiomeJS's three primary features, each of which would typically require separate tools in traditional setups:
- The import organizer, which intelligently groups and sorts import statements
- The linter with its curated set of recommended rules for catching potential bugs and enforcing best practices
- The formatter that enforces consistent code style without requiring separate Prettier configuration
To see BiomeJS in action, create a deliberately messy JavaScript file with some common code issues:
import { readFileSync } from "fs";
import path from "path";
// Intentionally messy code to demonstrate BiomeJS
function processFile(fileName) {
const content = readFileSync(fileName, 'utf8')
let result = content.split('\n').map((line) => {
return line.trim()
}).filter(line => line !== '')
console.log("Results:", result);
return result
}
// Unused variable to trigger linting warning
const unusedVar = "This will trigger a warning";
processFile('./package.json');
Unlike using separate tools where you'd need multiple commands for linting and formatting, BiomeJS provides a unified interface. Run the following command to analyze and fix issues in one step:
npx @biomejs/biome check --apply index.js
The output illustrates BiomeJS's clear, actionable feedback system:
index.js:11:7 lint/style/useConst Use 'const' instead of 'let' for variable 'result' that is never reassigned.
|
11 | let result = content.split('\n').map((line) => {
| ^^^^^^
index.js:19:7 lint/correctness/noUnusedVariables 'unusedVar' is defined but never used.
|
19 | const unusedVar = "This will trigger a warning";
| ^^^^^^^^^
2 problems (0 errors, 2 warnings, 0 hints)
Examining your file after running this command reveals BiomeJS's dual-mode operation: it has automatically fixed formatting issues (spacing, indentation, line breaks) while leaving linting warnings for you to address deliberately. This approach respects that some code changes require developer judgment rather than automatic fixes.
Understanding BiomeJS components
BiomeJS's architecture fundamentally differs from traditional JavaScript tooling by unifying previously separate concerns into a single, coherent system. Understanding each component will help you appreciate how they work together synergistically rather than as isolated tools.
Linter
BiomeJS's linter doesn't simply clone ESLint's functionality—it's been reimagined from first principles. Written in Rust, the linter achieves dramatically faster analysis times, which is particularly noticeable in larger codebases where ESLint often becomes a bottleneck. Unlike ESLint's plugin architecture, which requires maintaining dozens of dependencies, BiomeJS bundles carefully curated rules targeting JavaScript, TypeScript, JSX, and TSX formats.
The rule system follows a hierarchical organization that makes customization intuitive:
{
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedVariables": "off"
}
}
}
}
This configuration demonstrates BiomeJS's layered approach—you start with the "recommended" ruleset as a foundation, then selectively adjust specific rules. Here, we're disabling the unused variable check, which might be desirable during rapid prototyping phases.
Formatter
Unlike Prettier, which operates as a separate tool with its own configuration system, BiomeJS's formatter is designed to work harmoniously with its linter from the ground up. This integration eliminates the common pain point of "rule fights" when ESLint and Prettier disagree about code formatting.
The formatter features opinionated defaults influenced by Prettier's philosophy but provides configuration options when team preferences differ:
{
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"formatWithErrors": false,
"include": ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx"],
"ignore": ["**/node_modules/**", "**/dist/**"]
}
}
The above example shows how to increase the default line width to 100 characters and configure specific pattern matching for files to include or exclude from formatting—a much simpler approach than maintaining separate .prettierignore and .eslintignore files.
Import sorter
BiomeJS's import organizer addresses a common pain point in collaborative development—inconsistent import ordering that leads to unnecessary merge conflicts. Unlike dedicated import sorting tools, BiomeJS's implementation works in concert with its formatter and linter, ensuring that imports are sorted and properly formatted and validated.
The organizer offers sophisticated grouping capabilities that adapt to project structures:
{
"organizeImports": {
"enabled": true,
"groups": [
"builtin",
"external",
"internal",
"parent",
"sibling",
"index",
"object",
"type",
"unknown"
]
}
}
This configuration demonstrates the semantic grouping system that automatically categorizes imports based on their source—separating Node.js built-ins from third-party packages, internal modules, and relative imports. The result is logically grouped imports that enhance code readability and organization without manual intervention.
Integrating BiomeJS with your workflow
BiomeJS's unified approach shines brightest when properly integrated into your development workflow. Unlike traditional toolchains that require coordinating multiple commands and tools, BiomeJS simplifies this process considerably.
Command-line interface
BiomeJS features a streamlined CLI designed around common developer workflows. Unlike ESLint and Prettier which require separate commands for linting and formatting, BiomeJS provides logical groupings of functionality:
Analyze files without making changes (combines lint+format checking):
npx @biomejs/biome check ./src
Apply formatting changes only:
npx @biomejs/biome format --write ./src
Run only the linter:
npx @biomejs/biome lint ./src
One-step analysis and fix (great for CI/pre-commit):
px @biomejs/biome check --apply ./src
Note the logical progression of commands—you can check without changing, focus on specific aspects (format or lint), or combine operations. BiomeJS's Rust foundation makes these operations surprisingly fast, even on large codebases where traditional tools might take several seconds or even minutes.
Git hooks for consistent code quality
BiomeJS's speed makes it ideal for Git hook integration, where slow-running tools frustrate developers.
Set up automated quality checks using Husky:
npm install --save-dev husky
npx husky init
Then, create a pre-commit hook that leverages BiomeJS's unified capabilities:
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
BiomeJS's performance allows checking changed files only:
npx @biomejs/biome check --apply $(git diff --staged --name-only --diff-filter=ACMR | grep -E '\.(js|ts|jsx|tsx)
Migrating from other tools to BiomeJS
Switching from tools like ESLint, Prettier, or TSLint to BiomeJS involves a bit of preparation, but it can be done gradually. A good starting point is installing BiomeJS alongside your current setup. This lets you test things out without disrupting your existing configuration. You can install it with the following command:
npm install --save-dev @biomejs/biome
Once installed, create a basic BiomeJS configuration that won’t interfere with your current tools. For example, you might enable linting but keep the formatter disabled for now:
{
"$schema": "https://biomejs.dev/schemas/1.4.1/schema.json",
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": false
}
}
To understand how BiomeJS compares to what you already use, try running both side-by-side. For instance:
npx eslint ./src && npx @biomejs/biome check ./src
As you get more comfortable, start phasing out parts of your old tooling. A typical approach is to enable BiomeJS's formatter before adjusting any linting rules. Your updated configuration might look like this:
{
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
}
}
After you're satisfied with the setup, update your scripts and CI/CD pipelines to use BiomeJS exclusively. When everything is running smoothly, you can remove the older tools and their configurations. For example:
npm uninstall eslint prettier eslint-config-prettier
rm .eslintrc .prettierrc
Troubleshooting common issues
Occasionally, you may run into conflicts between BiomeJS rules and TypeScript behavior. One example is when a rule like noExplicitAny
conflicts with your project's type annotations. In such cases, it's possible to disable or adjust rules in your configuration selectively:
{
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
}
}
}
}
This flexibility allows you to fine-tune BiomeJS to match your project's needs without disrupting your development flow.
Final thoughts
This article provides a comprehensive overview of BiomeJS by discussing its key features and how to configure and customize it for your specific needs. We hope the information in this guide has helped you understand how to use BiomeJS effectively in your JavaScript and TypeScript projects.
It's impossible to learn everything about BiomeJS and its capabilities in one article, so I highly recommend consulting the official documentation for more information on its basic and advanced features.
Thanks for reading, and happy coding!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github