frontend/angular-project-starter/SKILL.md
Scaffold an Angular 19 project with standalone components (no NgModules), signals, lazy-loaded routing, `inject()` services, and Angular Material or PrimeNG.
npx skillsauth add achreftlili/deep-dev-skills angular-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scaffold an Angular 19 project with standalone components (no NgModules), signals, lazy-loaded routing,
inject()services, and Angular Material or PrimeNG.
npm install -g @angular/cli)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
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
NgModule declarations — each component, directive, and pipe imports its own dependencies.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.*.routes.ts file, loaded via loadChildren or loadComponent.input() ("dumb").FormGroup and FormControl — never UntypedFormGroup.changeDetection: ChangeDetectionStrategy.OnPush on all components.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(),
],
};
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",
},
];
// 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);
});
}
}
// 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>();
}
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);
}
}
// 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"]);
};
// 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);
};
// 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);
},
});
}
}
// 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);
}
}
.env.example to .env and fill in valuesnpm installng serve --open# 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
npm install -D jest @angular-builders/jest) or Vitest with @analogjs/vite-plugin-angular.npm install @ngrx/signals).HttpClient with typed interceptors handles HTTP. Pair with an API skill for backend integration.ReactiveFormsModule is powerful. Pair with Zod or class-validator for complex validation schemas.--ssr during project creation or run ng add @angular/ssr later.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.