Skip to content

Angular: Best Practices on Developing with AI

A practical guide for using AI coding assistants effectively in Angular projects, covering project setup, context configuration, architecture patterns, and daily workflows. Includes Claude Code-specific snippets as a ready-to-use reference implementation.

Content Table

Purpose

Angular projects benefit greatly from AI-assisted development, but generic LLM output could produce outdated patterns (e.g., NgModules instead of standalone components, *ngIf instead of @if). This guide solves that problem by showing how to configure AI coding assistants with Angular-specific context so they generate idiomatic Angular code from the start.

By the end of this guide you will have:

  • An AI context file tailored to your Angular project (with examples for Claude Code, Copilot, Cursor, and others)
  • Reusable task instructions for common Angular workflows
  • Reference architecture snippets that any AI assistant can follow
  • Practical workflows for daily Angular development

Audience

This guide is for Angular developers who want to integrate AI coding assistants into their daily workflow. You should be comfortable with Angular fundamentals (components, services, routing) and have a working Angular project. Prior experience with AI coding assistants is helpful but not required.

Prerequisites


Setting Up Your Angular Project for AI

Before writing a single prompt, give your AI assistant the context it needs to understand your project. This section covers the three layers of context you should configure.

Layer 1: Project Context File

Every modern AI coding assistant supports a project-level context file that provides persistent instructions. This file tells the assistant about your project structure, conventions, and constraints.

The content is largely the same across tools — what changes is the file name and location. See AI Context Files for Angular Projects for the full mapping of tools to context files and what to include, and snippets/CLAUDE.md for a ready-to-use Claude Code example you can adapt.

AI Labs: Reusable Task Instructions

Most AI assistants support some form of saved instructions or task templates that can be reused across sessions. For Claude Code, these are called skills — saved instructions stored in .claude/skills/<skill-name>/SKILL.md that the whole team shares via version control.

This guide includes ready-to-use Claude Code skills in the snippets/ directory. See Reusable AI Task Instructions below. The patterns in these skills can be adapted for other tools' custom instruction formats.

Layer 3: MCP Servers (Optional)

Angular CLI MCP Server

Angular CLI v19.2+ includes an experimental MCP server that gives AI assistants direct access to CLI capabilities: code generation, package management, documentation search, and more.

To configure it, run ng mcp in your project and follow the instructions for your editor.

The MCP server provides these tools by default:

ToolDescription
ai_tutorInteractive Angular tutor for projects v20+
find_examplesRetrieves curated official code examples
get_best_practicesAccesses Angular best practices guidance
list_projectsEnumerates workspace projects from angular.json
search_documentationQueries angular.dev docs (requires internet)

Experimental tools (build, devserver, test, e2e, modernize) can be enabled with the -E flag:

Use --read-only to prevent modifications, or --local-only to restrict to offline-capable tools.

Nx MCP Server (for Nx monorepos)

If your project uses Nx, you can configure the Nx MCP server to give AI assistants access to workspace information, project graphs, generators, and task execution.

The Nx MCP server provides tools for:

  • Querying workspace architecture and project dependencies
  • Running and inspecting tasks (build, test, lint)
  • Accessing generator schemas and running generators
  • Visualizing the project graph
  • Reading Nx documentation

Playwright MCP Server

The Playwright MCP server gives AI assistants browser interaction capabilities: navigating pages, taking screenshots, and clicking elements. This provides visual context — the assistant can see how your Angular application actually renders and verify UI changes against expectations.

Chrome DevTools MCP Server

The Chrome DevTools MCP server connects AI assistants to Chrome DevTools Protocol, enabling access to console logs, network requests, DOM inspection, and performance data. Useful for debugging runtime issues without manually copying error output into prompts.


Angular Official AI Resources

Angular maintains several AI-specific resources. Reference them rather than recreating the content.

ResourceURLUsage
Best Practices for AIbest-practices.mdSystem instructions for LLM code generation
llms.txtllms.txtIndex file with links to key resources
llms-full.txtllms-full.txtComprehensive Angular documentation for LLMs
AI Development Guideangular.dev/aiOfficial guide on developing with AI
MCP Serverangular.dev/ai/mcpAngular CLI MCP setup instructions
Web Codegen ScorerGitHubEvaluate AI-generated code quality

