Biome

Discover how to set up Biome in your Strapi v5 project for unified linting and formatting. This guide covers installation, configuration, Git hooks, and practical examples to maintain consistent code quality

Biome  logo

What Is Biome?

Biome is a Rust-based toolchain that consolidates linting, formatting, and import sorting for JavaScript and TypeScript into a single unified tool. It positions itself as a direct replacement for the ESLint plus Prettier combination that most Node.js projects currently use.

The tool includes 436 linting rules sourced from ESLint, TypeScript ESLint, and other ecosystem tools. Its formatter maintains 97% compatibility with Prettier while running significantly faster due to its compiled Rust architecture and parallel processing capabilities.

Why Integrate Biome with Strapi

Strapi v5 introduced a TypeScript-first architecture that pairs naturally with Biome's capabilities. Here's what the integration offers for your Strapi development workflow:

  • Faster feedback loops: Biome's Rust-based architecture with parallel processing and incremental caching provides performance advantages during development when working across Strapi's admin panel customizations, API routes, and custom plugins simultaneously.
  • Single configuration file: Consolidate .eslintrc, .prettierrc, and import sorting configurations into one biome.json, reducing maintenance burden and eliminating conflicts between tools.
  • TypeScript-aware linting: Biome's type inference engine enables type-aware linting rules, including detection of floating promises in async contexts, without requiring the TypeScript compiler in your linting pipeline.
  • Unified tooling approach: Biome handles formatting, linting, and import organization through a single tool with built-in configuration, reducing dependency management complexity compared to managing separate eslint-config-* and prettier-plugin-* packages.
  • CI/CD optimization: The biome ci command replaces multiple tool invocations in your pipeline, reducing build times and simplifying workflow configuration.
  • Consistent team standards: One configuration file in version control ensures everyone runs identical checks, whether in their IDE, pre-commit hooks, or CI environment.

How to Integrate Biome with Strapi

Prerequisites

Before starting, ensure you have:

  • Node.js 18 or later installed
  • A Strapi v5 project (create one with npx create-strapi@latest my-project if needed)
  • Package manager: npm, pnpm, or yarn
  • Git initialized in your project (for hooks integration)

Familiarity with Strapi's project structure helps when configuring file exclusions.

Step 1: Install Biome

Add Biome as a development dependency with exact version pinning using the -E flag:

# npm
npm install -D -E @biomejs/biome

# pnpm
pnpm add -D -E @biomejs/biome

# yarn
yarn add -D -E @biomejs/biome

# bun
bun add -D -E @biomejs/biome

The -E flag (or --save-exact) ensures an exact version is pinned in package.json, preventing unexpected updates that could introduce breaking changes in your Strapi v5 project.

Step 2: Initialize Configuration

Generate the configuration file using the biome init command:

npx @biomejs/biome init

This creates a biome.json or biome.jsonc file at your project root with sensible defaults. The generated file enables formatting, linting with recommended rules, and import organization.

Step 3: Configure for Strapi v5

Strapi v5 generates several directories that shouldn't be linted: build artifacts like dist/ and build/, temporary files in .tmp/, auto-generated admin files in .strapi/, and auto-generated TypeScript type definitions in src/types/. Replace the default biome.json with this Strapi-optimized configuration:

{
  "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true
  },
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "complexity": {
        "noExcessiveCognitiveComplexity": "error",
        "noUselessCatch": "error"
      },
      "correctness": {
        "noUnusedVariables": "error",
        "noUndeclaredVariables": "error",
        "noDebugger": "error"
      },
      "suspicious": {
        "noFloatingPromises": "error",
        "noDoubleEquals": "error",
        "noImplicitCoercions": "error",
        "noConsoleLog": "warn"
      },
      "style": {
        "useConst": "error",
        "useExplicitFunctionReturnType": "warn"
      }
    }
  },
  "formatter": {
    "enabled": true,
    "formatWithErrors": false,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100,
    "lineEnding": "lf"
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single",
      "jsxQuoteStyle": "double",
      "semicolons": "always",
      "trailingCommas": "es5",
      "arrowParentheses": "always"
    }
  },
  "files": {
    "include": ["src/**/*", "config/**/*"],
    "ignore": [
      "node_modules",
      "dist",
      "build",
      ".tmp",
      ".strapi",
      ".cache",
      "src/types",
      "public/uploads",
      "coverage",
      "*.min.js"
    ]
  }
}

