BackDev Patterns & Practices
Setup//

Angular 19 Development Setup

Practical Angular 19 setup: CLI and workspace configuration, TypeScript tips, ESLint and Prettier integration, plus commit-hook examples to maintain a robust workflow.

Introduction

This guide provides a practical, up-to-date walkthrough to set up a modern development environment with Angular 19. It is intended for teams migrating from older versions and for developers starting a new project who want to apply best practices from day one.

We cover CLI and project setup using Standalone Components, the Signals-based reactivity model, and essential tooling for code quality (ESLint, Prettier) and workflow integration (Husky, lint-staged). The guide also includes recommendations on folder structure, testing, accessibility, and production optimizations.

By the end of this article you will have a reference project ready for production: configured for a great developer experience, with robust linting and formatting rules, local CI hooks, and scaffolding that supports maintainability and scalability.

TL;DR

Quick summary

  • Create a new Angular 19 project with Standalone Components.
  • Use ESLint (flat config) + Prettier and hook them with Husky + lint-staged.
  • Prefer Signals and the new `input()`/`output()` helpers for modern patterns.
  • Verify with `pnpm exec tsc --noEmit`, `pnpm exec eslint` and `pnpm run format:check`.
  • Angular CLI 19 installation
  • Creating a new Standalone Components project
  • Signals-based architecture
  • ESLint configuration for Angular 19
  • Prettier configuration for Angular templates
  • Husky and lint-staged integration
What is New in Angular 19:
  • Standalone Components by default (no NgModules)
  • Signals-based reactivity
  • Improved performance and bundle size
  • New input() and output() functions
  • Simplified project structure

Prerequisites

Before you begin, make sure you have:

  • Node.js (v18.19+; v20.x LTS recommended) — Angular 19 requires at least Node 18.19
  • npm (included with Node.js) — v9.x or higher
  • A code editor (VSCode recommended)

Verify versions

1node --version  # Should be v18.19+ or v20.x+
2npm --version   # Should be 9.x or higher

Quick verification

This repository uses pnpm. If you prefer npm or yarn, equivalents are shown.

1pnpm install               # (npm) npm install
2pnpm dev                   # (npm) npm run dev | (yarn) yarn dev
3pnpm exec tsc --noEmit     # typecheck
4pnpm exec eslint "app/**/*.{ts,tsx,js,jsx}"  # lint
5pnpm exec prettier --check "src/**/*.{ts,tsx,html,scss,json}"  # format check

Step 1 — Install Angular CLI 19

Install Globally

1npm install -g @angular/cli@latest

Verify Installation

1ng version

Expected output:

1Angular CLI: 19.x.x
2Node: 20.x.x (or 18.19+)
3Package Manager: npm 10.x.x
4OS: win32 x64

Important Version Notes

  • Angular 19 requires Node.js 18.19+ (recommended: 20.x LTS)
    • npm (included with Node.js) — v9.x or higher
  • Uses Standalone Components by default

Step 2 — Create a New Angular 19 Project

Create Project with Standalone Components

1ng new my-angular-app

Configuration Options (Angular 19)

You will be asked several questions:

1. Which stylesheet format would you like to use?

