NG Best Practices: Prettier & ESLint

  1. The Perfect Project Setup for Angular: Structure and Automation for More Quality
  2. NG Best Practices: Prettier & ESLint

This is a reaction to Manfred’s Perfect Project Setup post from last week

I enjoyed reading Manfred's setup, and I – like Manfred – think a good setup is essential for a long-lasting, maintainable, and therefore successful project.

I consider myself a strict nerd when it comes to project rules, and I have a great need for clean, readable code. Hence, in this article, I want to discuss some additional configurations that I prefer to use in my projects.

Prettier and ESLint are among my favorite tools for ensuring code quality and consistency in Angular workspaces. They help maintain a clean codebase, enforce coding standards, and improve collaboration among team members. In this post, I will guide you through setting up Prettier and ESLint in your Angular project, ensuring that your code is not only functional but also beautifully formatted.

First things first, let’s distinguish between the roles of these two tools:

  • Prettier is a code formatter that ensures consistent code formatting across all developers.
  • ESLint is a code linter that enforces coding standards and best practices.

Base case: Prettier and ESLint with defaults (Manfred's setup)

To set up Prettier and ESLint with default settings, follow these steps:

Install Packages

pnpm i -D prettier eslint

Note: Use the package manager of your choice. I recommend using pnpm!

For the two tools to work together smoothly, install these additional packages:

pnpm i -D eslint-config-prettier eslint-plugin-prettier

Finally, add the Angular ESLint plugins (including TypeScript support):

ng add angular-eslint

You should now have the following packages in your package.json (besides your own):

{
  "devDependencies": {
    "angular-eslint": "^19.6.0",
    "eslint": "^9.27.0",
    "eslint-config-prettier": "^10.1.5",
    "eslint-plugin-prettier": "^5.4.1",
    "prettier": "^3.5.3",
    "typescript-eslint": "^8.33.0"
  }
}

Add / Check Config Files

Prettier Configuration

Create a .prettierrc.json file in your project root with the following content:

{
  "singleQuote": true // add this line to enforce single quotes
}

Note: Remove any comments from JSON files to avoid parsing errors.

  • singleQuote: Enforces single quotes in JavaScript and TypeScript files.

Most developers prefer single quotes since they are slightly more concise and don’t require holding the Shift key. As a rule of thumb: use single quotes in JavaScript/TypeScript and double quotes in HTML and (S)CSS.

ESLint Configuration

When you run ng add angular-eslint, it will generate an eslint.config.js file for you. We’re using the flat config format instead of .eslintrc.json because it offers better modularity and extensibility.

Here’s an example of what it might look like:

// eslint.config.js
// @ts-check
const eslint = require('@eslint/js');
const tseslint = require('typescript-eslint');
const angular = require('angular-eslint');
const eslintConfigPrettier = require('eslint-config-prettier'); // add the glue plugin

module.exports = tseslint.config(
  {
    ignores: ['.angular/**', '.nx/**', 'coverage/**', 'dist/**'], // add these ignores
    files: ['**/*.ts'],
    extends: [
      eslint.configs.recommended,
      ...tseslint.configs.recommended,
      ...tseslint.configs.stylistic,
      ...angular.configs.tsRecommended,
      eslintConfigPrettier, // add the glue plugin
    ],
    processor: angular.processInlineTemplates,
    rules: {
      '@angular-eslint/directive-selector': [
        'error',
        {
          type: 'attribute',
          prefix: 'app',
          style: 'camelCase',
        },
      ],
      '@angular-eslint/component-selector': [
        'error',
        {
          type: 'element',
          prefix: 'app',
          style: 'kebab-case',
        },
      ],
    },
  },
  {
    files: ['**/*.html'],
    extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],
    rules: {},
  },
);

Best Practice: My Recommended Setup

Prettier Config Best Practices

Try adding these two options to make your formatting even prettier:

{
  "bracketSameLine": true, // avoid useless/empty final line for multiline html opening tags
  "printWidth": 120, // raise line length to 120 characters (80 is too short for big screens)
  "singleQuote": true
}

Note: Remove any comments from JSON files to avoid parsing errors.

  • bracketSameLine: Avoids an extra blank line when you have multiline HTML opening tags, which makes your Angular templates cleaner.
  • printWidth: Sets the maximum line length to 120 characters (instead of the default 80), which is more comfortable on wide monitors.

Download this Prettier config here.

Prettier Ignore File

Create a .prettierignore file at the project root to exclude files and directories from Prettier:

/.angular
/.nx
/coverage
/dist
node_modules

Note: It's probably a good idea to check you .gitignore file for more things to ignore.

Caution: Prettifying Existing Projects ⚠️

Be careful when adding Prettier or changing its config in an existing codebase: it can reformat a lot of files at once. To avoid confusion, make sure you commit only the Prettier-related changes in a separate commit. That way, you can review formatting changes in isolation and not mix them with other code changes. 🙂

ESLint Config Best Practices

Now let’s make the ESLint configuration stricter and more comprehensive. This helps catch potential issues early and ensures best practices across your codebase. Some of these rules are, to be honest, opinionated—but they’re worth it for keeping an enterprise-scale codebase clean and maintainable. Feel free to adjust or remove rules as needed for your team.

// eslint.config.js
// @ts-check
const eslint = require('@eslint/js');
const tseslint = require('typescript-eslint');
const angular = require('angular-eslint');
const eslintConfigPrettier = require('eslint-config-prettier');

