Skip to content
·9 min read

Linting and Formatting with ESLint, Prettier, Biome

Stop arguing about semicolons and start catching real bugs automatically

Share

AI tools write code fast. Sometimes very fast. Fast enough that you push 300 lines without reading every one of them. Linting and formatting are the automated safety net that catches what you miss. This tutorial covers ESLint, Prettier, and Biome: what each one does, how to set them up, and which combination to use.

Why AI Code Needs Stricter Linting

Hand-written code has one author who knows the codebase. AI-generated code has no author in that sense. The model does not know what you broke last week, what patterns your codebase already uses, or whether the function it just wrote duplicates one that already exists. It writes syntactically correct code that compiles cleanly, but correctness and quality are different things.

Linting is automated code review. It scans your source files and flags patterns that are technically valid but problematic in practice: unused variables that clutter the code, implicit type coercions that produce surprising results, missing dependencies in React hooks that cause stale closure bugs. A linter catches these before they reach production.

Formatting is simpler. It enforces consistent whitespace, indentation, and punctuation across every file. It sounds trivial until you have an AI tool that uses double quotes one time and single quotes the next, two-space indentation here and four-space there. Inconsistent formatting creates noisy diffs, makes code review harder, and creates merge conflicts over nothing.

The combination matters because AI tools output a lot of code quickly, and that volume amplifies inconsistency. One human writing 50 lines a day stays pretty consistent. An AI producing 500 lines across 10 files in a single session will not.

ESLint Setup for AI Projects

ESLint is the standard JavaScript and TypeScript linter. It uses a plugin system, which means you install rule sets for whatever stack you use: TypeScript, React, Next.js, accessibility. It is highly configurable and has the largest rule library of any JavaScript linter.

Install it:

npm install --save-dev eslint @eslint/js typescript-eslint

Create eslint.config.js (the flat config format, current as of ESLint 9):

// eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    rules: {
      "@typescript-eslint/no-unused-vars": "error",
      "@typescript-eslint/no-explicit-any": "warn",
      "no-console": "warn",
    },
  }
);

Add a lint script to package.json:

{
  "scripts": {
    "lint": "eslint .",
    "lint:fix": "eslint . --fix"
  }
}

For React and Next.js projects, add the relevant plugins:

npm install --save-dev eslint-plugin-react eslint-plugin-react-hooks

Then extend the config:

import reactPlugin from "eslint-plugin-react";
import hooksPlugin from "eslint-plugin-react-hooks";

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    plugins: {
      react: reactPlugin,
      "react-hooks": hooksPlugin,
    },
    rules: {
      "react-hooks/rules-of-hooks": "error",
      "react-hooks/exhaustive-deps": "warn",
      "@typescript-eslint/no-unused-vars": "error",
    },
  }
);

The react-hooks/exhaustive-deps rule deserves special mention. AI tools frequently generate React hooks with missing dependency arrays. This causes subtle stale closure bugs where a callback references an old version of a variable. That rule catches those before they cause confusing behavior in production.

EXPLAINER DIAGRAM: Two code panels side by side. Left panel labeled WITHOUT LINTING shows a TypeScript function with a red underline under an unused variable, a useEffect hook with an empty dependency array missing a referenced variable, and a variable typed as any with no type annotation. Right panel labeled WITH ESLINT shows the same three issues flagged with error icons and brief inline messages: unused-vars error, exhaustive-deps warning, and no-explicit-any warning. Below both panels a bar labeled BUGS CAUGHT IN DEV vs BUGS CAUGHT IN PROD shows the linted version catching issues earlier.
ESLint flags unused variables, missing hook dependencies, and implicit any types before they become runtime bugs.

Prettier for Consistent Formatting

Prettier is an opinionated code formatter. It does not lint for bugs. It reformats your code to a consistent style and makes zero compromises about it. You do not configure the rules, you configure a few preferences (tab width, quote style, semicolons) and Prettier handles everything else.

Install it:

npm install --save-dev prettier

Create .prettierrc:

{
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 80
}

Add format scripts:

{
  "scripts": {
    "format": "prettier --write .",
    "format:check": "prettier --check ."
  }
}

The key workflow is running prettier --write . before committing. That reformats every file so your diff only contains real changes, not whitespace noise.

Prettier and ESLint serve different purposes. ESLint finds bugs. Prettier enforces style. Use both together. The one issue is that some ESLint rules conflict with Prettier's formatting. Resolve this by installing eslint-config-prettier, which disables any ESLint rules that would conflict:

npm install --save-dev eslint-config-prettier

Then add it to your ESLint config as the last item (so it overrides conflicting rules):

import prettier from "eslint-config-prettier";

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,
  prettier // must be last
);
Key Takeaway

ESLint catches bugs and code quality issues. Prettier enforces consistent formatting. They solve different problems and work well together. Install eslint-config-prettier to prevent them from conflicting. Run Prettier on every save or commit, and ESLint in your CI pipeline to block bad code from shipping.

