CtrlK
BlogDocsLog inGet started
Tessl Logo

angular-project-starter

Scaffold an Angular 19 project with standalone components (no NgModules), signals, lazy-loaded routing, `inject()` services, and Angular Material or PrimeNG.

79

Quality

73%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./frontend/angular-project-starter/SKILL.md
SKILL.md
Quality
Evals
Security

Angular Project Starter

Scaffold an Angular 19 project with standalone components (no NgModules), signals, lazy-loaded routing, inject() services, and Angular Material or PrimeNG.

Prerequisites

  • Node.js >= 20.x
  • npm >= 10.x
  • Angular CLI >= 19.x (npm install -g @angular/cli)
  • Git

Scaffold Command

ng new my-app --style=scss --routing --ssr=false --standalone
cd my-app

# Add Angular Material (optional — choose one UI library)
ng add @angular/material

# Or add PrimeNG (alternative)
npm install primeng @primeng/themes

Project Structure

src/
├── app/
│   ├── core/
│   │   ├── guards/            # Route guards (authGuard, roleGuard)
│   │   ├── interceptors/      # HTTP interceptors (auth, error handling)
│   │   ├── services/          # Singleton services (AuthService, ApiService)
│   │   └── models/            # Core domain models/interfaces
│   ├── features/
│   │   ├── dashboard/
│   │   │   ├── dashboard.component.ts
│   │   │   ├── dashboard.component.html
│   │   │   ├── dashboard.component.scss
│   │   │   ├── dashboard.routes.ts          # Feature routes
│   │   │   ├── components/                  # Feature-specific child components
│   │   │   └── services/                    # Feature-specific services
│   │   ├── auth/
│   │   │   ├── login.component.ts
│   │   │   ├── auth.routes.ts
│   │   │   └── services/
│   │   └── settings/
│   │       ├── settings.component.ts
│   │       └── settings.routes.ts
│   ├── shared/
│   │   ├── components/        # Reusable UI components
│   │   ├── directives/        # Custom directives
│   │   ├── pipes/             # Custom pipes
│   │   └── utils/             # Utility functions
│   ├── app.component.ts       # Root component
│   ├── app.component.html
│   ├── app.config.ts          # Application config (providers)
│   └── app.routes.ts          # Top-level route definitions
├── environments/
│   ├── environment.ts
│   └── environment.prod.ts
├── styles.scss                # Global styles
└── main.ts                    # Bootstrap entry point

Key Conventions

  • Standalone components only: Angular 19 defaults to standalone. No NgModule declarations — each component, directive, and pipe imports its own dependencies.
  • Signals over RxJS for state: use Angular signals (signal(), computed(), effect()) for component and service state. Reserve RxJS for event streams, HTTP, and WebSocket scenarios.
  • inject() over constructor injection: use the inject() function for DI in components and services. Cleaner and works in functional guards/resolvers.
  • Lazy-loaded routes: each feature has its own *.routes.ts file, loaded via loadChildren or loadComponent.
  • Smart/dumb component split: feature root components fetch data ("smart"); child components receive data via input() ("dumb").
  • Typed forms: always use typed FormGroup and FormControl — never UntypedFormGroup.
  • OnPush change detection: use changeDetection: ChangeDetectionStrategy.OnPush on all components.

Essential Patterns

Application Config (app.config.ts)

// src/app/app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from "@angular/core";
import { provideRouter, withComponentInputBinding } from "@angular/router";
import { provideHttpClient, withInterceptors } from "@angular/common/http";
import { provideAnimationsAsync } from "@angular/platform-browser/animations/async";
import { routes } from "./app.routes";
import { authInterceptor } from "./core/interceptors/auth.interceptor";

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

Root Routes with Lazy Loading (app.routes.ts)

// src/app/app.routes.ts
import { Routes } from "@angular/router";
import { authGuard } from "./core/guards/auth.guard";

export const routes: Routes = [
  {
    path: "",
    redirectTo: "dashboard",
    pathMatch: "full",
  },
  {
    path: "auth",
    loadChildren: () =>
      import("./features/auth/auth.routes").then((m) => m.AUTH_ROUTES),
  },
  {
    path: "dashboard",
    canActivate: [authGuard],
    loadComponent: () =>
      import("./features/dashboard/dashboard.component").then(
        (m) => m.DashboardComponent
      ),
  },
  {
    path: "settings",
    canActivate: [authGuard],
    loadChildren: () =>
      import("./features/settings/settings.routes").then(
        (m) => m.SETTINGS_ROUTES
      ),
  },
  {
    path: "**",
    redirectTo: "dashboard",
  },
];

Standalone Component with Signals

// src/app/features/dashboard/dashboard.component.ts
import { Component, ChangeDetectionStrategy, inject, OnInit, signal, computed } from "@angular/core";
import { UserService } from "../../core/services/user.service";
import { UserCardComponent } from "./components/user-card.component";

