Angular Setup Example

November 10, 2025 · View on GitHub

Quick setup for Angular projects - Copy and customize


Setup Claude Code Documentation for [Angular Project Name]

Project Context

Project Type: Angular Application Tech Stack:

  • Angular 17+ (Standalone Components)
  • TypeScript
  • [State: NgRx / Signals / Services]
  • [Styling: Angular Material / Tailwind / SCSS]
  • [Testing: Jasmine + Karma / Jest]

Main Features: [Description]

Use Universal Setup

First, run the universal setup from UNIVERSAL_SETUP.md, then customize with Angular-specific content below.

Angular-Specific Content

ARCHITECTURE_MAP.md

## Core Directories (Angular 17+ Standalone)

src/
├── app/
│   ├── components/       # Standalone components
│   ├── services/         # Injectable services
│   ├── guards/           # Route guards
│   ├── interceptors/     # HTTP interceptors
│   ├── pipes/            # Custom pipes
│   ├── directives/       # Custom directives
│   ├── models/           # TypeScript interfaces/types
│   ├── app.routes.ts     # Route configuration
│   ├── app.config.ts     # App configuration
│   └── app.component.ts  # Root component
├── assets/               # Static assets
└── environments/         # Environment configs

## Key Patterns

### Component Structure
- Standalone components (recommended in Angular 17+)
- Component composition with inputs/outputs
- OnPush change detection for performance
- Smart/Container vs Presentational components

### State Management
- Services with RxJS for simple state
- Signals for reactive state (Angular 16+)
- NgRx for complex application state

### Routing
- Standalone route configuration
- Lazy loading with loadComponent
- Route guards for authentication
- Resolvers for pre-loading data

COMMON_MISTAKES.md

## Top 5 Angular Mistakes

### 1. Not Unsubscribing from Observables

**Symptom**: Memory leaks, duplicate API calls, performance degradation

**Anti-pattern:**
```typescript
export class MyComponent implements OnInit {
  ngOnInit() {
    this.userService.getUsers().subscribe(users => {
      this.users = users;
    });
    // ❌ Subscription never cleaned up!
  }
}

Correct (Option 1 - takeUntilDestroyed):

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

export class MyComponent implements OnInit {
  private destroyRef = inject(DestroyRef);

  ngOnInit() {
    this.userService.getUsers()
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(users => this.users = users);
  }
}

Correct (Option 2 - async pipe):

export class MyComponent {
  users$ = this.userService.getUsers();
}

// Template:
// <div *ngFor="let user of users$ | async">{{ user.name }}</div>

2. Mutating Input Properties

Anti-pattern:

@Component({
  selector: 'app-user-card',
  template: `<button (click)="updateName()">Update</button>`
})
export class UserCardComponent {
  @Input() user!: User;

  updateName() {
    this.user.name = 'New Name';  // ❌ Mutating input!
  }
}

Correct:

@Component({
  selector: 'app-user-card',
  template: `<button (click)="updateName()">Update</button>`
})
export class UserCardComponent {
  @Input() user!: User;
  @Output() userChange = new EventEmitter<User>();

  updateName() {
    this.userChange.emit({ ...this.user, name: 'New Name' });
  }
}

3. Not Using OnPush Change Detection

Symptom: Unnecessary re-renders, poor performance

Anti-pattern:

@Component({
  selector: 'app-user-list',
  // ❌ Default change detection checks on every event
  template: `...`
})
export class UserListComponent {}

Correct:

@Component({
  selector: 'app-user-list',
  changeDetection: ChangeDetectionStrategy.OnPush,  // ✅
  template: `...`
})
export class UserListComponent {
  @Input() users: User[] = [];  // Works with immutable inputs
}

4. Importing Entire NgModule Instead of Standalone Components

Anti-pattern (Angular 17+):

import { CommonModule } from '@angular/common';

@Component({
  standalone: true,
  imports: [CommonModule],  // ❌ Imports everything!
  template: `<div *ngIf="show">Content</div>`
})

Correct:

import { NgIf } from '@angular/common';

@Component({
  standalone: true,
  imports: [NgIf],  // ✅ Import only what you need
  template: `<div *ngIf="show">Content</div>`
})

5. Not Using trackBy in *ngFor

Anti-pattern:

@Component({
  template: `
    <div *ngFor="let item of items">  <!-- ❌ Re-renders all on change -->
      {{ item.name }}
    </div>
  `
})

Correct:

@Component({
  template: `
    <div *ngFor="let item of items; trackBy: trackById">
      {{ item.name }}
    </div>
  `
})
export class MyComponent {
  trackById(index: number, item: any) {
    return item.id;  // ✅ Track by unique ID
  }
}

### docs/learnings/

Create these Angular-specific files:

**component-patterns.md:**
```markdown
# Angular Component Patterns

