How to configure ESLint and Prettier to prevent formatting conflicts in a Turborepo monorepo

Why do ESLint and Prettier constantly fight in a Turborepo monorepo?

Have you ever watched your CI pipeline fail because a single trailing comma triggered an ESLint error, right after Prettier just formatted the exact same file? In a Turborepo monorepo managing dozens of packages, this formatting tug-of-war isn't just a minor annoyance. It actively destroys developer velocity. When your frontend app enforces single quotes but your backend API defaults to double quotes, Git diffs explode with irrelevant whitespace changes. The core issue stems from overlapping responsibilities. ESLint wants to enforce code quality, while Prettier dictates visual style. When both try to control formatting simultaneously, they clash. Let's dismantle this conflict and build a bulletproof, unified pipeline for your entire workspace.

How do you set up a shared Prettier configuration across all workspaces?

The golden rule of monorepo formatting is absolute consistency. You cannot rely on individual developers remembering to sync their local editor settings. Instead, centralize your Prettier configuration at the root of your Turborepo.

Create a .prettierrc file in your root directory. Keep it strict and simple.

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

Next, ensure every workspace inherits this setup. You don't need to copy this file into every package. Prettier automatically traverses up the directory tree to find the nearest configuration file. However, to prevent rogue configurations from sneaking into specific apps, add a .prettierignore at the root. Ignore build directories, node_modules, and Turborepo's .turbo cache folder.

Consider the math behind this centralization. A mid-sized engineering team of 12 developers spends an average of 15 minutes per week resolving formatting disputes in pull requests. Over a year, that equates to 156 hours of wasted engineering time. Centralizing the config eliminates this friction entirely, dropping formatting-related PR comments to near zero.

What is the best way to integrate ESLint with Prettier without plugin conflicts?

The Death of eslint-plugin-prettier

Historically, developers used eslint-plugin-prettier to run Prettier as an ESLint rule. This approach is now considered an anti-pattern. It slows down linting significantly and clutters your ESLint output with formatting warnings. The modern, high-performance solution is to use eslint-config-prettier. This package simply turns off all ESLint rules that are unnecessary or might conflict with Prettier.

In a Turborepo setup, you should create a shared internal package for your ESLint rules, often named @repo/eslint-config. Inside this package, create a base configuration file.

// @repo/eslint-config/base.js
module.exports = {
  extends: [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ],
  plugins: ["@typescript-eslint"],
  parser: "@typescript-eslint/parser",
  rules: {
    "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
  }
};

Notice how "prettier" sits at the very end of the extends array. This guarantees it overrides any formatting rules introduced by TypeScript or React presets. Your individual apps and packages will then extend this shared base config, ensuring uniform code quality checks without stepping on Prettier's toes.

How can you configure Turborepo to cache linting and formatting tasks efficiently?

Turborepo's superpower is remote and local caching. If you don't configure your turbo.json correctly, you will re-lint thousands of files on every single commit, even if you only changed a single README file.

Defining Cache Inputs and Outputs

