RoboDodd

Don't Mix Prettier and ESLint: A Cleaner Angular 22 Setup

My old guide ran Prettier through ESLint. Here's why that mix is a mistake and how to set up Angular so Prettier formats on save and ESLint only lints.

Neon isometric scene with the Angular shield and Prettier logos on two separate glowing platforms, representing Prettier and ESLint as separate tools
Angular 5 min read

About a year ago I wrote a guide on wiring up Prettier and ESLint in an Angular project. It worked, plenty of people used it, and for a long time it was how I set up every new app. But I’ve since changed my mind about one important detail: you shouldn’t run Prettier through ESLint. Mixing the two the way I originally suggested creates noise, slows your linting down, and blurs the line between two tools that are happiest doing separate jobs.

This post is the update. If you followed the old guide, here’s why I’d do it differently now — and the cleaner setup I use today on Angular 20+ (this example is from a real Angular 22 workspace).

The original guide: Prettier and ESLint 9 in Your Angular 19+ Project
My earlier walkthrough that ran Prettier as an ESLint rule. Still useful for context — this post supersedes its approach.

What my old setup actually did

In the original guide I installed eslint-plugin-prettier and prettier-eslint, then added this to the ESLint config:

const eslintPluginPrettierRecommended = require('eslint-plugin-prettier/recommended');

module.exports = [
  // ...
  eslintPluginPrettierRecommended,
];

That line does something specific: it registers Prettier as an ESLint rule (prettier/prettier). Every formatting difference — a missing space, the wrong quote, a line that’s too long — gets reported as an ESLint error. To fix formatting, you’d run source.fixAll.eslint on save, and ESLint would call Prettier under the hood.

It works. But it’s the wrong tool doing the wrong job.

Why running Prettier through ESLint isn’t ideal

Once you live with it for a while, the downsides add up:

  • Formatting noise drowns out real problems. Your editor lights up with red squiggles for whitespace and quote style. Those aren’t bugs — they’re cosmetics Prettier will fix automatically — but they sit in the Problems panel right next to the lint warnings that actually matter, like an unused variable or a missing await.
  • It’s slower. ESLint now has to hand every file to Prettier on every lint pass and diff the result. On a big workspace, ng lint and on-save fixes get noticeably heavier because you’re doing two passes’ worth of work in one.
  • Two tools, one job, fighting over it. ESLint is a linter — it understands your code’s semantics and catches mistakes. Prettier is a formatter — it reprints your code from the AST. Forcing the formatter to run inside the linter couples them together for no real benefit.
  • Even Prettier recommends against it. The Prettier team’s own docs suggest not using eslint-plugin-prettier for most projects, precisely because of the noise and performance cost. The prettier-eslint package I had you install is also unnecessary here.

The fix isn’t to throw ESLint or Prettier away. It’s to let each one stay in its own lane.

The better mental model: separate lanes

Here’s the rule I follow now:

  • Prettier owns formatting. It runs on save through the Prettier VS Code extension. That’s it.
  • ESLint owns code quality. It catches bugs and enforces conventions, and it never touches formatting.
  • The only glue you need is eslint-config-prettier, which simply turns off the ESLint rules that would overlap with (and fight) Prettier.

Notice the difference from before: I install eslint-config-prettier (the config that disables rules) but not eslint-plugin-prettier (the plugin that runs Prettier as a rule). That one swap is the whole point of this post.

Step 1: Install dependencies

This is the exact set I run on Angular 22 (it works the same on Angular 20 and 21):

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

Compared to the old guide, eslint-plugin-prettier and prettier-eslint are gone. You only keep eslint-config-prettier.

Step 2: Configure ESLint (flat config)

Create eslint.config.js in your project root. The important part is that it spreads prettierConfig.rules to switch off stylistic rules, and there is no prettier/prettier rule anywhere:

// @ts-check
const typescript = require('@typescript-eslint/eslint-plugin');
const tsParser = require('@typescript-eslint/parser');
const angular = require('@angular-eslint/eslint-plugin');
const angularTemplate = require('@angular-eslint/eslint-plugin-template');
const templateParser = require('@angular-eslint/template-parser');
const prettierConfig = require('eslint-config-prettier');