## Standalone Components (Angular 17+)

### Basic Component
```typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-user-card',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="card">
      <h2>{{ title }}</h2>
      <button (click)="increment()">Count: {{ count }}</button>
    </div>
  `,
  styles: [`
    .card { padding: 1rem; border: 1px solid #ddd; }
  `]
})
export class UserCardComponent {
  title = 'My Component';
  count = 0;

  increment() {
    this.count++;
  }
}

Input/Output Communication

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-user-form',
  standalone: true,
  template: `
    <input [value]="name" (input)="onNameChange($event)">
    <button (click)="submit()">Submit</button>
  `
})
export class UserFormComponent {
  @Input() name: string = '';
  @Output() nameChange = new EventEmitter<string>();
  @Output() submitForm = new EventEmitter<void>();

  onNameChange(event: Event) {
    const value = (event.target as HTMLInputElement).value;
    this.nameChange.emit(value);
  }

  submit() {
    this.submitForm.emit();
  }
}

Signals (Angular 16+)

import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <div>Count: {{ count() }}</div>
    <div>Double: {{ double() }}</div>
    <button (click)="increment()">+</button>
  `
})
export class CounterComponent {
  count = signal(0);
  double = computed(() => this.count() * 2);

  increment() {
    this.count.update(value => value + 1);
  }
}

Services (Dependency Injection)

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'  // Singleton service
})
export class UserService {
  constructor(private http: HttpClient) {}

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users');
  }
}

// Usage in component:
export class MyComponent {
  private userService = inject(UserService);  // Modern inject()

  users$ = this.userService.getUsers();
}

**state-management.md:**
```markdown
# Angular State Management

## Simple State (Services + RxJS)

```typescript
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class UserStateService {
  private userSubject = new BehaviorSubject<User | null>(null);
  user$: Observable<User | null> = this.userSubject.asObservable();

  private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
  isAuthenticated$ = this.isAuthenticatedSubject.asObservable();

  setUser(user: User) {
    this.userSubject.next(user);
    this.isAuthenticatedSubject.next(true);
  }

  logout() {
    this.userSubject.next(null);
    this.isAuthenticatedSubject.next(false);
  }
}

Signals (Angular 16+)

import { Injectable, signal, computed } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class UserStore {
  // State
  private userSignal = signal<User | null>(null);
  private loadingSignal = signal(false);

  // Selectors
  user = this.userSignal.asReadonly();
  loading = this.loadingSignal.asReadonly();
  isAuthenticated = computed(() => this.user() !== null);

  // Actions
  setUser(user: User) {
    this.userSignal.set(user);
  }

  setLoading(loading: boolean) {
    this.loadingSignal.set(loading);
  }
}

NgRx (Complex State)

// State
export interface AppState {
  users: User[];
  loading: boolean;
}

// Actions
export const loadUsers = createAction('[Users] Load Users');
export const loadUsersSuccess = createAction(
  '[Users] Load Users Success',
  props<{ users: User[] }>()
);

// Reducer
export const userReducer = createReducer(
  initialState,
  on(loadUsers, state => ({ ...state, loading: true })),
  on(loadUsersSuccess, (state, { users }) => ({
    ...state,
    users,
    loading: false
  }))
);

// Effects
@Injectable()
export class UserEffects {
  loadUsers$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadUsers),
      switchMap(() =>
        this.userService.getUsers().pipe(
          map(users => loadUsersSuccess({ users })),
          catchError(() => of(loadUsersFailure()))
        )
      )
    )
  );

  constructor(
    private actions$: Actions,
    private userService: UserService
  ) {}
}

// Selectors
export const selectUsers = (state: AppState) => state.users;
export const selectLoading = (state: AppState) => state.loading;

**routing-patterns.md:**
```markdown
# Angular Routing Patterns

## Route Configuration (Standalone)

```typescript
// app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () => import('./components/home/home.component')
      .then(m => m.HomeComponent)
  },
  {
    path: 'users/:id',
    loadComponent: () => import('./components/user/user.component')
      .then(m => m.UserComponent),
    resolve: {
      user: userResolver
    }
  },
  {
    path: 'admin',
    canActivate: [authGuard],
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES)
  },
  {
    path: '**',
    redirectTo: ''
  }
];

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes)
  ]
};

Route Guards

// guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isAuthenticated()) {
    return true;
  }

  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url }
  });
};

Route Resolvers

// resolvers/user.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { UserService } from '../services/user.service';

export const userResolver: ResolveFn<User> = (route, state) => {
  const userService = inject(UserService);
  const userId = route.paramMap.get('id')!;
  return userService.getUser(userId);
};

Programmatic Navigation

import { Component, inject } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-user',
  standalone: true,
  template: `<button (click)="goToEdit()">Edit</button>`
})
export class UserComponent {
  private router = inject(Router);
  private route = inject(ActivatedRoute);

  goToEdit() {
    const userId = this.route.snapshot.paramMap.get('id');
    this.router.navigate(['/users', userId, 'edit']);

    // Or with query params:
    this.router.navigate(['/users'], {
      queryParams: { page: 2, filter: 'active' }
    });
  }
}

**testing-patterns.md:**
```markdown
# Angular Testing Patterns