Biome as the All-in-One Alternative

Biome is a Rust-based tool that does both linting and formatting in a single binary with no plugin system required. It is dramatically faster than ESLint plus Prettier, simpler to configure, and increasingly compatible with the same rule sets.

Install it:

npm install --save-dev --save-exact @biomejs/biome

Initialize the config:

npx @biomejs/biome init

This creates biome.json:

{
  "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true
    }
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 80
  }
}

Add scripts:

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

biome check runs both the linter and formatter in one pass. biome check --write fixes everything it can automatically. It handles imports, formatting, and many lint rules in a single command.

The speed difference is real. On a medium-sized Next.js project with ~200 files, ESLint plus Prettier takes 8 to 15 seconds. Biome takes under a second. That is the difference between a check you run on every save versus one you skip because it slows you down.

EXPLAINER DIAGRAM: A horizontal speed comparison chart with three rows. First row labeled ESLINT + PRETTIER shows a progress bar that takes 3 sections out of 10 to complete, labeled 8 to 15 seconds. Second row labeled BIOME shows a progress bar that fills only 0.5 sections out of 10, labeled under 1 second. Third row labeled PROJECT SIZE shows a scale from 50 FILES on the left to 500 FILES on the right with a marker at 200 FILES. Below the bars, two columns compare the tools: ESLint plus Prettier shows PLUGIN ECOSYSTEM and HIGHLY CONFIGURABLE as advantages; Biome shows SINGLE BINARY and ZERO CONFLICTS as advantages.
Biome runs linting and formatting 10 to 20 times faster than ESLint plus Prettier on most projects.

Which Combo to Choose

Here is a direct comparison:

FactorESLint + PrettierBiome
Speed8 to 15s on medium projectsUnder 1s
ConfigurationModerate (two tools, plugin installs)Simple (one config file)
Rule coverageExtensive (huge plugin ecosystem)Good (growing, covers most common rules)
TypeScript supportExcellent via typescript-eslintGood, improving fast
React hooks ruleseslint-plugin-react-hooksBuilt-in
Accessibility ruleseslint-plugin-jsx-a11yPartial
Conflict potentialPossible (needs eslint-config-prettier)None (same tool)
Migration effortNone if you already use itLow for new projects
Community maturityEstablished, widely documentedNewer, less documentation

The practical decision comes down to two scenarios.

Use ESLint plus Prettier if you are working on an existing project that already has them configured, if you need specific plugins like eslint-plugin-security or eslint-plugin-jest, or if you rely on custom rules your team has written. The ecosystem is more mature and there is more documentation available when you hit an edge case.

Use Biome if you are starting a new project, if build speed matters (monorepos feel this most), or if you want zero configuration overhead. It is the better default for greenfield vibe coding projects where you want fast feedback and simple setup.

One middle path worth knowing: Biome can replace Prettier even if you keep ESLint for linting. You get faster formatting without losing the ESLint rule set you depend on. This hybrid approach is increasingly common for larger codebases.

Common Mistake

Configuring ESLint and Prettier separately without installing eslint-config-prettier. Without it, the two tools will fight over formatting rules. ESLint will flag things Prettier just reformatted, and vice versa. The fix is one install and one line in your ESLint config. Do it at setup time or you will spend an afternoon debugging why your linter is angry about code that looks perfectly formatted.

What This Means For You

Pick one setup and add it to your project today. For a new project, Biome is the fastest path: one install, one config file, one command that does everything. For an existing project already using ESLint, add Prettier and eslint-config-prettier, then add your lint script to your CI pipeline so it runs on every push.

The specific tool matters less than having the habit. Run linting before you push. Add it to your CI pipeline so it blocks deployments when AI-generated code slips in something problematic. The five-minute setup pays for itself the first time it catches a missing hook dependency or a variable that was used before it was defined.

AI tools are not going to lint their own output. That job is yours, and these tools make it automatic.

Ready to Ship Cleaner Code?

See how linting fits into a complete CI/CD workflow for vibe coders.

Read the CI/CD guide

One last thing. Whatever you choose, configure your editor to run it on save. VS Code has Biome and ESLint extensions. Cursor respects them. The fastest feedback loop is the one that does not require you to switch to a terminal. Catch the problem in the same moment you introduce it, and it costs you nothing to fix.

Building With AI Every Day?

Get practical tutorials on tooling, deployment, and shipping faster.

Explore the blog
PJ
Pranay Joshi

20+ years building products at scale. VP of Product & Engineering, startup founder, and AI coach. Helping dreamers turn ideas into reality with vibe coding.

The Tuesday Shipping Report

Every Tuesday, one focused email:

  • - The tool or technique that's actually working right now
  • - A real problem from the community (and how to solve it)
  • - What changed this week in the vibe coding landscape

Read by 1,000+ founders, developers, and creators building with AI. Free forever. No spam.