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 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.
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:
.eslintrc, .prettierrc, and import sorting configurations into one biome.json, reducing maintenance burden and eliminating conflicts between tools.eslint-config-* and prettier-plugin-* packages.biome ci command replaces multiple tool invocations in your pipeline, reducing build times and simplifying workflow configuration.Before starting, ensure you have:
npx create-strapi@latest my-project if needed)Familiarity with Strapi's project structure helps when configuring file exclusions.
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/biomeThe -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.
Generate the configuration file using the biome init command:
npx @biomejs/biome initThis 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.
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:
.gitignore patterns automaticallynoFloatingPromises: Critical for Strapi's async controllers and services, catches unhandled promises that could cause silent failures in asynchronous operationssrc/ (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 developersAdd 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:
| Script | Behavior |
|---|---|
lint | Reports linting issues in code without modifying files, using the biome check command |
lint:fix | Applies safe, automatically fixable linting errors using biome check --write, transforming code to match linting rules |
format | Shows code formatting differences according to Biome's configuration without writing changes to disk |
format:fix | Formats all code according to Biome configuration and writes changes directly to files using biome format --write |
biome:ci | CI-optimized unified check that runs linting, formatting, and import organization simultaneously, designed to fail on any issue encountered in continuous integration pipelines |
Run the linter against your project using the biome check command:
npm run lintIf 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:fixInstall 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.
Pre-commit hooks catch issues before they reach your repository. Install dependencies:
npm install -D husky lint-stagedInitialize Husky:
npx husky initAdd the pre-commit hook:
echo "npx lint-staged" > .husky/pre-commitConfigure 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.
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.
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.
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.
npx create-strapi@latest blog-api --quickstart
cd blog-apinpm install -D -E @biomejs/biome
npx @biomejs/biome initReplace biome.json with the Strapi-optimized configuration from Step 3 above.
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)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;
},
}));npm run lintBiome 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;
}npm run lint:fixBiome 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;
},
}));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 };
},
}));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,
},
},
],
};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.
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.
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.