## Component Testing

```typescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserCardComponent } from './user-card.component';

describe('UserCardComponent', () => {
  let component: UserCardComponent;
  let fixture: ComponentFixture<UserCardComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [UserCardComponent]  // Standalone component
    }).compileComponents();

    fixture = TestBed.createComponent(UserCardComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should display user name', () => {
    component.user = { id: 1, name: 'John Doe' };
    fixture.detectChanges();

    const compiled = fixture.nativeElement as HTMLElement;
    expect(compiled.querySelector('.name')?.textContent).toContain('John Doe');
  });

  it('should emit event on button click', () => {
    spyOn(component.userChange, 'emit');

    const button = fixture.nativeElement.querySelector('button');
    button?.click();

    expect(component.userChange.emit).toHaveBeenCalled();
  });
});

Service Testing

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should fetch users', () => {
    const mockUsers = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Jane' }
    ];

    service.getUsers().subscribe(users => {
      expect(users.length).toBe(2);
      expect(users).toEqual(mockUsers);
    });

    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });
});

Testing with Signals

import { TestBed } from '@angular/core/testing';
import { UserStore } from './user.store';

describe('UserStore', () => {
  let store: UserStore;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [UserStore]
    });
    store = TestBed.inject(UserStore);
  });

  it('should update user', () => {
    const user = { id: 1, name: 'John' };

    store.setUser(user);

    expect(store.user()).toEqual(user);
    expect(store.isAuthenticated()).toBe(true);
  });
});

### QUICK_REFERENCE.md

```markdown
## Angular Quick Commands

# Development
ng serve              # Start dev server
ng build              # Production build
ng build --watch      # Watch mode
ng test               # Run tests (Karma/Jest)
ng lint               # ESLint
ng e2e                # E2E tests

# Generation
ng generate component <name> --standalone
ng generate service <name>
ng generate guard <name>
ng generate pipe <name>
ng generate directive <name>

# Common Patterns

### Standalone Component
```typescript
@Component({
  selector: 'app-my-component',
  standalone: true,
  imports: [CommonModule],
  template: `<div>{{ data }}</div>`
})
export class MyComponent {}

Service with DI

@Injectable({ providedIn: 'root' })
export class MyService {
  constructor(private http: HttpClient) {}
}

Signal State

count = signal(0);
double = computed(() => this.count() * 2);
this.count.set(5);
this.count.update(n => n + 1);

Route Navigation

this.router.navigate(['/users', userId]);

Environment Variables

// environments/environment.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000'
};

## Expected Structure After Setup

my-angular-project/ ├── CLAUDE.md ├── .claudeignore ├── .claude/ │ ├── COMMON_MISTAKES.md (Angular-specific) │ ├── QUICK_START.md │ ├── ARCHITECTURE_MAP.md (Angular structure) │ ├── LEARNINGS_INDEX.md │ ├── DOCUMENTATION_MAINTENANCE.md │ ├── completions/ │ ├── sessions/ │ └── templates/ ├── docs/ │ ├── INDEX.md │ ├── QUICK_REFERENCE.md (Angular commands) │ ├── learnings/ │ │ ├── component-patterns.md │ │ ├── state-management.md │ │ ├── routing-patterns.md │ │ ├── testing-patterns.md │ │ └── performance.md │ └── archive/ ├── src/ │ ├── app/ │ │ ├── components/ │ │ ├── services/ │ │ ├── guards/ │ │ └── app.routes.ts │ └── environments/ └── angular.json


## Quick Start After Setup

```bash
# 1. Load essential docs (~800 tokens)
CLAUDE.md
.claude/COMMON_MISTAKES.md
.claude/QUICK_START.md
.claude/ARCHITECTURE_MAP.md

# 2. For component work, load:
docs/learnings/component-patterns.md (~600 tokens)

# 3. For state management, load:
docs/learnings/state-management.md (~500 tokens)

# 4. For routing, load:
docs/learnings/routing-patterns.md (~400 tokens)

Total: ~1,300-1,500 tokens (vs 9,000+ before)

Last Updated: 2025-11-11