Angular also provides specific rules files that embed best practices directly into AI assistants.

These files can be downloaded from the Angular AI page and placed in your project. For Claude Code, the equivalent is your CLAUDE.md file. See AI Context Files for Angular Projects for the full mapping of tools to context files.


AI Context Files for Angular Projects

A well-crafted context file is the single most impactful thing you can do for AI-assisted Angular development. It prevents AI assistants from generating outdated patterns and ensures consistency across the project. The content below applies regardless of which tool you use — only the file name and location differ.

AI AssistantContext FileLocation
Claude CodeCLAUDE.mdProject root or .claude/
GitHub Copilotcopilot-instructions.md.github/
Cursor.cursorrulesProject root

What to Include

  1. Project overview: what the app does, one paragraph
  2. Tech stack: Angular version, UI library, state management, backend
  3. Architecture conventions: folder structure, naming rules
  4. Angular-specific rules: modern patterns to follow, legacy patterns to avoid
  5. Commands: build, test, lint, serve commands
  6. Domain context: business terms, API patterns, key entities

Example

A full example context file is provided at snippets/CLAUDE.md. The content can be copied into any tool's context file. Here is the key Angular conventions section:

markdown
## Angular Conventions

- Use standalone components exclusively (no NgModules)
- Use signals for state management (`signal()`, `computed()`, `effect()`)
- Use the new control flow syntax: `@if`, `@for`, `@switch` (not `*ngIf`, `*ngFor`, `[ngSwitch]`)
- Use `input()` and `output()` functions (not `@Input()` / `@Output()` decorators)
- Use `inject()` function for dependency injection (not constructor injection)
- Use `ChangeDetectionStrategy.OnPush` on all components
- Use reactive forms (not template-driven forms)
- Use route-level lazy loading for feature areas
- Use `NgOptimizedImage` for static images
- Prefer `class` and `style` bindings over `ngClass` / `ngStyle`
- Distinguish page (container) components from view (presentational) components
- For NgRx projects: use `createActionGroup()`, `createReducer()`, `createSelector()`, `createEffect()`

The full snippets/CLAUDE.md file includes project structure, commands, domain context, and more. Copy the content into your tool's context file as a starting point and tailor it to your project.


Reusable AI Task Instructions

Reusable task instructions automate repetitive Angular tasks with consistent quality. The concept applies across tools — saved prompts or templates that encode your team's conventions so every AI interaction follows the same standards.

Skills Across Tools

Several AI assistants now support a skills concept — reusable Markdown files with structured instructions that the assistant can invoke for specific tasks.

AI AssistantSkills LocationFile FormatDocs
Claude Code.claude/skills/<name>/SKILL.mdMarkdown with YAML frontmatterClaude Code Skills
Cursor.cursor/skills/<name>/SKILL.mdMarkdown with YAML frontmatterCursor Skills
GitHub Copilot.github/skills/<name>.md or
.claude/skills/<name>/SKILL.md
Markdown with YAML frontmatterCopilot Skills

All three tools use directory-based skills centered on a SKILL.md file with YAML frontmatter and free-form Markdown instructions, so most basic syntax and structure is shared; they use the same open format, making skills directly portable between them.

For a community-maintained collection of Angular-specific skills, see angular-skills.

Task Overview

SkillTriggerPurpose
ng-component/ng-component <name>Generate a component following project conventions
ng-service/ng-service <name>Generate a service with injection and tests
ng-test/ng-testWrite or improve unit tests for Angular code
ng-review/ng-reviewReview Angular code for best practices

ng-component

Generates a standalone Angular component with signals, OnPush change detection, and proper file structure.

Location: .claude/skills/ng-component/SKILL.mdFull file: snippets/claude-skills/ng-component/SKILL.md

Usage:

/ng-component user-profile
/ng-component order-list --feature orders

ng-service

