Chapter 15: Code Quality (Biome) and Content (Fumadocs)
In Chapter 14, we established a solid CI/CD pipeline that acts as the 'gatekeeper' for our SAAS application. One of the core responsibilities of this gatekeeper is running pnpm lint and pnpm typecheck. But what's actually working behind these commands?
Chapter 15: Code Quality (Biome) and Content (Fumadocs)
In Chapter 14, we established a solid CI/CD pipeline that acts as the "gatekeeper" for our SAAS application. One of the core responsibilities of this gatekeeper is running pnpm lint and pnpm typecheck. But what's actually working behind these commands?
At the same time, a SAAS without documentation is a black box. Users (and even your future self) won't be able to understand how to use your product.
In this chapter, we'll dive deep into two key tools that ensure code quality and content delivery: Biome (our code steward) and Fumadocs (our content delivery network).
15.1. Why Biome? (vs. ESLint + Prettier)
For most JavaScript/TypeScript developers (including those migrating from Python), "code quality" typically means a "double combo": ESLint (for code linting and rule checking) + Prettier (for code formatting).
This combination has been the standard for the past few years, but it also brings obvious pain points:
- Configuration Hell: You need to maintain multiple configuration files:
.eslintrc.js,.prettierrc,.eslintignore,.prettierignore, etc. - Conflicts: ESLint's formatting rules often conflict with Prettier's rules. You need to install
eslint-config-prettierandeslint-plugin-prettierto "turn off" ESLint's rules and let Prettier take over. - Performance: Both are written in JavaScript. In a large project, running
eslint . --fixandprettier . --writecan take tens of seconds or longer. - Plugin Dependencies: You need to install and configure a bunch of plugins for TypeScript (
@typescript-eslint/parser), React (eslint-plugin-react), Imports (eslint-plugin-import), etc.
Biome (formerly Rome) is the modern answer to this problem.
Biome is a high-performance all-in-one toolchain written in Rust, designed to replace ESLint, Prettier, tsc (some lint functionality), and more tools in the future.
Why did our SAAS template choose Biome?
- Blazing Fast: Written in Rust, it's orders of magnitude faster than the ESLint + Prettier combo. In CI,
pnpm lintruntime drops from tens of seconds to a few seconds. - Single Tool: One executable, one
biome.jsonconfiguration file, one command (biome checkorbiome format) is all you need. - Unified: Linting and Formatting are handled by the same engine, sharing the same AST (Abstract Syntax Tree), fundamentally eliminating conflicts between the two.
- Powerful Defaults: Biome provides very reasonable "recommended" rule sets that work out of the box, greatly reducing configuration burden.
- Built-in Features: It natively supports TypeScript, JSX, automatic import sorting (
organizeImports), without any plugins.
From a Python developer's perspective, this is like no longer needing three tools (flake8 + black + isort), but having one lightning-fast tool that handles everything.
15.2. [Code Analysis]: Analyzing biome.json Configuration
All of Biome's configuration is concentrated in the biome.json file in the project root. Here's an extremely simple configuration example:
// biome.json
{
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
// Specify the language used by the project
"javascript": {
"parser": {
// Allow unsafe 'any' types but warn about them
// Useful in transition projects
"unsafeParameterDecorators": true
},
// Specify Biome's formatter
"formatter": {
"semicolons": "asNeeded", // Add semicolons as needed
"quoteStyle": "single", // Prefer single quotes
"trailingComma": "all" // Trailing commas
}
},
// Formatter master switch
"formatter": {
"enabled": true,
"formatWithErrors": false, // Try to format even if code has lint errors
"indentStyle": "space", // Use space indentation
"indentWidth": 2, // Indent 2 spaces
"lineWidth": 80, // Line width 80 characters
"ignore": ["node_modules/", "dist/"] // Ignored directories
},
// Linter master switch
"linter": {
"enabled": true,
// Enable Biome's recommended rule set
"rules": {
"recommended": true,
// (Example) You can override specific rules
"suspicious": {
"noExplicitAny": "warn" // Downgrade "noExplicitAny" rule from "error" to "warn"
}
}
},
// Auto-organize imports
"organizeImports": {
"enabled": true
}
}Configuration Breakdown:
$schema: Provides autocomplete and validation in editors like VS Code.formatter: Defines all formatting rules, like 2-space indentation, 80-character line width. This single configuration section completely replaces Prettier.linter: Defines lint rules."recommended": trueautomatically enables a carefully selected core set of rules, replacing ESLint and its host of plugins.organizeImports: Replaceseslint-plugin-importorisort(in Python), automatically sorting and grouping import statements.
In package.json, our scripts become extremely simple:
"scripts": {
// ...
"lint": "biome check .",
"lint:fix": "biome check --apply .",
"format": "biome format --write ."
}In the GitHub Actions from Chapter 14, our pnpm lint (i.e., biome check .) is now an ultra-fast, unified quality check.
15.3. SAAS Documentation Site: Fumadocs
A SAAS product without documentation doesn't exist. You need a place to host:
- User Guides: How to register, how to use feature A, how to configure B.
- API Reference: If your SAAS provides an API, developers need to consult it.
- Tutorials and Guides: "How to solve [X problem] with our SAAS".
You need a documentation tool that must:
- Be easy to write (typically Markdown).
- Be compatible with your tech stack (Next.js 15+ App Router).
- Be fast, supporting modern features like search, dark mode, etc.
Fumadocs is a modern documentation generator built specifically for this. Unlike Docusaurus or GitBook, Fumadocs is a "Next.js-first" toolkit that leverages Next.js's App Router to build documentation sites, not a separate system.
Why choose Fumadocs?
- Based on App Router: It's not a "black box" framework but gives you a set of tools and components to use within Next.js App Router.
- MDX-Powered: You can seamlessly use React components in Markdown (
.mdx) files. - High Performance: Because it is Next.js, it enjoys all performance benefits like RSC, static generation, etc.
- Feature-Complete: Built-in search (powered by
next-docs-zeta), file tree navigation, i18n support, dark mode, etc.
15.4. [Code Analysis]: Analyzing content/ Directory and pnpm content
In our project, documentation content is stored in the content/ folder in the project root and served by our Next.js application (under the src/app/[locale]/docs/ route).
content/ Directory Structure
Fumadocs relies on a clear folder structure and uses _meta.json files to define the order and titles of sidebars.
.
├── content/
│ └── docs/
│ ├── _meta.json # (Defines "Root" sidebar)
│ ├── 01.introduction.mdx # Introduction
│ ├── 02.getting-started/ # "Getting Started" folder
│ │ ├── _meta.json
│ │ ├── 01.installation.mdx
│ │ └── 02.configuration.mdx
│ └── 03.api-reference/ # "API" folder
│ ├── _meta.json
│ ├── 01.auth.mdx
│ └── 02.payments.mdx
└── src/
└── app/
└── [locale]/
└── docs/ # Next.js routes to render 'content/'
└── [[...slug]]/
├── layout.tsx
└── page.tsx_meta.json File Breakdown
This _meta.json file is Fumadocs's "glue," telling Next.js how to organize pages in the content/ directory.
// content/docs/_meta.json
{
"title": "Documentation", // Sidebar group title
"pages": [
"introduction",
"getting-started", // Link to 'getting-started' folder
"---", // A separator
"api-reference" // Link to 'api-reference' folder
]
}
// content/docs/02.getting-started/_meta.json
{
"title": "Getting Started", // Title for this group
"pages": [
"installation",
"configuration"
]
}.mdx File Breakdown
The introduction.mdx file is just a mix of Markdown and React.
# Introduction
Welcome to our SAAS platform!
This is documentation written in MDX. You can use **standard Markdown** here.
<Callout type="warning">
You can also use custom React components defined in your codebase!
</Callout>The pnpm content Command?
In the context of this book, pnpm content is a fictional script we use for processing documentation content. In actual projects, it might represent:
pnpm docs:dev: A script defined inpackage.json, like"docs:dev": "next dev -p 3001", specifically for running the documentation site locally (if the documentation site is a separate app).pnpm content:validate: A script usingcontentlayeror similar tools to validate that all.mdxfiles' frontmatter conforms to the schema at build time.
For our integrated SAAS template, the content/ directory is just dynamically read by the src/app/.../docs routes. Therefore, no separate pnpm content command is needed. Running pnpm dev starts both the main app and documentation pages. Any changes in the content/ directory trigger Next.js's hot reload, just like changes in the src/app directory.
Chapter 15 Summary
In this chapter, we added two critical layers of "safety net" to our SAAS project:
- Code Quality (Biome): We replaced the traditional (and slow) ESLint + Prettier combo with a unified, ultra-fast Rust-based tool Biome. Through a single
biome.jsonfile, we unified Linting and Formatting, ensuring codebase consistency and greatly accelerating the CI process. - Content Delivery (Fumadocs): We chose Fumadocs as our documentation solution. Because it's deeply integrated with Next.js App Router, we can write rich documentation in MDX in the
content/directory while enjoying the same tech stack, performance, and development experience as our main SAAS application.
With Biome ensuring "internal quality" and Fumadocs ensuring "external clarity," our SAAS project has taken a major step forward in maintainability and user-friendliness.
Categories
content/ Directory Structure_meta.json File Breakdown.mdx File BreakdownThe pnpm content Command?Chapter 15 SummaryMore Posts
Chapter 1: Hello, JavaScript (A Python Developer's Perspective)
This is the first and most critical mindset shift you'll experience as a Python backend developer.
Chapter 10: Database Migrations and Operations
In Chapters 7 and 9, we defined our database table structure in the src/db/schema/ directory. But we left a critical question: when you modify the schema (like adding a bio field to usersTable), how do you safely apply this change to a production database that's already running live?
Chapter 3: React - Declarative UI (Goodbye Jinja2)
As a Python backend developer, you're probably most familiar with Jinja2. In Flask or Django, you fetch data from the database, 'inject' it into an HTML template, the server 'renders' an HTML string, and sends it to the browser. This process is imperative - you tell the template engine 'loop here, insert this variable here'...