@Component({
  selector: "app-dashboard",
  standalone: true,
  imports: [UserCardComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="p-6">
      <h1 class="mb-4 text-2xl font-bold">Dashboard</h1>

      <input
        type="text"
        [value]="searchQuery()"
        (input)="searchQuery.set(($event.target as HTMLInputElement).value)"
        placeholder="Search users..."
        class="mb-4 rounded border px-3 py-2"
      />

      <p class="mb-2 text-sm text-gray-500">
        Showing {{ filteredUsers().length }} of {{ users().length }} users
      </p>

      @for (user of filteredUsers(); track user.id) {
        <app-user-card [user]="user" />
      } @empty {
        <p class="text-gray-500">No users found.</p>
      }
    </div>
  `,
})
export class DashboardComponent implements OnInit {
  private userService = inject(UserService);

  users = signal<User[]>([]);
  searchQuery = signal("");

  filteredUsers = computed(() => {
    const query = this.searchQuery().toLowerCase();
    return this.users().filter((u) =>
      u.name.toLowerCase().includes(query)
    );
  });

  ngOnInit() {
    this.userService.getUsers().subscribe((users) => {
      this.users.set(users);
    });
  }
}

Child Component with Signal Inputs

// src/app/features/dashboard/components/user-card.component.ts
import { Component, ChangeDetectionStrategy, input } from "@angular/core";

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

@Component({
  selector: "app-user-card",
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="mb-2 rounded border p-3">
      <p class="font-medium">{{ user().name }}</p>
      <p class="text-sm text-gray-500">{{ user().email }}</p>
    </div>
  `,
})
export class UserCardComponent {
  user = input.required<User>();
}

Service with inject() and HttpClient

// src/app/core/services/user.service.ts
import { Injectable, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";

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

@Injectable({ providedIn: "root" })
export class UserService {
  private http = inject(HttpClient);
  private apiUrl = "/api/users";

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }

  getUser(id: string): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }

  createUser(user: Omit<User, "id">): Observable<User> {
    return this.http.post<User>(this.apiUrl, user);
  }
}

Functional Route Guard

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

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

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

  return router.createUrlTree(["/auth/login"]);
};

HTTP Interceptor (Functional)

// src/app/core/interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from "@angular/common/http";
import { inject } from "@angular/core";
import { AuthService } from "../services/auth.service";

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.getToken();

  if (token) {
    const cloned = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`,
      },
    });
    return next(cloned);
  }

  return next(req);
};

Typed Reactive Form

// src/app/features/auth/login.component.ts
import { Component, ChangeDetectionStrategy, inject, signal } from "@angular/core";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { AuthService } from "../../core/services/auth.service";

@Component({
  selector: "app-login",
  standalone: true,
  imports: [ReactiveFormsModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()" class="flex flex-col gap-4 p-6">
      <input formControlName="email" type="email" placeholder="Email"
        class="rounded border px-3 py-2" />
      <input formControlName="password" type="password" placeholder="Password"
        class="rounded border px-3 py-2" />

      @if (error()) {
        <p class="text-sm text-red-600">{{ error() }}</p>
      }

      <button type="submit" [disabled]="form.invalid || isLoading()"
        class="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50">
        {{ isLoading() ? 'Signing in...' : 'Sign In' }}
      </button>
    </form>
  `,
})
export class LoginComponent {
  private fb = inject(FormBuilder);
  private authService = inject(AuthService);
  private router = inject(Router);

  isLoading = signal(false);
  error = signal<string | null>(null);

  form = this.fb.nonNullable.group({
    email: ["", [Validators.required, Validators.email]],
    password: ["", [Validators.required, Validators.minLength(8)]],
  });

  async onSubmit() {
    if (this.form.invalid) return;

    this.isLoading.set(true);
    this.error.set(null);

    const { email, password } = this.form.getRawValue();

    this.authService.login(email, password).subscribe({
      next: () => this.router.navigate(["/dashboard"]),
      error: (err) => {
        this.error.set(err.message ?? "Login failed");
        this.isLoading.set(false);
      },
    });
  }
}

Auth Service with Signals

// src/app/core/services/auth.service.ts
import { Injectable, inject, signal, computed } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable, tap } from "rxjs";

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

interface LoginResponse {
  user: User;
  token: string;
}

@Injectable({ providedIn: "root" })
export class AuthService {
  private http = inject(HttpClient);

  private currentUser = signal<User | null>(null);
  private token = signal<string | null>(null);

  user = this.currentUser.asReadonly();
  isAuthenticated = computed(() => this.currentUser() !== null);

  getToken(): string | null {
    return this.token();
  }

  login(email: string, password: string): Observable<LoginResponse> {
    return this.http.post<LoginResponse>("/api/auth/login", { email, password }).pipe(
      tap((response) => {
        this.currentUser.set(response.user);
        this.token.set(response.token);
      })
    );
  }

  logout(): void {
    this.currentUser.set(null);
    this.token.set(null);
  }
}

First Steps After Scaffold

  1. Copy .env.example to .env and fill in values
  2. Install dependencies: npm install
  3. Start dev server: ng serve --open
  4. Verify the app loads at http://localhost:4200

Common Commands

# Development
ng serve                       # Start dev server (http://localhost:4200)
ng serve --open                # Start and open browser

# Generate
ng generate component features/profile/profile --standalone
ng generate service core/services/notification

# Build
ng build                       # Production build
ng build --configuration=development

# Test
ng test                        # Run unit tests (Karma/Jasmine)
ng test --watch=false          # Single run

# Lint
ng lint                        # Run ESLint (requires @angular-eslint)

# Analyze bundle
npx ng build --stats-json
npx webpack-bundle-analyzer dist/my-app/stats.json

Integration Notes

  • Testing: Angular ships with Karma + Jasmine. For a modern alternative, migrate to Jest (npm install -D jest @angular-builders/jest) or Vitest with @analogjs/vite-plugin-angular.
  • E2E: use Playwright or Cypress. Protractor is deprecated.
  • State management: Angular signals handle most state. For complex cross-feature state, add NgRx Signal Store (npm install @ngrx/signals).
  • UI library: Angular Material provides a complete component set. PrimeNG is a richer alternative with more components out of the box.
  • API layer: HttpClient with typed interceptors handles HTTP. Pair with an API skill for backend integration.
  • Auth: pair with an auth skill. The guard/interceptor pattern shown above is the standard integration approach.
  • Forms: Angular's ReactiveFormsModule is powerful. Pair with Zod or class-validator for complex validation schemas.
  • SSR: for server-side rendering, add --ssr during project creation or run ng add @angular/ssr later.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.