Generates an Angular service with proper injection, error handling, and a companion .spec.ts file.

Location: .claude/skills/ng-service/SKILL.mdFull file: snippets/claude-skills/ng-service/SKILL.md

Usage:

/ng-service auth
/ng-service product-catalog

ng-test

Analyzes existing Angular code and writes or improves unit tests. Handles components, services, pipes, and directives.

Location: .claude/skills/ng-test/SKILL.mdFull file: snippets/claude-skills/ng-test/SKILL.md

Usage:

/ng-test src/app/features/auth/login.component.ts
/ng-test src/app/core/services/api.service.ts

ng-review

Reviews Angular code for adherence to modern best practices and project conventions.

Location: .claude/skills/ng-review/SKILL.mdFull file: snippets/claude-skills/ng-review/SKILL.md

Usage:

/ng-review src/app/features/dashboard/
/ng-review src/app/shared/components/data-table/

Architecture Patterns and Code Snippets

This section provides reference patterns that AI assistants can follow when generating code for your project. Include these in your context file or as supporting files in your task instructions.

Project Structure

A recommended Angular project structure for medium-to-large applications:

src/
  app/
    core/                          # Singleton services, guards, interceptors
      guards/
        auth.guard.ts
      interceptors/
        api.interceptor.ts
      services/
        auth.service.ts
        api.service.ts
    shared/                        # Reusable components, directives, pipes
      components/
        confirm-dialog/
          confirm-dialog.component.ts
          confirm-dialog.component.html
          confirm-dialog.component.scss
          confirm-dialog.component.spec.ts
      directives/
        highlight.directive.ts
      pipes/
        format-date.pipe.ts
    features/                      # Feature areas (lazy-loaded)
      dashboard/
        dashboard.component.ts
        dashboard.component.html
        dashboard.routes.ts
      users/
        user-list/
          user-list.component.ts
          user-list.component.html
        user-detail/
          user-detail.component.ts
        models/
          user.model.ts
        services/
          user.service.ts
        users.routes.ts
    app.component.ts
    app.routes.ts
    app.config.ts
  environments/
    environment.ts
    environment.prod.ts

Key principles:

  • core/ contains singleton services, guards, and interceptors used application-wide
  • shared/ contains reusable, presentational components, directives, and pipes
  • features/ groups code by business domain, each with its own routes file for lazy loading
  • Colocate related files (component + template + styles + tests) in the same folder

Nx Monorepo Project Structure

For teams using Nx, the structure shifts to a library-based architecture:

apps/
  my-app/
    src/app/
      components/
        pages/                         # App-level container components
          landing-page.component.ts
        views/                         # App-level presentational components
          landing-view.component.ts
      config/                          # App configuration and constants
        app-initializer.config.ts
        constants/
      types/                           # App-level types
      app.component.ts
      app.routes.ts
      app.config.ts
libs/ui/
  component-library/                   # Pure UI components (no business logic)
    src/components/
      actions/
        button.component.ts
      display/
        logo.component.ts
  data-access/                         # State management and API layer
    src/lib/
      api/                             # REST API services
        items-api.service.ts
      clients/                         # HTTP, WebSocket, auth token clients
      state/                           # NgRx feature slices
        items/
          items.actions.ts
          items.reducers.ts
          items.selectors.ts
          items.effects.ts
      config/
        data-access-state.config.ts    # provideState() and provideEffects()
  shared-ui/                           # Shared services, guards, directives
    src/
      services/
      guards/
      directives/
      pipes/
  features/                            # Feature libraries (lazy-loaded)
    dashboard/
      src/lib/
        components/
          pages/
          views/
        dashboard.routes.ts

Key principles:

  • component-library/ has zero business logic — pure UI building blocks
  • data-access/ owns all state and API communication
  • features/ are lazy-loaded and depend on data-access and shared-ui, never on each other
  • Module boundaries are enforced via ESLint @nx/enforce-module-boundaries with scope tags
  • Each library exposes a public API via index.ts barrel exports

Page and View Component Pattern

