Scaffold an Angular 19 project with standalone components (no NgModules), signals, lazy-loaded routing, `inject()` services, and Angular Material or PrimeNG.
79
73%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./frontend/angular-project-starter/SKILL.mdScaffold 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/themessrc/
├── 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 pointNgModule 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.jsonnpm 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.181fcbc
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.