module.exports = tseslint.config(
  {
    ignores: ['.angular/**', '.nx/**', 'coverage/**', 'dist/**'],
    files: ['**/*.ts'],
    extends: [
      eslint.configs.recommended,
      ...tseslint.configs.recommended,
      ...tseslint.configs.stylistic,
      ...angular.configs.tsRecommended,
      eslintConfigPrettier,
    ],
    processor: angular.processInlineTemplates,
    rules: {
      '@angular-eslint/directive-selector': [
        'error',
        {
          type: 'attribute',
          prefix: 'app',
          style: 'camelCase',
        },
      ],
      '@angular-eslint/component-selector': [
        'error',
        {
          type: ['attribute', 'element'],
          prefix: 'app',
          style: 'kebab-case',
        },
      ],

      // Angular best practices
      '@angular-eslint/no-empty-lifecycle-method': 'warn',
      '@angular-eslint/prefer-on-push-component-change-detection': 'warn',
      '@angular-eslint/prefer-output-readonly': 'warn',
      '@angular-eslint/prefer-signals': 'warn',
      '@angular-eslint/prefer-standalone': 'warn',

      // TypeScript best practices
      '@typescript-eslint/array-type': ['warn'],
      '@typescript-eslint/consistent-indexed-object-style': 'off',
      '@typescript-eslint/consistent-type-assertions': 'warn',
      '@typescript-eslint/consistent-type-definitions': ['warn', 'type'],
      '@typescript-eslint/explicit-function-return-type': 'error',
      '@typescript-eslint/explicit-member-accessibility': [
        'error',
        {
          accessibility: 'no-public',
        },
      ],
      '@typescript-eslint/naming-convention': [
        'warn',
        {
          selector: 'variable',
          format: ['camelCase', 'UPPER_CASE', 'PascalCase'],
        },
      ],
      '@typescript-eslint/no-empty-function': 'warn',
      '@typescript-eslint/no-empty-interface': 'error',
      '@typescript-eslint/no-explicit-any': 'warn',
      '@typescript-eslint/no-inferrable-types': 'warn',
      '@typescript-eslint/no-shadow': 'warn',
      '@typescript-eslint/no-unused-vars': 'warn',

      // JavaScript best practices
      eqeqeq: 'error',
      complexity: ['error', 20],
      curly: 'error',
      'guard-for-in': 'error',
      'max-classes-per-file': ['error', 1],
      'max-len': [
        'warn',
        {
          code: 120,
          comments: 160,
        },
      ],
      'max-lines': ['error', 400], // my favorite rule to keep files small
      'no-bitwise': 'error',
      'no-console': 'off',
      'no-new-wrappers': 'error',
      'no-useless-concat': 'error',
      'no-var': 'error',
      'no-restricted-syntax': 'off',
      'no-shadow': 'error',
      'one-var': ['error', 'never'],
      'prefer-arrow-callback': 'error',
      'prefer-const': 'error',
      'sort-imports': [
        'error',
        {
          ignoreCase: true,
          ignoreDeclarationSort: true,
          allowSeparatedGroups: true,
        },
      ],

      // Security
      'no-eval': 'error',
      'no-implied-eval': 'error',
    },
  },
  {
    files: ['**/*.html'],
    extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],
    rules: {
      // Angular template best practices
      '@angular-eslint/template/attributes-order': [
        'error',
        {
          alphabetical: true,
          order: [
            'STRUCTURAL_DIRECTIVE', // deprecated, use @if and @for instead
            'TEMPLATE_REFERENCE', // e.g. <input #inputRef>
            'ATTRIBUTE_BINDING', // e.g. <input required>, id="3"
            'INPUT_BINDING', // e.g. [id]="3", [attr.colspan]="colspan",
            'TWO_WAY_BINDING', // e.g. [(id)]="id",
            'OUTPUT_BINDING', // e.g. (idChange)="handleChange()",
          ],
        },
      ],
      '@angular-eslint/template/button-has-type': 'warn',
      '@angular-eslint/template/cyclomatic-complexity': ['warn', { maxComplexity: 10 }],
      '@angular-eslint/template/eqeqeq': 'error',
      '@angular-eslint/template/prefer-control-flow': 'error',
      '@angular-eslint/template/prefer-ngsrc': 'warn',
      '@angular-eslint/template/prefer-self-closing-tags': 'warn',
      '@angular-eslint/template/use-track-by-function': 'warn',
    },
  },
);

Disclaimer: This is my current opinion and subject to improvements. Feel free to hook me up on LinkedIn, bluesky or X.

Download this ESLint config.

Workflow Integration

To ensure that your code is always formatted and linted correctly, integrate Prettier and ESLint into your development workflow: I run Prettier automatically on every save. That works charmingly in Cursor, VS Code and Webstorm (even NeoVIM, I guess). Sometimes I just write my code in one ugly line and then press Cmd + S (yes Mac user, but I do have a Pixel phone – best combo 😜) to format it.

And then before committing, I run ESLint to check for any issues. This can be done manually (that's what I prefer) or automatically using a pre-commit hook. This is fully integrated in my manual pre-commit workflow:

  • run ng lint to check for linting issues
  • run ng b to check if the project builds
  • run ng e2e to ensure e2e tests pass

If you're using Nx then just replace ng with nx in the commands above.

Workshops

If you want to deep-dive into Angular, we offer a variety of workshops – both in English and German.

Conclusion

Setting up Prettier and ESLint in your Angular project is a straightforward process that can significantly enhance your code quality and maintainability. By following the steps outlined in this post, you can ensure that your code is not only functional but also beautifully formatted and adheres to best practices.

This blog post was written by Alexander Thalhammer. Comment it on LinkedIn, bluesky or X.