Separating container (page) and presentational (view) components keeps responsibilities clear:

  • Page components (pages/): inject services, manage state, handle navigation, delegate rendering to views
  • View components (views/): receive data via input(), emit events via output(), contain no service injection
typescript
// landing-page.component.ts (page / container)
@Component({
  selector: 'app-landing-page',
  standalone: true,
  imports: [LandingViewComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<app-landing-view (signIn)="onSignIn()" (signUp)="onSignUp()" />`
})
export class LandingPageComponent {
  private readonly nav = inject(AppNavigationService);

  async onSignIn(): Promise<void> {
    await this.nav.navigateToSignIn();
  }

  async onSignUp(): Promise<void> {
    await this.nav.navigateToSignUp();
  }
}
typescript
// landing-view.component.ts (view / presentational)
@Component({
  selector: 'app-landing-view',
  standalone: true,
  imports: [HeroSectionComponent, OverviewSectionComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <app-hero-section (signInClicked)="signIn.emit()" />
    <app-overview-section />
  `
})
export class LandingViewComponent {
  signIn = output();
  signUp = output();
}

Standalone Component Pattern

typescript
// user-profile.component.ts
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { DatePipe } from '@angular/common';

export interface UserProfile {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

@Component({
  selector: 'app-user-profile',
  standalone: true,
  imports: [DatePipe],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="profile-card">
      @if (user(); as u) {
        <h2>&#123;&#123; u.name &#125;&#125;</h2>
        <p>&#123;&#123; u.email &#125;&#125;</p>
        <small>Member since &#123;&#123; u.createdAt | date:'mediumDate' &#125;&#125;</small>
      } @else {
        <p>No user selected</p>
      }
    </div>
  `,
  styles: `
    .profile-card {
      padding: 1rem;
      border: 1px solid var(--border-color);
      border-radius: 8px;
    }
  `
})
export class UserProfileComponent {
  user = input<UserProfile>();
  displayName = computed(() => this.user()?.name?.toUpperCase() ?? 'Unknown');
}

Patterns demonstrated:

  • standalone: true (no NgModule needed)
  • ChangeDetectionStrategy.OnPush
  • input() function instead of @Input() decorator
  • computed() for derived state
  • @if control flow with as alias
  • Inline template and styles (suitable for small components)

Signal-Based Service Pattern

typescript
// user.service.ts
import { computed, inject, Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, EMPTY, tap } from 'rxjs';

export interface User {
  id: string;
  name: string;
  email: string;
}

@Injectable({ providedIn: 'root' })
export class UserService {
  private readonly http = inject(HttpClient);

  // State
  private readonly _users = signal<User[]>([]);
  private readonly _loading = signal(false);
  private readonly _error = signal<string | null>(null);

  // Public selectors
  readonly users = this._users.asReadonly();
  readonly loading = this._loading.asReadonly();
  readonly error = this._error.asReadonly();
  readonly userCount = computed(() => this._users().length);

  loadUsers(): void {
    this._loading.set(true);
    this._error.set(null);

    this.http.get<User[]>('/api/users').pipe(
      tap(users => {
        this._users.set(users);
        this._loading.set(false);
      }),
      catchError(err => {
        this._error.set(err.message);
        this._loading.set(false);
        return EMPTY;
      })
    ).subscribe();
  }

  addUser(user: Omit<User, 'id'>): void {
    this.http.post<User>('/api/users', user).pipe(
      tap(created => this._users.update(users => [...users, created])),
      catchError(err => {
        this._error.set(err.message);
        return EMPTY;
      })
    ).subscribe();
  }
}

Patterns demonstrated:

  • inject() function instead of constructor injection
  • providedIn: 'root' for tree-shakable singleton
  • Private writable signals with public readonly selectors
  • computed() for derived state
  • update() for immutable array operations
  • RxJS for HTTP calls with signal-based state

Lazy-Loaded Feature Routes

typescript
// app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    redirectTo: 'dashboard',
    pathMatch: 'full'
  },
  {
    path: 'dashboard',
    loadComponent: () =>
      import('./features/dashboard/dashboard.component')
        .then(m => m.DashboardComponent)
  },
  {
    path: 'users',
    loadChildren: () =>
      import('./features/users/users.routes')
        .then(m => m.usersRoutes)
  },
  {
    path: 'settings',
    loadChildren: () =>
      import('./features/settings/settings.routes')
        .then(m => m.settingsRoutes),
    canActivate: [() => inject(AuthService).isAuthenticated()]
  }
];
typescript
// features/users/users.routes.ts
import { Routes } from '@angular/router';

export const usersRoutes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./user-list/user-list.component')
        .then(m => m.UserListComponent)
  },
  {
    path: ':id',
    loadComponent: () =>
      import('./user-detail/user-detail.component')
        .then(m => m.UserDetailComponent)
  }
];

Patterns demonstrated:

  • loadComponent for single-component routes
  • loadChildren for feature route groups
  • Functional route guards with inject()
  • Each feature owns its routes file

Reactive Form Pattern

typescript
// edit-user.component.ts
import {
  ChangeDetectionStrategy,
  Component,
  inject,
  input,
  output
} from '@angular/core';
import {
  FormBuilder,
  ReactiveFormsModule,
  Validators
} from '@angular/forms';
import { User } from '../models/user.model';

@Component({
  selector: 'app-edit-user',
  standalone: true,
  imports: [ReactiveFormsModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <label>
        Name
        <input formControlName="name" />
        @if (form.controls.name.errors?.['required'] && form.controls.name.touched) {
          <span class="error">Name is required</span>
        }
      </label>

      <label>
        Email
        <input formControlName="email" type="email" />
        @if (form.controls.email.errors?.['email'] && form.controls.email.touched) {
          <span class="error">Invalid email</span>
        }
      </label>

      <button type="submit" [disabled]="form.invalid">Save</button>
    </form>
  `
})
export class EditUserComponent {
  private readonly fb = inject(FormBuilder);

  user = input.required<User>();
  saved = output<User>();

  form = this.fb.nonNullable.group({
    name: ['', Validators.required],
    email: ['', [Validators.required, Validators.email]]
  });

  constructor() {
    // Populate form when user input changes
    effect(() => {
      const u = this.user();
      this.form.patchValue({ name: u.name, email: u.email });
    });
  }

  onSubmit(): void {
    if (this.form.valid) {
      this.saved.emit({ ...this.user(), ...this.form.getRawValue() });
    }
  }
}

Patterns demonstrated:

  • Reactive forms with FormBuilder and nonNullable
  • input.required() for mandatory inputs
  • output() function instead of @Output() decorator
  • @if control flow for validation messages
  • effect() to sync signal input to reactive form

HTTP Interceptor Pattern

typescript
// api.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';

export const apiInterceptor: HttpInterceptorFn = (req, next) => {
  const auth = inject(AuthService);
  const token = auth.token();

  if (token) {
    req = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` }
    });
  }

  return next(req);
};
typescript
// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { apiInterceptor } from './core/interceptors/api.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes),
    provideHttpClient(withInterceptors([apiInterceptor]))
  ]
};

Patterns demonstrated:

  • Functional interceptor (HttpInterceptorFn) instead of class-based
  • inject() inside the interceptor function
  • provideHttpClient with withInterceptors in app config
  • No NgModule — pure function-based configuration

Component Communication with Signals

typescript
// parent.component.ts
@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: `
    <app-child
      [items]="filteredItems()"
      (itemSelected)="onItemSelected($event)"
    />
    <p>Selected: &#123;&#123; selectedItem()?.name ?? 'none' &#125;&#125;</p>
  `
})
export class ParentComponent {
  private readonly itemService = inject(ItemService);

  searchTerm = signal('');
  selectedItem = signal<Item | null>(null);

  // Derived state: filters items reactively when searchTerm or items change
  filteredItems = computed(() => {
    const term = this.searchTerm().toLowerCase();
    return this.itemService.items().filter(
      item => item.name.toLowerCase().includes(term)
    );
  });

  onItemSelected(item: Item): void {
    this.selectedItem.set(item);
  }
}
typescript
// child.component.ts
@Component({
  selector: 'app-child',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @for (item of items(); track item.id) {
      <div (click)="itemSelected.emit(item)" class="item">
        &#123;&#123; item.name &#125;&#125;
      </div>
    } @empty {
      <p>No items found</p>
    }
  `
})
export class ChildComponent {
  items = input.required<Item[]>();
  itemSelected = output<Item>();
}

Patterns demonstrated:

  • Parent manages state with signals
  • computed() derives a filtered list reactively
  • Child receives data via input.required(), emits via output()
  • @for with track for efficient rendering
  • @empty block for empty state

NgRx Store Pattern

For projects using NgRx for state management, follow this pattern for feature state slices:

typescript
// items.actions.ts
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Item } from '../../types/item.types';

export const ItemsActions = createActionGroup({
  source: 'Items',
  events: {
    'Load Items': emptyProps(),
    'Load Items Success': props<{ items: Item[] }>(),
    'Load Items Failure': props<{ error: string }>(),
  }
});
typescript
// items.reducers.ts
import { createReducer, on } from '@ngrx/store';
import { EntityAdapter, EntityState, createEntityAdapter } from '@ngrx/entity';
import { ItemsActions } from './items.actions';

export interface ItemsState extends EntityState<Item> {
  loading: boolean;
  error: string | null;
}

export const itemsAdapter: EntityAdapter<Item> = createEntityAdapter<Item>();

const initialState: ItemsState = itemsAdapter.getInitialState({
  loading: false,
  error: null
});

export const itemsReducer = createReducer(
  initialState,
  on(ItemsActions.loadItems, state => ({ ...state, loading: true, error: null })),
  on(ItemsActions.loadItemsSuccess, (state, { items }) =>
    itemsAdapter.setAll(items, { ...state, loading: false })
  ),
  on(ItemsActions.loadItemsFailure, (state, { error }) => ({
    ...state, loading: false, error
  }))
);
typescript
// items.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ItemsState, itemsAdapter } from './items.reducers';

const selectItemsState = createFeatureSelector<ItemsState>('items');
const { selectAll, selectTotal } = itemsAdapter.getSelectors();

export const ItemsSelectors = {
  selectAllItems: createSelector(selectItemsState, selectAll),
  selectItemCount: createSelector(selectItemsState, selectTotal),
  selectLoading: createSelector(selectItemsState, state => state.loading),
  selectError: createSelector(selectItemsState, state => state.error),
};
typescript
// items.effects.ts
@Injectable()
export class ItemsEffects {
  private readonly actions$ = inject(Actions);
  private readonly api = inject(ItemsApiService);

  readonly loadItems$ = createEffect(() =>
    this.actions$.pipe(
      ofType(ItemsActions.loadItems),
      exhaustMap(() =>
        this.api.getItems().pipe(
          map(items => ItemsActions.loadItemsSuccess({ items })),
          catchError(error => of(ItemsActions.loadItemsFailure({ error: error.message })))
        )
      )
    )
  );
}
typescript
// data-access-state.config.ts — register in app.config.ts
export function provideDataAccessState() {
  return [
    provideState('items', itemsReducer),
    provideEffects(ItemsEffects),
  ];
}

Patterns demonstrated:

  • createActionGroup() for typed, organized actions
  • EntityAdapter for normalized collection state
  • Memoized selectors exported as a named object
  • Effects use inject() and handle errors with catchError
  • Separate API service keeps effects focused on orchestration
  • State registration via provideState() and provideEffects()

Daily Workflows with AI Assistants

Generating a New Feature

Describe the feature with enough context. Include the data model, user interactions, and where it fits in the app.

Standard Angular CLI project:

Create a "notifications" feature under src/app/features/notifications/ with:
- A NotificationService that fetches from GET /api/notifications and uses signals for state
- A notification-list component that displays notifications with @for
- A notification-badge component that shows unread count using computed()
- Lazy-loaded routes in notifications.routes.ts
- Unit tests for the service and both components
Follow the patterns in the project context file.

Nx monorepo project (with NgRx):

Create a "notifications" feature:
- In libs/ui/data-access/src/lib/state/notifications/, create NgRx actions, reducer, selectors, and effects
- In libs/ui/data-access/src/lib/api/, create a NotificationsApiService for GET /api/notifications
- In libs/ui/features/notifications/src/lib/components/, create:
  - pages/notifications-page.component.ts (dispatches actions, selects from store)
  - views/notification-list-view.component.ts (displays items with @for)
  - views/notification-badge-view.component.ts (shows unread count via input)
- Add lazy-loaded routes in libs/ui/features/notifications/src/lib/notifications.routes.ts
- Register the state slice in data-access-state.config.ts
- Unit tests for all artifacts
Follow the patterns in the project context file.

Migrating Legacy Code

When migrating from older Angular patterns, be explicit about what to change and what to preserve.

Migrate src/app/legacy/user-management/ from NgModule-based to standalone components:
- Convert UserManagementModule to standalone components with lazy-loaded routes
- Replace *ngIf and *ngFor with @if and @for
- Replace @Input()/@Output() decorators with input()/output() functions
- Replace constructor injection with inject()
- Add ChangeDetectionStrategy.OnPush to all components
- Keep all existing functionality and tests passing

Writing Tests

Point the AI assistant at the specific file and describe coverage expectations. The example below uses a Claude Code skill trigger, but the prompt content works with any tool.

/ng-test src/app/features/orders/order.service.ts

Additional requirements:
- Mock HttpClient using HttpTestingController
- Test loadOrders success and error paths
- Test signal state transitions (loading, error, data)
- Test computed property orderCount

Code Review

Ask the AI assistant to review a feature directory or specific files. The example below uses a Claude Code skill trigger, but the prompt content works with any tool.

/ng-review src/app/features/checkout/

Focus on:
- Signal usage and reactivity patterns
- Route guard implementation
- Form validation completeness

Debugging

Provide the error message and relevant context. AI assistants can read the files and trace the issue.

I'm getting "NG0100: ExpressionChangedAfterItHasBeenCheckedError" in the
dashboard component. The component uses a signal that is updated in ngOnInit
by calling a service method. Help me find and fix the root cause.

Tips and Pitfalls

Do

  • Keep your context file updated. When your team adopts new conventions, update the file. It is the single source of truth for your AI assistant.
  • Use reusable instructions for repetitive tasks. If your team generates the same kind of component repeatedly, a saved instruction template ensures consistency.
  • Provide concrete examples. Instead of "use signals", show the AI assistant a 10-line example from your codebase. Patterns in context files beat abstract rules.
  • Be specific in prompts. "Create a component" is vague. "Create a standalone component at src/app/features/orders/order-summary/ that takes an Order input and displays line items with @for" is actionable.
  • Iterate. If the output isn't right, tell the assistant what's wrong specifically. "The component should use input() not @Input()" is more useful than "fix it".
  • Specify your state management approach. If your project uses NgRx, say so in your context file and prompts. Otherwise AI assistants may default to signal-based services.
  • Document Nx library boundaries. If using Nx, include the scope tag rules so the AI assistant respects module boundaries when placing new files.

Avoid

  • Don't rely on AI knowing your Angular version. Always state it in your context file. Angular evolves rapidly; v19 patterns differ from v17, and v20 introduces further changes.
  • Don't skip the review. AI-generated code can introduce subtle issues: missing track expressions in @for, incorrect form validation, or services not properly error-handled.
  • Don't generate entire features in one shot. Break large features into smaller requests: service first, then components, then routes, then tests. This produces better results.
  • Don't use deprecated patterns in examples. If your context file shows *ngIf examples, the AI assistant will follow them. Keep your reference code modern.
  • Don't ignore MCP servers. The Angular CLI MCP and Nx MCP servers can look up documentation, run generators, and query workspace structure directly, reducing hallucination.

References

Angular and AI

Angular Ecosystem

Claude Code (used for snippets in this guide)

Copyright © MSG Systems Romania AI Labs