module.exports = [
  {
    ignores: ['dist/', 'node_modules/', '.angular/', '.cache/', 'coverage/', '**/*.d.ts'],
  },
  // TypeScript files
  {
    files: ['**/*.ts'],
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        project: ['./tsconfig.json', './projects/*/tsconfig.*.json'],
        createDefaultProgram: true,
      },
    },
    plugins: {
      '@typescript-eslint': typescript,
      '@angular-eslint': angular,
    },
    rules: {
      ...typescript.configs.recommended.rules,
      ...angular.configs.recommended.rules,
      // Turn off ESLint rules that conflict with Prettier
      ...prettierConfig.rules,

      '@angular-eslint/directive-selector': [
        'error',
        { type: 'attribute', prefix: 'app', style: 'camelCase' },
      ],
      '@angular-eslint/component-selector': 'off',
      '@angular-eslint/prefer-standalone': 'warn',
      '@angular-eslint/prefer-on-push-component-change-detection': 'warn',

      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
      '@typescript-eslint/explicit-function-return-type': 'off',
      '@typescript-eslint/no-inferrable-types': 'off',
    },
  },
  // Angular templates
  {
    files: ['**/*.html'],
    languageOptions: {
      parser: templateParser,
    },
    plugins: {
      '@angular-eslint/template': angularTemplate,
    },
    rules: {
      ...angularTemplate.configs.recommended.rules,
      // Turn off ESLint rules that conflict with Prettier
      ...prettierConfig.rules,

      '@angular-eslint/template/prefer-self-closing-tags': 'error',
      '@angular-eslint/template/use-track-by-function': 'error',
    },
  },
];

The contrast with the old config is the prettier/prettier rule is simply not here. ESLint no longer knows or cares what your code looks like — only whether it’s correct.

Step 3: Configure Prettier

.prettierrc is the same as before — Prettier’s job didn’t change, only who calls it:

{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "printWidth": 120,
  "tabWidth": 2,
  "endOfLine": "auto",
  "overrides": [
    { "files": "*.scss", "options": { "singleQuote": false } },
    { "files": "*.html", "options": { "printWidth": 120 } }
  ]
}

Add a .prettierignore so Prettier skips generated and vendored files:

build
coverage
e2e
node_modules
dist
.angular
.cache
**/*.d.ts
package-lock.json

An .editorconfig is a nice companion — it keeps basic whitespace consistent even for editors and tools that don’t run Prettier:

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

[*.ts]
quote_type = single

Step 4: VS Code — format on save

This is where the two lanes meet, cleanly. Install the two extensions (ESLint and Prettier - Code formatter), then drop this in .vscode/settings.json:

{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "prettier.requireConfig": true,
  "eslint.useFlatConfig": true,
  "eslint.validate": ["javascript", "typescript", "html"],
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }
  },
  "[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }
  },
  "[scss]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.formatOnSave": true
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode",
    "editor.formatOnSave": true
  }
}

Read what this does, because it’s the heart of the new approach:

  • editor.formatOnSave + the Prettier formatter handle all formatting. Save the file, Prettier reprints it. No ESLint involved.
  • source.fixAll.eslint runs on save too, but now it only fixes real lint issues — unused imports, Angular rule violations — because formatting rules were disabled by eslint-config-prettier.
  • prettier.requireConfig keeps Prettier from formatting projects that don’t actually have a .prettierrc, so it won’t reformat unrelated files.

Compare that to the old guide, where I had editor.formatOnSave set to false for HTML and leaned on source.fixAll.eslint to do the formatting. Now formatting and linting each have exactly one owner.

Commit a .vscode/extensions.json so teammates get prompted to install the right tools:

{
  "recommendations": [
    "angular.ng-template",
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode"
  ]
}

Step 5: Scripts for CI and the command line

Format-on-save covers the day-to-day, but you still want commands for CI and for anyone whose editor isn’t set up. Add these to package.json:

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

format:check is the one to wire into CI — it fails the build if anything isn’t formatted, without modifying files. lint stays purely about code quality.

Was the old way wrong?

Not wrong, exactly — it produced formatted, linted code, and if you’re happily running it there’s no emergency. But running Prettier through ESLint trades clarity and speed for a single unified “fix everything” command, and that trade isn’t worth it. Keeping the tools separate means:

  • ESLint’s Problems panel only shows things you should actually think about.
  • Linting is faster because Prettier isn’t bolted onto every pass.
  • Each tool can be upgraded, configured, or swapped without touching the other.

Conclusion

If there’s one thing to take from this update, it’s the mental model: Prettier formats, ESLint lints, and eslint-config-prettier keeps them from stepping on each other. Drop eslint-plugin-prettier, turn on format-on-save, and let each tool do the one thing it’s good at. Your editor gets quieter, your lint runs get faster, and your code stays just as clean. Happy coding!