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

I have always been meticulous about enforcing strict project conventions and maintaining clean, readable code. In this post, I’ll share additional configuration choices I commonly apply in my own workspaces.

Prettier and ESLint are two tools I rely on heavily to keep code quality high and formatting consistent across Angular workspaces. They help maintain a tidy codebase, uphold coding standards, and make collaboration smoother for the whole team. Below, I’ll walk through configuring both tools in an Angular project, so your code stays both functional and well-formatted.

Before diving in, it’s worth clarifying what each tool handles:

  • Prettier handles code formatting, ensuring every developer produces output with the same style.
  • ESLint performs static analysis, enforcing code quality rules and best practices.

The baseline: Prettier and ESLint using standard settings (Manfred’s approach)

To get Prettier and ESLint running with their default configurations, take these steps:

Install Dependencies

pnpm i -D prettier eslint

Note: Pick whichever package manager you prefer. I’d personally go with pnpm!

To make the two tools work together seamlessly, add these extra packages:

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

Then, bring in the Angular ESLint plugins, which include TypeScript support:

ng add angular-eslint

After installation, your package.json should include these packages (alongside your own dependencies):

{
  "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"
  }
}

Create and Verify Configuration Files

Prettier configuration

Add a .prettierrc.json file at the root of your project, containing the following:

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

Note: Be sure to strip comments from any JSON files to prevent parsing errors.

  • singleQuote: Forces the use of single quotes in JavaScript and TypeScript files.

Most developers prefer single quotes because they’re shorter and don’t require pressing the Shift key. A general guideline: use single quotes for JavaScript/TypeScript, and double quotes for HTML and (S)CSS.

ESLint configuration

Running ng add angular-eslint will create an eslint.config.js file for you. We’re adopting the flat config format rather than .eslintrc.json, as it provides better modularity and extensibility.

Here’s what the generated configuration 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: {},
  },
);

Prettier Configuration Insights

Consider applying these two extra settings to refine your formatting further:

{
  "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 parse errors.

  • bracketSameLine: Eliminates additional blank lines when using multiline HTML opening tags, making your Angular templates much neater.
  • printWidth: Raises the maximum line width to 120 characters (instead of 80), which is easier on the eyes for widescreen displays.

You can grab this Prettier configuration here.

Prettier Ignore File

Place a .prettierignore file in the project root to tell Prettier which files or directories to skip:

/.angular
/.nx
/coverage
/dist
node_modules

Note: Review your .gitignore file to see if there are additional items worth excluding.

Caution: Formatting Existing Codebases ⚠️

Exercise care when introducing Prettier or modifying its settings in an established project: it can reformat many files at once. To keep things tidy, commit only the Prettier-related changes as a separate commit. That allows you to evaluate formatting revisions independently without mixing them with functional updates. 🙂

ESLint Configuration Insights

Next, let’s tighten the ESLint setup and broaden its coverage. This approach helps catch problems early and enforces good practices throughout your codebase. Admittedly, some of these rules are subjective—but they’re valuable for maintaining a sizable enterprise codebase. Tweak or drop rules to suit your team’s needs.

// 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: These are my current thoughts, always open to refinement. Feel free to reach out on LinkedIn, bluesky or X.

Download this ESLint configuration.

Integrating Into Your Workflow

To guarantee your code always meets formatting and linting standards, bake Prettier and ESLint into your daily routine. I set Prettier to run automatically on save. This works nicely in Cursor, VS Code, and WebStorm (and even NeoVIM, I believe). Often, I’ll write a line in one go and hit Cmd + S (yes, I’m a Mac user, but I have a Pixel – best combo 😜) to clean it up.

Right before committing, I run ESLint to catch any problems. You can do this manually (my preference) or automate it with a pre-commit hook. This is how it slots into my manual pre-commit routine:

  • run ng lint to spot linting errors
  • run ng b to confirm the project compiles
  • run ng e2e to verify end-to-end tests pass

If you’re using Nx, swap ng for nx in the commands above.

Workshops

If you’d like to explore Angular in depth, we provide a range of workshops in both English and German.

Wrapping Up

Integrating Prettier and ESLint into your Angular workspace is a simple step with substantial payoff for code quality and maintainability. Following the guidance above will keep your code functional, properly styled, and aligned with industry standards.

This article was authored by Alexander Thalhammer. Discuss it on LinkedIn, bluesky or X.