1? Which stylesheet format would you like to use?
2CSS
3❯ SCSS   [ https://sass-lang.com/documentation/syntax#scss                ]
4Sass   [ https://sass-lang.com/documentation/syntax#the-indented-syntax ]
5Less   [ http://lesscss.org                                             ]

Recommendation: Select SCSS for modern styling

2. Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)?

1? Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? (y/N)
  • Yes: For better SEO and initial load performance
  • No: For client-side only applications

Note: Angular 19 NO longer asks about routing - routing is included by default via provideRouter() in app.config.ts.

Navigate to Project

1cd my-angular-app

Test the Installation

1ng serve

Open browser at http://localhost:4200 - you should see the Angular welcome page.

Development server options:
1ng serve --open          # Opens browser automatically
2ng serve --port 4300     # Use different port
3ng serve --ssl           # Enable HTTPS

Angular 19 Project Structure (Standalone)

Key Difference: No more app.module.ts - Angular 19 uses Standalone Components by default!

1my-angular-app/
2├── public/
3│   └── favicon.ico                # Favicon
4├── src/
5│   ├── app/
6│   │   ├── app.component.ts       # Root standalone component
7│   │   ├── app.component.html     # Root template
8│   │   ├── app.component.scss     # Root styles
9│   │   ├── app.component.spec.ts  # Unit tests
10│   │   ├── app.config.ts          # App configuration (replaces app.module.ts)
11│   │   └── app.routes.ts          # Routes configuration
12│   ├── index.html                 # Main HTML
13│   ├── main.ts                    # App entry point (uses bootstrapApplication)
14│   └── styles.scss                # Global styles
15├── angular.json                   # Angular CLI config
16├── package.json                   # Dependencies
17├── package-lock.json              # Locked dependencies
18├── tsconfig.json                  # TypeScript base config
19├── tsconfig.app.json              # App-specific TypeScript config
20├── tsconfig.spec.json             # Test-specific TypeScript config
21└── README.md                      # Project documentation

Note: Angular 19 uses public/ folder for static files (like favicon) instead of the old src/assets/ approach.

Key Files Explained

main.ts - Bootstrap with Standalone:

1bootstrapApplication(AppComponent, appConfig).catch((err) =>
2    console.error(err)
3);

app.config.ts - Application Configuration:

1export const appConfig: ApplicationConfig = {
2    providers: [provideRouter(routes)],
3};

app.component.ts - Standalone Component:

1@Component({
2    selector: "app-root",
3    standalone: true, // ← Standalone component
4    imports: [RouterOutlet], // ← Import dependencies directly
5    templateUrl: "./app.component.html",
6    styleUrl: "./app.component.scss",
7})
8export class AppComponent {
9    title = "my-angular-app";
10}

app.routes.ts - Routes Configuration:

1export const routes: Routes = [];

Configure ESLint for Angular 19

ESLint is a widely used open-source static code analysis tool for identifying and fixing problems in JavaScript, TypeScript, and related languages. It helps developers maintain code quality and consistency by enforcing coding standards, detecting syntax errors, and highlighting potential bugs or anti-patterns before code is executed. ESLint is highly configurable and supports custom rules, plugins, and integrations with modern frameworks like Angular, React, and Vue. In Angular projects, ESLint can also validate HTML templates and enforce Angular-specific best practices, making it an essential tool for scalable, maintainable, and secure codebases.

Why Angular Needs Specific ESLint?

  • Validation of HTML templates (directives, pipes, binding)
  • Rules for Standalone Components
  • Validation of Signals and new reactive primitives
  • Rules for new input() and output() functions
  • Detection of Angular anti-patterns

Difference with Generic ESLint

FeatureGeneric ESLintAngular 19 ESLint
Validates TypeScript
Validates HTML templates
Standalone component rules
Signals validation
Modern input/output functions

Install — Angular ESLint

1ng add @angular-eslint

What does this command do?

  1. Installs angular-eslint package (v20.5.1) - all-in-one package
  2. Installs ESLint 9.x (v9.38.0+) - latest with flat config
  3. Installs typescript-eslint (v8.46.2+)
  4. Creates eslint.config.js with flat config format (ESLint 9+)
  5. Updates angular.json with ESLint builder
  6. Adds lint script to package.json
  7. Includes rules for Standalone Components and Signals

Installed packages:

  • angular-eslint: 20.5.1 (includes builder, plugin, template parser, and schematics)
  • eslint: ^9.38.0
  • typescript-eslint: 8.46.2
  • typescript: ~5.7.2

Note: angular-eslint is now a unified package that includes all necessary Angular ESLint components.

Verify Installation

1ng lint

Should show: "All files pass linting" or list of errors to fix.

Project Structure After ESLint Installation

After running ng add @angular-eslint, your project structure will look like this:

1my-angular-app/
2├── public/
3│   └── favicon.ico
4├── src/
5│   ├── app/
6│   │   ├── app.component.ts
7│   │   ├── app.component.html
8│   │   ├── app.component.scss
9│   │   ├── app.component.spec.ts
10│   │   ├── app.config.ts
11│   │   └── app.routes.ts
12│   ├── index.html
13│   ├── main.ts
14│   └── styles.scss
15├── angular.json                   # ← Updated with ESLint builder
16├── eslint.config.js               # ← NEW: ESLint 9 flat config
17├── package.json                   # ← Updated with ESLint dependencies
18├── package-lock.json              # ← Updated
19├── tsconfig.json
20├── tsconfig.app.json
21├── tsconfig.spec.json
22└── README.md

Files created/modified:

  • eslint.config.js - New ESLint 9 flat configuration file
  • angular.json - Updated with @angular-eslint/builder:lint
  • package.json - New dependencies and scripts added
    • angular-eslint (v20.5.1)
    • eslint (v^9.38.0)
    • typescript-eslint (v8.46.2)
    • lint script added to scripts section

ESLint 9 Flat Config Format (Angular 19)

Important: Angular ESLint 20.x uses ESLint 9 with the new flat config format (eslint.config.js), replacing the old .eslintrc.json format.

Configuration — Generated eslint.config.js

The ng add command creates a flat config file optimized for Angular 19:

1// @ts-check
2
3module.exports = tseslint.config(
4    {
5        files: ["**/*.ts"],
6        extends: [
7            eslint.configs.recommended,
8            ...tseslint.configs.recommended,
9            ...tseslint.configs.stylistic,
10            ...angular.configs.tsRecommended,
11        ],
12        processor: angular.processInlineTemplates,
13        rules: {
14            "@angular-eslint/directive-selector": [
15                "error",
16                {
17                    type: "attribute",
18                    prefix: "app",
19                    style: "camelCase",
20                },
21            ],
22            "@angular-eslint/component-selector": [
23                "error",
24                {
25                    type: "element",
26                    prefix: "app",
27                    style: "kebab-case",
28                },
29            ],
30        },
31    },
32    {
33        files: ["**/*.html"],
34        extends: [
35            ...angular.configs.templateRecommended,
36            ...angular.configs.templateAccessibility,
37        ],
38        rules: {},
39    }
40);

Rules — Key Differences: Flat Config vs Old Format

AspectOld Format (.eslintrc.json)New Format (eslint.config.js)
File name.eslintrc.jsoneslint.config.js
ESLint versionESLint 8.xESLint 9.x
FormatJSON with overridesJavaScript with flat arrays
Config structureNested overridesFlat array of config objects
Extending configsextends arraySpread operator ...configs
TypeScript supportSeparate parser configBuilt-in with typescript-eslint
FlexibilityLimitedFull JavaScript power

Configuration — Flat Config Structure Explained

1. TypeScript Files Configuration:

1{
2    files: ["**/*.ts"],  // ← Glob pattern for TypeScript files
3    extends: [
4        eslint.configs.recommended,              // ← Base ESLint rules
5        ...tseslint.configs.recommended,         // ← TypeScript rules
6        ...tseslint.configs.stylistic,           // ← TypeScript style rules
7        ...angular.configs.tsRecommended,        // ← Angular-specific rules
8    ],
9    processor: angular.processInlineTemplates, // ← Process inline templates
10    rules: {
11        // Custom rules here
12    },
13}

2. HTML Template Files Configuration:

1{
2    files: ["**/*.html"],  // ← Glob pattern for HTML templates
3    extends: [
4        ...angular.configs.templateRecommended,   // ← Template rules
5        ...angular.configs.templateAccessibility, // ← Accessibility rules
6    ],
7    rules: {
8        // Custom template rules here
9    },
10}

New in Angular 19:

  • @angular-eslint/prefer-standalone - Encourages standalone components
  • Better support for Signals and reactive primitives
  • Improved template accessibility rules

Understanding angular.json ESLint Configuration

After running ng add @angular-eslint/schematics, your angular.json is updated with:

1{
2  "projects": {
3      "my-angular-app": {
4          "architect": {
5              "lint": {
6                  "builder": "@angular-eslint/builder:lint",
7                  "options": {
8                      "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
9                  }
10              }
11          }
12      }
13  }
14}

Configuration explained:

  • builder: Uses Angular ESLint builder instead of the old TSLint builder
  • lintFilePatterns: Defines which files to lint (all .ts and .html files in src/)

Running the linter:

1npm run lint
2# or
3ng lint

Configuration — Detailed Flat Config Explanation

What is Included in Each Config

  • eslint.configs.recommended: Core ESLint rules (no-unused-vars, no-undef, etc.)
  • tseslint.configs.recommended: TypeScript-specific rules, type-aware linting
  • tseslint.configs.stylistic: Code style preferences, naming conventions
  • angular.configs.tsRecommended: Angular selector validation, lifecycle method rules, Standalone component preferences
  • angular.configs.templateRecommended: Template syntax validation, binding syntax, structural directive usage
  • angular.configs.templateAccessibility: Accessibility rules (ARIA, alt text, labels)

Selector Rules in Flat Config

Directives:

1"@angular-eslint/directive-selector": [
2    "error",
3    {
4        type: "attribute",    // ← Directives are attributes
5        prefix: "app",        // ← Your app prefix
6        style: "camelCase"    // ← Style: appMyDirective
7    }
8],

Example:

1// Correct
2@Directive({
3    selector: '[appHighlight]',
4    standalone: true
5})
6
7// Incorrect
8@Directive({
9    selector: '[highlight]'  // Missing 'app' prefix
10})

Components:

1"@angular-eslint/component-selector": [
2    "error",
3    {
4        type: "element",      // ← Components are elements
5        prefix: "app",        // ← Your app prefix
6        style: "kebab-case"   // ← Style: app-my-component
7    }
8],

Example (Angular 19 Standalone):

1// Correct - Standalone component
2@Component({
3    selector: "app-user-card",
4    standalone: true,
5    imports: [CommonModule],
6})
7export class UserCardComponent {}
8
9// Incorrect
10@Component({
11    selector: "userCard", // Doesn't use kebab-case or prefix
12})
13export class UserCardComponent {}

Rules — Useful Angular 19 Rules (Flat Config)

Adding Custom Rules

You can add custom rules to your eslint.config.js:

1module.exports = tseslint.config(
2    {
3        files: ["**/*.ts"],
4        extends: [
5            eslint.configs.recommended,
6            ...tseslint.configs.recommended,
7            ...tseslint.configs.stylistic,
8            ...angular.configs.tsRecommended,
9        ],
10        processor: angular.processInlineTemplates,
11        rules: {
12            "@angular-eslint/directive-selector": [
13                "error",
14                {
15                    type: "attribute",
16                    prefix: "app",
17                    style: "camelCase",
18                },
19            ],
20            "@angular-eslint/component-selector": [
21                "error",
22                {
23                    type: "element",
24                    prefix: "app",
25                    style: "kebab-case",
26                },
27            ],
28            "@angular-eslint/no-output-on-prefix": "error",
29            "@angular-eslint/no-output-native": "error",
30            "@angular-eslint/no-input-rename": "error",
31            "@angular-eslint/use-lifecycle-interface": "error",
32            "@angular-eslint/prefer-on-push-component-change-detection": "warn",
33
34            // TypeScript Rules
35            "@typescript-eslint/no-explicit-any": "warn",
36            "@typescript-eslint/explicit-function-return-type": "error",
37            "@typescript-eslint/explicit-module-boundary-types": "error",
38            "@typescript-eslint/explicit-member-accessibility": [
39                "error",
40                {
41                    accessibility: "explicit",
42                    overrides: {
43                        constructors: "no-public",
44                    },
45                },
46            ],
47            "no-console": "warn",
48
49            // Import ordering
50            "sort-imports": [
51                "error",
52                {
53                    ignoreCase: false,
54                    ignoreDeclarationSort: true,
55                    ignoreMemberSort: false,
56                    memberSyntaxSortOrder: ["none", "all", "multiple", "single"],
57                    allowSeparatedGroups: true,
58                },
59            ],
60
61            // Naming Convention Rules
62            "@typescript-eslint/naming-convention": [
63                "error",
64                {
65                    selector: "variable",
66                    format: ["camelCase", "UPPER_CASE"],
67                },
68                {
69                    selector: "property",
70                    format: ["camelCase"],
71                },
72                {
73                    selector: "method",
74                    format: ["camelCase"],
75                },
76                {
77                    selector: "typeLike",
78                    format: ["PascalCase"],
79                },
80            ],
81        },
82    },
83    {
84        files: ["**/*.html"],
85        extends: [
86            ...angular.configs.templateRecommended,
87            ...angular.configs.templateAccessibility,
88        ],
89        rules: {
90            // Template Rules
91            "@angular-eslint/template/no-negated-async": "error",
92            "@angular-eslint/template/eqeqeq": "error",
93            "@angular-eslint/template/banana-in-box": "error",
94            "@angular-eslint/template/accessibility-alt-text": "warn",
95            "@angular-eslint/template/accessibility-label-for": "warn",
96            "@angular-eslint/template/click-events-have-key-events": "warn",
97        },
98    }
99);

Rule Examples Explained

prefer-signals: Recommend using Signals over traditional observables

1// Warning - Old approach
2export class UserComponent {
3    userName = "";
4    @Input() title = "";
5}
6
7// Better - Using Signals (Angular 19)
8export class UserComponent {
9    userName = signal("");
10    title = input<string>(""); // New input() function
11}

no-output-on-prefix: Do not use "on" prefix in Outputs

1// Incorrect - Using decorator
2@Output() onClick = new EventEmitter();
3
4// Correct - Using new output() function (Angular 19)
5userClick = output<User>();  // New output() function

use-lifecycle-interface: Implement lifecycle interfaces

1// Incorrect
2export class MyComponent {
3    ngOnInit() {}
4}
5
6// Correct
7export class MyComponent implements OnInit {
8    ngOnInit() {}
9}

prefer-on-push-component-change-detection: Recommends OnPush

1// Warning
2@Component({
3    selector: 'app-my-component',
4    standalone: true,
5    templateUrl: './my-component.html'
6})
7
8// Better performance with Signals
9@Component({
10    selector: 'app-my-component',
11    standalone: true,
12    templateUrl: './my-component.html',
13    changeDetection: ChangeDetectionStrategy.OnPush  // Works great with Signals
14})

HTML Templates

1{
2    "@angular-eslint/template/no-negated-async": "error",
3    "@angular-eslint/template/eqeqeq": "error",
4    "@angular-eslint/template/banana-in-box": "error",
5    "@angular-eslint/template/accessibility-alt-text": "warn"
6}

Explanation:

no-negated-async: Do not negate async pipes

1<!-- Incorrect -->
2<div *ngIf="!(user$ | async)">No user</div>
3
4<!-- Correct -->
5<div *ngIf="(user$ | async) === null">No user</div>

eqeqeq: Use === in templates

1<!-- Incorrect -->
2<div *ngIf="status == 'active'">Active</div>
3
4<!-- Correct -->
5<div *ngIf="status === 'active'">Active</div>

banana-in-box: Detects common ngModel error

1<!-- Incorrect (inverted parentheses) -->
2<input ([ngModel])="name" />
3
4<!-- Correct -->
5<input [(ngModel)]="name" />

accessibility-alt-text: Requires alt on images

1<!-- Incorrect -->
2<img [src]="userAvatar" />
3
4<!-- Correct -->
5<img [src]="userAvatar" [alt]="userName" />

Step 4 — Configure Prettier for Angular

Prettier is an opinionated code formatter that enforces a consistent style across your codebase. It supports TypeScript, HTML, CSS/SCSS, and JSON files, making it ideal for Angular projects. By integrating Prettier with ESLint, you can ensure that your code is not only free of linting errors but also consistently formatted according to your preferences. In this step, we will set up Prettier in your Angular 19 project and configure it to work seamlessly with the new ESLint flat config.

Why Prettier for Angular?

  • TypeScript files (.ts)
  • HTML templates (.html)
  • SCSS/CSS files (.scss, .css)
  • JSON configuration files

Install Prettier

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

Create .prettierrc Configuration

Create .prettierrc at project root:

1{
2  "semi": true,
3  "singleQuote": true,
4  "tabWidth": 2,
5  "useTabs": false,
6  "trailingComma": "es5",
7  "printWidth": 100,
8  "arrowParens": "always",
9  "endOfLine": "lf",
10  "bracketSpacing": true,
11  "bracketSameLine": false,
12  "htmlWhitespaceSensitivity": "css",
13  "overrides": [
14    {
15      "files": "*.html",
16      "options": {
17        "parser": "angular",
18        "printWidth": 120
19      }
20    },
21    {
22      "files": "*.component.html",
23      "options": {
24        "parser": "angular"
25      }
26    }
27  ]
28}

Prettier Configuration Explained

Angular-Specific Options
  • htmlWhitespaceSensitivity: "css"
    • Respects CSS display property for whitespace
    • Better for Angular templates with inline styles
  • Override for HTML Templates:
    1  {
    2    "files": "*.html",
    3    "options": {
    4        "parser": "angular",
    5        "printWidth": 120
    6  }
    7}
  • Why printWidth: 120 for HTML?
    • Angular templates often have long attribute bindings
    • 120 chars prevents excessive line breaks

Examples

TypeScript files (.ts) - Angular 19 with Signals:
1import { Component, signal, input, output } from "@angular/core";
2
3@Component({
4    selector: "app-user",
5    standalone: true,
6    templateUrl: "./user.component.html",
7})
8export class UserComponent {
9    userName = signal("John Doe");
10    userId = input.required<string>();
11    userSelected = output<string>();
12
13    selectUser() {
14        this.userSelected.emit(this.userId());
15    }
16}
HTML templates (.html) - Angular 19 Syntax:
1<!-- Uses Angular parser with printWidth: 120 -->
2@if (user(); as user) {
3<div
4    class="user-card"
5    [class.active]="user.isActive"
6    (click)="selectUser(user)"
7>
8    <h2>{{ user.name }}</h2>
9    <p>{{ user.email }}</p>
10</div>
11}
12
13<!-- Old syntax still supported -->
14<div class="user-card" *ngIf="user$ | async as user">
15    <h2>{{ user.name }}</h2>
16</div>

Create .prettierignore

1# Dependencies
2node_modules/
3
4# Build outputs
5dist/
6.angular/
7coverage/
8
9# Cache
10.cache/
11.vscode/
12
13# Logs
14*.log

Integration — Integrate Prettier with ESLint Flat Config

With ESLint 9 flat config, Prettier integration is simpler. Install the plugin:

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

Update your eslint.config.js to include Prettier:

1// @ts-check
2
3module.exports = tseslint.config(
4    {
5        files: ["**/*.ts"],
6        extends: [
7            eslint.configs.recommended,
8            ...tseslint.configs.recommended,
9            ...tseslint.configs.stylistic,
10            ...angular.configs.tsRecommended,
11            prettier, // ← Disable conflicting rules with Prettier
12        ],
13        processor: angular.processInlineTemplates,
14        rules: {
15            // Your custom rules
16        },
17    },
18    {
19        files: ["**/*.html"],
20        extends: [
21            ...angular.configs.templateRecommended,
22            ...angular.configs.templateAccessibility,
23            prettier, // ← Disable conflicting rules for templates too
24        ],
25        rules: {},
26    }
27);

Important: Add prettier at the end of the extends array to ensure it disables any conflicting ESLint rules.

VSCode Settings for Angular

Create or update .vscode/settings.json:

1{
2    "editor.defaultFormatter": "esbenp.prettier-vscode",
3    "editor.formatOnSave": true,
4    "editor.codeActionsOnSave": {
5        "source.fixAll.eslint": "explicit"
6    },
7    "[typescript]": {
8        "editor.defaultFormatter": "esbenp.prettier-vscode"
9    },
10    "[html]": {
11        "editor.defaultFormatter": "esbenp.prettier-vscode"
12    },
13    "[scss]": {
14        "editor.defaultFormatter": "esbenp.prettier-vscode"
15    },
16    "[json]": {
17        "editor.defaultFormatter": "esbenp.prettier-vscode"
18    }
19}

Add Scripts to package.json

1{
2    "scripts": {
3        "ng": "ng",
4        "start": "ng serve",
5        "build": "ng build",
6        "test": "ng test",
7        "lint": "ng lint",
8        "lint:fix": "ng lint --fix",
9        "format": "prettier --write \"src/**/*.{ts,html,scss,json}\"",
10        "format:check": "prettier --check \"src/**/*.{ts,html,scss,json}\"
11    }
12}

Test Prettier

Format all files:
1npm run format
Check formatting:
1npm run format:check

Step 5 — Setup Husky and lint-staged

Husky and lint-staged allow you to run ESLint and Prettier on staged files before they are committed. This ensures that only linted and formatted code is committed to your repository, improving code quality and consistency across your Angular project. In this step, we will set up Husky to create Git hooks and lint-staged to run ESLint and Prettier on the files you are trying to commit.

Why Husky and lint-staged for Angular?

  • Run ESLint only on staged files (fast)
  • Format code with Prettier before commit
  • Prevent bad code from entering the repository
  • Ensure consistent code quality across the team

Install Dependencies

1npm install --save-dev husky lint-staged

Initialize Husky

1npx husky init

Configure lint-staged

Add to package.json:

1{
2"lint-staged": {
3    "*.ts": ["eslint --fix --max-warnings=0", "prettier --write"],
4    "*.html": ["prettier --write"],
5    "*.{scss,css}": ["prettier --write"],
6    "*.{json,md}": ["prettier --write"]
7}
8}

Create Pre-Commit Hook

Edit .husky/pre-commit:

1npx lint-staged

Update package.json Scripts

1{
2    "scripts": {
3        "ng": "ng",
4        "start": "ng serve",
5        "build": "ng build",
6        "test": "ng test",
7        "lint": "ng lint",
8        "lint:fix": "ng lint --fix",
9        "format": "prettier --write \"src/**/*.{ts,html,scss,json}\"",
10        "format:check": "prettier --check \"src/**/*.{ts,html,scss,json}\"",
11        "prepare": "husky install"
12    }
13}

Test the Setup

  1. Make a change to a file:
    1echo "console.log('test')" >> src/app/app.component.ts
  2. Stage the file:
    1git add src/app/app.component.ts
  3. Try to commit:
    1git commit -m "test: verify hooks work"
Expected result:
  • ESLint will check the file
  • Prettier will format the file
  • If errors exist, commit will be blocked

Useful Commands

Linting Commands

Check all files:
1ng lint
Check specific project (workspace):
1ng lint project-name
Auto-fix issues:
1ng lint --fix
Check only modified files:
1ng lint --files="src/app/components/**/*.ts"

Formatting Commands

Format all files:
1npm run format
Check formatting (CI/CD):
1npm run format:check
Format specific folder:
1prettier --write "src/app/components/**/*.{ts,html,scss}"

Angular 19: New Features and Syntax

Signals - Modern Reactive State

Old approach (still works):
1export class UserComponent {
2    userName = "John";
3
4    updateName(newName: string) {
5        this.userName = newName; // Manual change detection
6    }
7}
New approach with Signals (Angular 19 recommended):
1import { Component, signal, computed } from "@angular/core";
2
3export class UserComponent {
4  userName = signal("John");
5  userNameUpper = computed(() => this.userName().toUpperCase());
6
7  updateName(newName: string) {
8    this.userName.set(newName); // Automatic reactive updates
9  }
10}

Modern Input/Output Functions

Old approach with decorators:
1export class UserCardComponent {
2    @Input() userId!: string;
3    @Input() userName = "";
4    @Output() userSelected = new EventEmitter<string>();
5}
New approach with functions (Angular 19):
1import { Component, input, output } from "@angular/core";
2
3export class UserCardComponent {
4    userId = input.required<string>(); // Required input
5    userName = input<string>(""); // Optional with default
6    userSelected = output<string>(); // Output event
7
8    selectUser() {
9        this.userSelected.emit(this.userId());
10    }
11}

New Control Flow Syntax

Old syntax (*ngIf, *ngFor):
1        <div *ngIf="user">
2    <h2>{{ user.name }}</h2>
3</div>
4
5<ul className="list-disc pl-6">
6    <li *ngFor="let item of items">{{ item }}</li>
7</ul>
New syntax (Angular 17+, optimized in 19):
1        @if (user) {
2<div>
3    <h2>{{ user.name }}</h2>
4</div>
5} @for (item of items; track item.id) {
6<li>{{ item }}</li>
7} @switch (status) { @case ('active') {
8<span>Active</span>
9} @case ('inactive') {
10<span>Inactive</span>
11} @default {
12<span>Unknown</span>
13} }

Generating Components in Angular 19

Generate standalone component (default):
1ng generate component user-profile
2# or short form
3ng g c user-profile
Generated component (Angular 19):
1import { Component } from "@angular/core";
2
3@Component({
4    selector: "app-user-profile",
5    standalone: true,
6    imports: [CommonModule],
7    templateUrl: "./user-profile.component.html",
8    styleUrl: "./user-profile.component.scss",
9})
10export class UserProfileComponent {}
Generate with inline template:
1ng g c user-card --inline-template --inline-style

Migration from Older Angular Versions

Step 1 — Update to Angular 19

1ng update @angular/core@19 @angular/cli@19

Step 2 — Migrate to Standalone Components

1ng generate @angular/core:standalone
  • Converts components to standalone
  • Updates imports and providers
  • Removes NgModules
  • Updates routing configuration

Step 3 — Adopt Signals (Optional but Recommended)

Manual migration of @Input() to input():

1// Before
2@Input() userName = '';
3
4// After
5userName = input<string>('');

Manual migration of @Output() to output():

1// Before
2@Output() userClick = new EventEmitter<User>();
3
4// After
5userClick = output<User>();

Here is the complete eslint.config.js with all recommended rules:

1// @ts-check
2
3module.exports = tseslint.config({
4    files: ["**/*.ts"],
5    extends: [
6        eslint.configs.recommended,
7        ...tseslint.configs.recommended,
8        ...tseslint.configs.stylistic,
9        ...angular.configs.tsRecommended,
10        prettier,
11    ],
12    processor: angular.processInlineTemplates,
13    rules: {
14        "@angular-eslint/directive-selector": [
15            "error",
16            { type: "attribute", prefix: "app", style: "camelCase" },
17        ],
18        "@angular-eslint/component-selector": [
19            "error",
20            { type: "element", prefix: "app", style: "kebab-case" },
21        ],
22        "@angular-eslint/no-output-on-prefix": "error",
23        "@angular-eslint/no-output-native": "error",
24        "@angular-eslint/no-input-rename": "error",
25        "@angular-eslint/use-lifecycle-interface": "error",
26        "@angular-eslint/prefer-on-push-component-change-detection": "warn",
27        "@typescript-eslint/no-explicit-any": "warn",
28        "@typescript-eslint/explicit-function-return-type": "off",
29        "no-console": "warn",
30    },
31},
32{
33    files: ["**/*.html"],
34    extends: [
35        ...angular.configs.templateRecommended,
36        ...angular.configs.templateAccessibility,
37        prettier,
38    ],
39    rules: {
40        "@angular-eslint/template/no-negated-async": "error",
41        "@angular-eslint/template/eqeqeq": "error",
42        "@angular-eslint/template/banana-in-box": "error",
43        "@angular-eslint/template/accessibility-alt-text": "warn",
44        "@angular-eslint/template/accessibility-label-for": "warn",
45        "@angular-eslint/template/accessibility-elements-content": "warn",
46        "@angular-eslint/template/click-events-have-key-events": "warn",
47    },
48});

package.json (Complete Example)

Dependencies (Angular 19.2.x):
1{
2  "name": "my-angular-app",
3  "version": "0.0.0",
4  "private": true,
5  "dependencies": {
6    "@angular/common": "^19.2.0",
7    "@angular/compiler": "^19.2.0",
8    "@angular/core": "^19.2.0",
9    "@angular/forms": "^19.2.0",
10    "@angular/platform-browser": "^19.2.0",
11    "@angular/platform-browser-dynamic": "^19.2.0",
12    "@angular/router": "^19.2.0",
13    "rxjs": "~7.8.0",
14    "tslib": "^2.3.0",
15    "zone.js": "~0.15.0"
16  }
17}
DevDependencies (after ESLint + Prettier + Husky):
1{
2  "devDependencies": {
3    "@angular-devkit/build-angular": "^19.2.17",
4    "@angular/cli": "^19.2.17",
5    "@angular/compiler-cli": "^19.2.0",
6    "@types/jasmine": "~5.1.0",
7    "angular-eslint": "20.5.1",
8    "eslint": "^9.38.0",
9    "eslint-config-prettier": "^9.1.0",
10    "husky": "^9.0.11",
11    "jasmine-core": "~5.6.0",
12    "karma": "~6.4.0",
13    "karma-chrome-launcher": "~3.2.0",
14    "karma-coverage": "~2.2.0",
15    "karma-jasmine": "~5.1.0",
16    "karma-jasmine-html-reporter": "~2.1.0",
17    "lint-staged": "^15.2.2",
18    "prettier": "^3.2.5",
19    "typescript": "~5.7.2",
20    "typescript-eslint": "8.46.2"
21  }
22}
Scripts:
1{
2  "scripts": {
3    "ng": "ng",
4    "start": "ng serve",
5    "build": "ng build",
6    "watch": "ng build --watch --configuration development",
7    "test": "ng test",
8    "lint": "ng lint",
9    "lint:fix": "ng lint --fix",
10    "format": "prettier --write \"src/**/*.{ts,html,scss,json}\"",
11    "format:check": "prettier --check \"src/**/*.{ts,html,scss,json}\"",
12    "prepare": "husky install"
13  }
14}
Key observations:
  • angular-eslint is a single unified package
  • Angular 19.2.x uses RxJS 7.8 and zone.js 0.15
  • Testing setup includes Jasmine 5.1 and Karma 6.4
  • TypeScript 5.7.2 is the current version for Angular 19
  • watch script included by default for continuous builds

.prettierrc

1{
2    "semi": true,
3    "singleQuote": true,
4    "tabWidth": 2,
5    "useTabs": false,
6    "trailingComma": "es5",
7    "printWidth": 100,
8    "arrowParens": "always",
9    "endOfLine": "lf",
10    "bracketSpacing": true,
11    "bracketSameLine": false,
12    "htmlWhitespaceSensitivity": "css",
13    "overrides": [
14        {
15            "files": "*.html",
16            "options": {
17                "parser": "angular",
18                "printWidth": 120
19            }
20        }
21    ]
22}

lint-staged Configuration (add to package.json)

1{
2    "lint-staged": {
3    "*.ts": ["eslint --fix --max-warnings=0", "prettier --write"],
4    "*.html": ["prettier --write"],
5    "*.{scss,css}": ["prettier --write"],
6    "*.{json,md}": ["prettier --write"]
7    }
8}

.husky/pre-commit

1npx lint-staged

.vscode/settings.json

1{
2  "editor.defaultFormatter": "esbenp.prettier-vscode",
3  "editor.formatOnSave": true,
4  "editor.codeActionsOnSave": {
5    "source.fixAll.eslint": "explicit"
6  },
7  "[typescript]": {
8    "editor.defaultFormatter": "esbenp.prettier-vscode"
9  },
10  "[html]": {
11    "editor.defaultFormatter": "esbenp.prettier-vscode"
12  },
13  "[scss]": {
14    "editor.defaultFormatter": "esbenp.prettier-vscode"
15  }
16}

Resources

Angular 19

New Angular 19 Features

ESLint for Angular

Prettier

Development Tools

Next Steps

Ready For Production Checklist

  • Run typecheck:
    1pnpm exec tsc --noEmit
  • Run linter and fix issues:
    1pnpm exec eslint "app/**/*.{ts,tsx}"
  • Run formatter check:
    1pnpm exec prettier --check "src/**/*.{ts,tsx,html,scss,json}"
  • Build the app:
    1pnpm exec ng build --configuration production
  • Run unit and e2e smoke tests (Jasmine/Karma or Playwright)
  • Verify CI passes and pre-commit hooks (Husky) are installed
  1. Read the common tool guides for deeper understanding:
  2. Learn about Angular 19 best practices:
    • Standalone component architecture
    • Signals-based state management (recommended over NgRx for simple cases)
    • New input() and output() functions
    • Reactive programming with RxJS (still important for async operations)
    • Deferrable views with @defer
    • Server-Side Rendering (SSR) with Angular Universal
  3. Set up additional tooling:
    • Unit testing with Jasmine/Karma or Jest (modern alternative)
    • E2E testing with Playwright (recommended) or Cypress
    • CI/CD pipelines with GitHub Actions or Azure DevOps
  4. Explore related setups:
    • EditorConfig - Editor consistency
    • Backend integration (.NET, Node.js)

About

Dev Patterns & Practices is a space for long-form thinking on design, technology, and craft. Every piece is written with care and the belief that the best ideas deserve room to breathe.