Configuration breakdown:

  • VCS integration: Respects your .gitignore patterns automatically
  • noFloatingPromises: Critical for Strapi's async controllers and services, catches unhandled promises that could cause silent failures in asynchronous operations
  • File inclusions: Targets src/ (your custom API logic, plugins, and admin customizations) and config/ (Strapi configuration files)
  • src/types exclusion: Strapi v5 automatically generates TypeScript definitions in this directory. Excluding it from linting prevents noise since these files are auto-generated and not manually edited by developers

Step 4: Add npm Scripts

Add convenience scripts to package.json:

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

Each script serves a different purpose:

ScriptBehavior
lintReports linting issues in code without modifying files, using the biome check command
lint:fixApplies safe, automatically fixable linting errors using biome check --write, transforming code to match linting rules
formatShows code formatting differences according to Biome's configuration without writing changes to disk
format:fixFormats all code according to Biome configuration and writes changes directly to files using biome format --write
biome:ciCI-optimized unified check that runs linting, formatting, and import organization simultaneously, designed to fail on any issue encountered in continuous integration pipelines

Step 5: Verify the Setup

Run the linter against your project using the biome check command:

npm run lint

If everything is configured correctly, Biome processes your src/ and config/ directories while ignoring Strapi's auto-generated files and build artifacts. Fix any reported issues:

npm run lint:fix

Step 6: Configure VS Code Integration

Install the Biome VS Code extension from the marketplace, then configure workspace settings in .vscode/settings.json:

{
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  },
  "[javascript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[json]": {
    "editor.defaultFormatter": "biomejs.biome"
  }
}

Now saving a file triggers formatting, import organization, and safe lint fixes automatically.

Step 7: Set Up Git Hooks

Pre-commit hooks catch issues before they reach your repository. Install dependencies:

npm install -D husky lint-staged

Initialize Husky:

npx husky init

Add the pre-commit hook:

echo "npx lint-staged" > .husky/pre-commit

Configure lint-staged in package.json:

{
  "lint-staged": {
    "*.{js,ts,jsx,tsx,json}": ["biome check --write --no-errors-on-unmatched"]
  }
}

The --no-errors-on-unmatched flag prevents Biome from throwing errors when it encounters files that don't match its configured linting patterns, which is particularly useful when using lint-staged with glob patterns that may include files outside Biome's scope.

Step 8: Add CI/CD Integration

For GitHub Actions, create .github/workflows/code-quality.yml:

name: Code Quality
on: [push, pull_request]
jobs:
  biome:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx @biomejs/biome ci .

The biome ci command is optimized for CI environments. It performs checks in read-only mode without writing files to disk and exits with non-zero status codes on issues for pipeline integration.

Apply Fixes

Before committing code to your Strapi v5 project, Biome can automatically apply fixes to resolve linting issues and formatting inconsistencies.

The --write flag applies only safe, automatically fixable issues:

npx @biomejs/biome check --write .

Safe fixes include formatting inconsistencies, import organization, and unambiguous rule violations like useConst converting var to const.

For potentially unsafe fixes that may require review, add the --unsafe flag:

npx @biomejs/biome check --write --unsafe .

Unsafe fixes should only be applied after careful review, as they may change code behavior.

Project Example: Blog API with Enforced Code Quality

Let's build a practical example: a blog API with content types defined in Strapi v5's project structure and custom business logic, demonstrating how Biome catches common issues during development.

Create the Strapi Project

npx create-strapi@latest blog-api --quickstart
cd blog-api

Initialize Biome

npm install -D -E @biomejs/biome
npx @biomejs/biome init

Replace biome.json with the Strapi-optimized configuration from Step 3 above.

Create a Content Type

Using the Strapi Admin Panel at http://localhost:1337/admin, create an "Article" collection type with these fields:

  • title (Text, required)
  • content (Rich Text)
  • slug (UID, attached to title)
  • publishedAt (DateTime)

Add Custom Service Logic

Create a custom service with intentional issues that Biome will catch. Add this to src/api/article/services/article.ts:

import { factories } from '@strapi/strapi';

export default factories.createCoreService('api::article.article', ({ strapi }) => ({
  async findPublished() {
    let articles = strapi.documents('api::article.article').findMany({
      filters: {
        publishedAt: {
          $notNull: true,
        },
      },
      sort: { publishedAt: 'desc' },
    });

    return articles;
  },

  async findBySlug(slug) {
    const article = await strapi.documents('api::article.article').findFirst({
      filters: { slug },
    });

    if (slug == null) {
      return null;
    }

    return article;
  },
}));