To optimize this, define your lint and format tasks in the root turbo.json.

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "lint": {
      "inputs": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", ".eslintrc*"],
      "outputs": []
    },
    "format": {
      "inputs": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.css", "**/*.json"],
      "outputs": [],
      "cache": false
    }
  }
}

Let's break down this configuration. The lint task is fully cacheable. By explicitly defining the inputs, Turborepo knows exactly which file changes should invalidate the cache. If you modify a .tsx file, Turborepo re-runs the linter. If you only update an image asset, it returns a cache hit in milliseconds.

Conversely, the format task is often run with a --write flag, which mutates files. Mutating tasks shouldn't be cached in the same way, hence "cache": false. When running turbo run lint format in your CI pipeline, this precise configuration can reduce pipeline execution time by up to 70% on large codebases.

Why is your VS Code "Format on Save" breaking the monorepo, and how do you fix it?

Even with perfect CLI configurations, the battle is lost if developers use their local editor settings to format code. One developer's VS Code might format with tabs, while another uses spaces. You must enforce editor settings at the repository level.

Create a .vscode folder at the root of your Turborepo and add a settings.json file.

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "eslint.workingDirectories": [
    {"pattern": "apps/*"},
    {"pattern": "packages/*"}
  ]
}

Handling Workspace Directories

The eslint.workingDirectories array is the secret weapon for monorepos. By default, the VS Code ESLint extension only looks for a configuration file at the root of the opened workspace. In a Turborepo, your ESLint configs live inside the individual packages. Explicitly telling the extension where to look ensures that the correct rules are applied to each specific app or library.

Pair this with an extensions.json file that recommends the Prettier and ESLint extensions. When a new developer clones the repo and opens it in VS Code, they are prompted to install the exact tools required to maintain the codebase standards.

How do you enforce these rules in your CI/CD pipeline?

Local setups only go so far. Human error happens. A developer might bypass the pre-commit hook or ignore the VS Code recommendations. Your CI pipeline is the final gatekeeper.

Add a dedicated validation step to your GitHub Actions or GitLab CI workflow.

- name: Lint and Format Check
  run: |
    pnpm turbo run lint
    pnpm format:check

Notice the format:check script. This should map to prettier --check . in your root package.json. Unlike prettier --write, the --check flag does not modify files. It simply exits with a non-zero status code if any file violates the shared Prettier configuration. If a developer pushes poorly formatted code, the pipeline fails immediately, forcing them to run the formatter locally before merging.

Combining Turborepo's caching with strict CI checks creates an impenetrable wall against formatting inconsistencies. Your team stops arguing about syntax and starts focusing on architecture.

Frequently Asked Questions

How do I set up ESLint and Prettier in a Turborepo monorepo?

Start by installing ESLint, Prettier, and the necessary plugins at the root, then create shared config files (e.g., .eslintrc.js and .prettierrc) that individual packages can extend. Add lint and format scripts to each package and reference them in turbo.json using the pipeline.

How do I prevent ESLint and Prettier from conflicting in a monorepo?

Use eslint-config-prettier to disable all ESLint rules that are unnecessary or might conflict with Prettier. This turns off formatting-related rules so Prettier handles code style while ESLint focuses on code quality.

Should I use eslint-config-prettier or eslint-plugin-prettier in Turborepo?

It's generally recommended to use eslint-config-prettier to disable conflicting ESLint rules, while running Prettier as a separate tool. Avoid eslint-plugin-prettier in a monorepo because running Prettier inside ESLint can slow down linting and duplicate formatting responsibilities.

How do I share ESLint and Prettier configs across packages in Turborepo?

Create a root-level config package, such as @repo/eslint-config and @repo/prettier-config, and export the configs from their package.json files. Individual packages can then extend these shared configs, ensuring consistency across all apps and packages in the monorepo.

How do I run ESLint and Prettier as Turborepo tasks?

Define "lint" and "format" scripts in each package's package.json, then add these tasks to the pipeline in turbo.json with options like "dependsOn" and "outputs". Running `turbo run lint` and `turbo run format` will execute the tasks across all workspaces in parallel.

What is the correct order to run ESLint and Prettier in a Turborepo pipeline?

There is no strict order, but it's common to run Prettier first to format code, then ESLint to catch linting issues. In turbo.json, you can define separate tasks without dependencies, or create a combined "check" task that depends on both lint and format --check.

How do I fix ESLint errors that are actually formatting issues from Prettier?

Install eslint-config-prettier and add it to the extends array in your ESLint config so all formatting rules are disabled. If you still see formatting-related errors, make sure Prettier is being run on the same files and that both tools are using the same parser and settings.

Can I use Prettier with ESLint in a Turborepo without conflicts?

Yes, by decoupling their responsibilities and using eslint-config-prettier to turn off all conflicting rules. This way, ESLint handles code-quality issues like unused variables, while Prettier handles code style in every package of the monorepo.

Why does my Turborepo ESLint config get overridden by Prettier rules?

This usually happens when prettier rules are enabled inside ESLint or when the extends order is incorrect. Always place eslint-config-prettier last in the extends array, and avoid using eslint-plugin-prettier unless you have a specific need for it.

How do I configure ESLint and Prettier for a specific package in Turborepo?

In each package, create or extend a local ESLint config that references the shared root config, and add a .prettierrc file or a prettier key in package.json. The package's scripts should call ESLint and Prettier with the correct file paths, while turbo.json coordinates the tasks across the monorepo.