Run the Linter

npm run lint

Biome reports code issues through its comprehensive rule set:

src/api/article/services/article.ts:6:5 lint/style/useConst
  Use 'const' instead of 'let'

src/api/article/services/article.ts:6:21 lint/suspicious/noFloatingPromises
  Unhandled promise - must be awaited or returned

src/api/article/services/article.ts:24:9 lint/suspicious/noDoubleEquals
  Use === instead of ==

The noFloatingPromises error is critical. In Strapi v5 services, this pattern is incorrect:

// ❌ Incorrect - floating promise
async findPublished() {
  let articles = strapi.documents('api::article.article').findMany();
  return articles;
}

The corrected pattern properly awaits the promise:

// ✅ Correct - promise awaited
async findPublished() {
  const articles = await strapi.documents('api::article.article').findMany();
  return articles;
}

Apply Fixes

npm run lint:fix

Biome automatically fixes safe issues. However, noFloatingPromises requires manual intervention since you need to decide whether to await or return the promise.

The corrected service:

import { factories } from '@strapi/strapi';

export default factories.createCoreService('api::article.article', ({ strapi }) => ({
  async findPublished() {
    const articles = await strapi.documents('api::article.article').findMany({
      filters: {
        publishedAt: {
          $notNull: true,
        },
      },
      sort: { publishedAt: 'desc' },
    });

    return articles;
  },

  async findBySlug(slug: string) {
    if (slug === null) {
      return null;
    }

    const article = await strapi.documents('api::article.article').findFirst({
      filters: { slug },
    });

    return article;
  },
}));

Create a Custom Controller

Add a controller that uses the custom service at src/api/article/controllers/article.ts:

import { factories } from '@strapi/strapi';

export default factories.createCoreController('api::article.article', ({ strapi }) => ({
  async findPublished(ctx) {
    const articles = await strapi.service('api::article.article').findPublished();

    ctx.body = {
      data: articles,
      meta: {
        count: articles.length,
      },
    };
  },

  async findBySlug(ctx) {
    const { slug } = ctx.params;
    const article = await strapi.service('api::article.article').findBySlug(slug);

    if (!article) {
      return ctx.notFound('Article not found');
    }

    ctx.body = { data: article };
  },
}));

Add Custom Routes

Create src/api/article/routes/custom-routes.ts:

export default {
  routes: [
    {
      method: 'GET',
      path: '/articles/published',
      handler: 'article.findPublished',
      config: {
        auth: false,
      },
    },
    {
      method: 'GET',
      path: '/articles/slug/:slug',
      handler: 'article.findBySlug',
      config: {
        auth: false,
      },
    },
  ],
};

Final Verification

Run the full check:

npx @biomejs/biome check .

With properly structured code, Biome reports no issues. Your Strapi API now has enforced code quality standards that run on every save, every commit, and every CI build.

Strapi Open Office Hours

If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST: Strapi Discord Open Office Hours.

For more details, visit the Strapi documentation and Biome documentation.

Frequently Answered Questions

Biome consolidates linting and formatting into a single tool, replacing separate ESLint and Prettier configurations. For Strapi v5 projects, this means one biome.json file instead of multiple config files. Biome's Rust-based architecture provides faster feedback loops through parallel processing and native code compilation, particularly beneficial for larger projects with custom plugins.

Yes, Biome provides automated migration commands. Run npx @biomejs/biome migrate eslint --write to port ESLint rules, then npx @biomejs/biome migrate prettier --write for formatting settings. After migration, review the generated biome.json and remove old ESLint and Prettier dependencies. Note that YAML-based ESLint configurations may require manual adjustments.

Exclude all auto-generated and build directories: .strapi/ (admin panel cache), dist/ (compiled backend), build/ (built admin assets), .tmp/ (temporary files), .cache/ (development cache), and src/types/ (auto-generated TypeScript definitions). Include src/ for custom code and config/ for Strapi configuration files.

Biome includes its own type inference engine that provides type-aware linting without depending on the TypeScript compiler. Rules like noFloatingPromises detect unhandled promises in async functions, which is valuable for Strapi controllers and services where async operations are prevalent.

Configure rule overrides in biome.json for specific files using the overrides property. For example, to allow console.log in test files. You can also use inline comments like // biome-ignore lint/suspicious/noConsoleLog: intentional debug output for individual lines.