🎨 Frontend Engineering Guidelines (Angular)
This document defines the standards, conventions, and best practices for all Frontend (Angular) development in our organization.
It ensures consistency, maintainability, scalability, and high-quality user experiences across all frontend projects.
Our frontend principles:
src/
├── app/
│ ├── core/ # Singleton services, guards, interceptors
│ │ ├── interceptors/ # HTTP interceptors (auth, error handling)
│ │ ├── guards/ # Route guards (auth, role-based)
│ │ ├── services/ # App-wide services (auth, config)
│ │ └── utils/ # Helper functions, constants
│ ├── shared/ # Shared/reusable components and utilities
│ │ ├── components/ # Dumb/presentational components
│ │ ├── directives/ # Custom directives
│ │ ├── pipes/ # Custom pipes
│ │ └── models/ # Interfaces, types, enums
│ ├── features/ # Feature modules (lazy-loaded)
│ │ ├── dashboard/
│ │ │ ├── components/ # Feature-specific components
│ │ │ ├── services/ # Feature-specific services
│ │ │ ├── dashboard.component.ts
│ │ │ ├── dashboard.routes.ts
│ │ │ └── dashboard.module.ts
│ │ └── user/
│ ├── graphql/ # GraphQL queries, mutations, fragments
│ │ ├── queries/
│ │ ├── mutations/
│ │ └── fragments/
│ ├── app-routing.module.ts
│ ├── app.component.ts
│ └── app.module.ts
├── assets/ # Static files (images, fonts, i18n)
├── environments/ # Environment configs
└── main.ts
core/: Import once in AppModule, contains singleton servicesshared/: Import in feature modules, contains reusable UI componentsfeatures/: Lazy-loaded, self-contained feature modulesgraphql/: Centralized GraphQL operations for better maintainability| Type | Convention | Example |
|---|---|---|
| Variables | camelCase | userList, totalCount |
| Functions / Methods | verb + noun | loadUsers(), updateProfile() |
| Classes / Components | PascalCase | UserCardComponent, AuthService |
| Constants | UPPER_SNAKE_CASE | API_BASE_URL, MAX_RETRIES |
| Files | kebab-case | user-card.component.ts |
| Selectors (HTML) | app- prefix | <app-user-card> |
| Modules | suffix-based | DashboardModule, UserModule |
| Interfaces | prefixed with I | IUser, IChartData |
| Types | prefixed with T | TUserRole, TApiResponse |
| Enums | PascalCase | UserStatus, HttpMethod |
| GraphQL Queries | GET_ prefix | GET_USER_PROFILE, GET_DASHBOARD_DATA |
| GraphQL Mutations | descriptive verb | UPDATE_USER_ROLE, CREATE_PROJECT |
| Observable variables | $ suffix | users$, loading$ |
| Boolean variables | is/has/should prefix | isLoading, hasPermission |
any type - use unknown or proper types/**
* DashboardService
* Handles data fetching and transformation for dashboard charts.
* @remarks Uses Apollo Client for GraphQL queries
*/
@Injectable({ providedIn: 'root' })
export class DashboardService {
constructor(
private apollo: Apollo,
private logger: LoggerService
) {}
/**
* Fetch employability data from GraphQL API
* @returns Observable of dashboard data
* @throws {ApolloError} When GraphQL query fails
*/
getDashboardData(): Observable<IDashboardData> {
return this.apollo.query<IDashboardData>({
query: GET_DASHBOARD_DATA,
fetchPolicy: 'network-only',
}).pipe(
map(result => result.data),
catchError(error => {
this.logger.error('Failed to fetch dashboard data', 'DashboardService');
throw error;
})
);
}
}
/**
* UserCardComponent
* Displays user information in a styled card format.
* @example
* <app-user-card [user]="currentUser" (selected)="onUserSelect($event)"></app-user-card>
*/
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html',
styleUrls: ['./user-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserCardComponent {
/** User data to display */
@Input() user!: IUser;
/** Emits when user card is clicked */
@Output() selected = new EventEmitter<IUser>();
/**
* Handles user card selection
*/
onSelect(): void {
this.selected.emit(this.user);
}
}
/**
* @fileoverview Service for managing user authentication
* @author Frontend Team
* @module core/services
*/
<component>.spec.tsdescribe('ComponentName', () => {})it('should do something', () => {})import { TestBed } from '@angular/core/testing';
import { ApolloTestingModule, ApolloTestingController } from 'apollo-angular/testing';
import { DashboardService } from './dashboard.service';
import { GET_DASHBOARD_DATA } from '../graphql/queries';
describe('DashboardService', () => {
let service: DashboardService;
let controller: ApolloTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ApolloTestingModule],
providers: [DashboardService],
});
service = TestBed.inject(DashboardService);
controller = TestBed.inject(ApolloTestingController);
});
afterEach(() => {
controller.verify();
});
it('should fetch dashboard data successfully', (done) => {
const mockData = { stats: { users: 100, revenue: 5000 } };
service.getDashboardData().subscribe(data => {
expect(data).toEqual(mockData);
done();
});
const op = controller.expectOne(GET_DASHBOARD_DATA);
op.flush({ data: mockData });
});
});
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserCardComponent } from './user-card.component';
import { IUser } from '@shared/models';
describe('UserCardComponent', () => {
let component: UserCardComponent;
let fixture: ComponentFixture<UserCardComponent>;
const mockUser: IUser = {
id: '1',
name: 'John Doe',
email: 'john@example.com',
};
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [UserCardComponent],
}).compileComponents();
fixture = TestBed.createComponent(UserCardComponent);
component = fixture.componentInstance;
component.user = mockUser;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should emit selected event when clicked', () => {
spyOn(component.selected, 'emit');
component.onSelect();
expect(component.selected.emit).toHaveBeenCalledWith(mockUser);
});
it('should display user name', () => {
const compiled = fixture.nativeElement;
expect(compiled.textContent).toContain('John Doe');
});
});
import { test, expect } from '@playwright/test';
test.describe('User Login Flow', () => {
test('should login successfully with valid credentials', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'user@example.com');
await page.fill('[data-testid="password-input"]', 'password123');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="welcome-message"]')).toBeVisible();
});
});
🔐 6. Security & Validation
innerHTML unless absolutely necessaryimport { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({
selector: 'app-safe-content',
template: `<div [innerHTML]="safeHtml"></div>`,
})
export class SafeContentComponent {
safeHtml: SafeHtml;
constructor(private sanitizer: DomSanitizer) {
const userInput = '<p>User content</p><script>alert("XSS")</script>';
// Sanitize HTML to prevent XSS attacks
this.safeHtml = this.sanitizer.sanitize(SecurityContext.HTML, userInput) || '';
}
}
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-user-form',
templateUrl: './user-form.component.html',
})
export class UserFormComponent implements OnInit {
userForm!: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.userForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [
Validators.required,
Validators.minLength(8),
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).*$/)
]],
age: ['', [Validators.required, Validators.min(18), Validators.max(120)]],
});
}
onSubmit(): void {
if (this.userForm.valid) {
// Proceed with validated data
console.log(this.userForm.value);
}
}
}
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from '@core/services/auth.service';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router
) {}
canActivate(): boolean {
if (this.authService.isAuthenticated()) {
return true;
}
this.router.navigate(['/login']);
return false;
}
}
// environment.ts - NEVER commit secrets here
export const environment = {
production: false,
apiUrl: 'https://api.dev.example.com',
graphqlEndpoint: 'https://api.dev.example.com/graphql',
// Use build-time injection for sensitive values
};
Note: Same rules as Backend — consistency across teams.
<type>(<scope>): <short summary> [<issue-id>]
Allowed types: feat, fix, refactor, docs, test, chore, style, perf, ci
Examples:
feat(user): implement profile edit modal [QA-101]
fix(auth): resolve token refresh bug [QA-087]
style(ui): improve dashboard layout spacing [QA-092]
perf(dashboard): optimize chart rendering [QA-093]
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'header-max-length': [2, 'always', 100],
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'perf', 'ci'],
],
'scope-empty': [2, 'never'],
'subject-case': [2, 'always', 'lower-case'],
},
};
[Type][Module] Short description [Issue-ID]
Examples:
[Feat][User] add profile picture upload [QA-101]
[Fix][Dashboard] resolve chart rendering issue [QA-111]
[Refactor][Auth] simplify token management [QA-115]
### 🧩 Description
Explain what this PR does and why.
### 🔖 Issue Ref
QA-101
### ✅ Changes
- Added user profile picture upload functionality
- Fixed validation for image file types
- Updated user model to include avatar URL
### 🧪 How to Test
1. Run `npm start`
2. Navigate to `/profile/edit`
3. Click "Upload Picture" button
4. Expected: Image uploads successfully and displays in preview
### 📸 Screenshots (if applicable)
<!-- Add screenshots here -->
### 🧠 Notes
- Image size is limited to 5MB
- Supported formats: JPG, PNG, WebP
- Follow-up: Add image cropping feature [QA-102]
Note: Identical to BE standards
feature/<module>-<short-desc>
fix/<module>-<short-desc>
chore/<short-desc>
hotfix/<issue-id>-<short-desc>
Examples:
feature/dashboard-chart-improvement
fix/user-login-form
chore/update-angular-version
hotfix/QA-125-critical-auth-bug
develop (or main for hotfixes)main / master: Protected, requires PR + approvalsdevelop: Protected, requires PR + approvals# Install Husky
npm install --save-dev husky
npx husky install
# Add commit-msg hook
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'
# Add pre-commit hook
npx husky add .husky/pre-commit 'npm run lint && npm run test:changed'
// _breakpoints.scss
$breakpoint-mobile: 768px;
$breakpoint-tablet: 1024px;
$breakpoint-desktop: 1440px;
@mixin mobile {
@media (max-width: #{$breakpoint-mobile - 1px}) {
@content;
}
}
@mixin tablet {
@media (min-width: $breakpoint-mobile) and (max-width: #{$breakpoint-tablet - 1px}) {
@content;
}
}
@mixin desktop {
@media (min-width: $breakpoint-desktop) {
@content;
}
}
// _variables.scss
$primary-color: #3f51b5;
$secondary-color: #ff4081;
$spacing-unit: 8px;
$border-radius: 4px;
// Component styles
.user-card {
padding: $spacing-unit * 2;
border-radius: $border-radius;
background-color: var(--surface-color);
&__header {
margin-bottom: $spacing-unit;
}
&__title {
font-size: 1.25rem;
font-weight: 600;
}
&--highlighted {
border: 2px solid $primary-color;
}
}
ChangeDetectionStrategy.OnPush for all componentstrackBy for *ngFor loops@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserListComponent {
users: IUser[] = [];
// TrackBy function for *ngFor optimization
trackByUserId(index: number, user: IUser): string {
return user.id;
}
}
<!-- Template with trackBy -->
<div *ngFor="let user of users; trackBy: trackByUserId">
<app-user-card [user]="user"></app-user-card>
</div>
| Concern | Standard | Notes |
|---|---|---|
| State Management | NGXS / NGRX | Choose one and stay consistent |
| GraphQL | Apollo Angular Client | For API communication |
| Local State | Component state + Services | For simple UI state |
| Global State | NGXS/NGRX Store | For cross-component data |
| Cache | Apollo cache / Store | Optimize data fetching |
| Observables | RxJS best practices | async pipe, takeUntil |
// user.state.ts
import { State, Action, StateContext, Selector } from '@ngxs/store';
export interface UserStateModel {
users: IUser[];
selectedUser: IUser | null;
loading: boolean;
}
@State<UserStateModel>({
name: 'user',
defaults: {
users: [],
selectedUser: null,
loading: false,
},
})
export class UserState {
constructor(private userService: UserService) {}
@Selector()
static users(state: UserStateModel): IUser[] {
return state.users;
}
@Selector()
static loading(state: UserStateModel): boolean {
return state.loading;
}
@Action(LoadUsers)
loadUsers(ctx: StateContext<UserStateModel>) {
ctx.patchState({ loading: true });
return this.userService.getUsers().pipe(
tap(users => {
ctx.patchState({ users, loading: false });
}),
catchError(error => {
ctx.patchState({ loading: false });
throw error;
})
);
}
}
// users.graphql.ts
import { gql } from 'apollo-angular';
export const GET_USERS = gql`
query GetUsers($limit: Int, $offset: Int) {
users(limit: $limit, offset: $offset) {
id
name
email
avatar
role
}
}
`;
export const UPDATE_USER = gql`
mutation UpdateUser($id: ID!, $input: UserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
}
}
`;
// user.service.ts
import { Injectable } from '@angular/core';
import { Apollo } from 'apollo-angular';
import { map } from 'rxjs/operators';
import { GET_USERS, UPDATE_USER } from './users.graphql';
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private apollo: Apollo) {}
getUsers(limit = 10, offset = 0) {
return this.apollo.query({
query: GET_USERS,
variables: { limit, offset },
fetchPolicy: 'cache-first',
}).pipe(
map(result => result.data.users)
);
}
updateUser(id: string, input: Partial<IUser>) {
return this.apollo.mutate({
mutation: UPDATE_USER,
variables: { id, input },
refetchQueries: [{ query: GET_USERS }],
});
}
}
// Smart component
@Component({
selector: 'app-user-container',
template: `
<app-user-list
[users]="users$ | async"
[loading]="loading$ | async"
[error]="error$ | async"
(userSelected)="onUserSelected($event)"
></app-user-list>
`,
})
export class UserContainerComponent {
users$ = this.store.select(UserState.users);
loading$ = this.store.select(UserState.loading);
error$ = this.store.select(UserState.error);
constructor(private store: Store) {
this.store.dispatch(new LoadUsers());
}
onUserSelected(user: IUser): void {
this.store.dispatch(new SelectUser(user));
}
}
Change Detection Strategy
@Component({
selector: 'app-optimized',
changeDetection: ChangeDetectionStrategy.OnPush,
})
Lazy Loading Modules
// app-routing.module.ts
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./features/dashboard/dashboard.module')
.then(m => m.DashboardModule)
}
];
TrackBy for Lists
trackByFn(index: number, item: any): any {
return item.id; // or index
}
Preloading Strategy
RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})
ng build --configuration productionnpm install --save-dev webpack-bundle-analyzer# Analyze bundle
ng build --stats-json
npx webpack-bundle-analyzer dist/stats.json
// Debounce user input
searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged()
).subscribe(value => this.search(value));
// Virtual scrolling for large lists
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items" class="item">
{{item}}
</div>
</cdk-virtual-scroll-viewport>
// Memoization for expensive calculations
private memoizedResult = new Map();
getExpensiveValue(key: string) {
if (!this.memoizedResult.has(key)) {
this.memoizedResult.set(key, this.calculateExpensiveValue(key));
}
return this.memoizedResult.get(key);
}
<!-- Lazy load images -->
<img loading="lazy" src="image.jpg" alt="Description">
<!-- Use WebP with fallback -->
<picture>
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Description">
</picture>
<!-- Responsive images -->
<img
srcset="small.jpg 300w, medium.jpg 768w, large.jpg 1024w"
sizes="(max-width: 768px) 100vw, 50vw"
src="medium.jpg"
alt="Description">
| Metric | Target | Critical |
|---|---|---|
| First Contentful Paint | < 1.5s | < 2.5s |
| Time to Interactive | < 3.0s | < 5.0s |
| Bundle Size (gzipped) | < 200KB | < 500KB |
| Lighthouse Score | > 90 | > 75 |
Target: Level AA compliance minimum
<!-- ✅ Good -->
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/home">Home</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Page Title</h1>
<p>Content...</p>
</article>
</main>
<footer>
<p>© 2025 Company</p>
</footer>
<!-- ❌ Bad -->
<div class="header">
<div class="nav">
<div class="link">Home</div>
</div>
</div>
<!-- Form labels -->
<label for="email">Email Address</label>
<input
id="email"
type="email"
aria-required="true"
aria-describedby="email-help">
<span id="email-help">We'll never share your email</span>
<!-- Buttons -->
<button
aria-label="Close dialog"
(click)="closeDialog()">
<mat-icon>close</mat-icon>
</button>
<!-- Loading states -->
<div
*ngIf="loading"
role="status"
aria-live="polite">
Loading...
</div>
<!-- Error messages -->
<div
*ngIf="error"
role="alert"
aria-live="assertive">
{{error}}
</div>
@Component({
selector: 'app-dialog',
template: `
<div
role="dialog"
aria-modal="true"
(keydown.escape)="close()">
<button
#closeButton
(click)="close()"
(keydown.enter)="close()">
Close
</button>
</div>
`
})
export class DialogComponent implements AfterViewInit {
@ViewChild('closeButton') closeButton!: ElementRef;
ngAfterViewInit() {
// Focus trap - focus first focusable element
this.closeButton.nativeElement.focus();
}
}
alt attributes// ❌ BAD - Memory leak
export class BadComponent {
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users = users;
}); // Never unsubscribed!
}
}
// ✅ GOOD - Using async pipe
@Component({
template: `<div *ngFor="let user of users$ | async">{{user.name}}</div>`
})
export class GoodComponent {
users$ = this.userService.getUsers();
}
// ✅ GOOD - Using takeUntil
export class GoodComponent implements OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.userService.getUsers()
.pipe(takeUntil(this.destroy$))
.subscribe(users => this.users = users);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
// ✅ GOOD - Using takeUntilDestroyed (Angular 16+)
export class ModernComponent {
private destroyRef = inject(DestroyRef);
ngOnInit() {
this.userService.getUsers()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(users => this.users = users);
}
}
// Debounce user input
searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.searchService.search(term))
);
// Combine multiple observables
combineLatest([
this.route.params,
this.store.select(state => state.user)
]).pipe(
map(([params, user]) => ({ params, user }))
);
// Error handling
this.http.get('/api/users').pipe(
retry(3),
catchError(error => {
console.error('Failed:', error);
return of([]); // Return fallback value
})
);
// Loading states
getData() {
this.loading = true;
return this.http.get('/api/data').pipe(
finalize(() => this.loading = false)
);
}
// Share expensive operations
expensiveData$ = this.service.getExpensiveData().pipe(
shareReplay(1) // Cache and share result
);
| Use Case | Operator | When to Use |
|---|---|---|
| Cancel previous request | switchMap |
Search, autocomplete |
| Wait for all requests | mergeMap / flatMap |
Parallel operations |
| Maintain order | concatMap |
Sequential operations |
| Combine latest values | combineLatest |
Multiple dependent streams |
| Wait for all to complete | forkJoin |
Multiple independent requests |
| Emit only after silence | debounceTime |
User input |
| Emit at intervals | throttleTime |
Scroll, resize events |
// graphql/fragments/user.fragment.ts
export const USER_FRAGMENT = gql`
fragment UserFields on User {
id
name
email
avatar
role
createdAt
}
`;
// graphql/queries/users.query.ts
import { USER_FRAGMENT } from '../fragments/user.fragment';
export const GET_USERS = gql`
${USER_FRAGMENT}
query GetUsers($filter: UserFilter, $pagination: PaginationInput) {
users(filter: $filter, pagination: $pagination) {
nodes {
...UserFields
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
`;
export const GET_USER = gql`
${USER_FRAGMENT}
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
projects {
id
name
}
}
}
`;
# Install codegen
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript
# codegen.yml
schema: 'https://api.example.com/graphql'
documents: 'src/**/*.graphql.ts'
generates:
src/app/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typescript-apollo-angular
// Usage with generated types
import { GetUsersGQL, GetUsersQuery } from '@app/generated/graphql';
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private getUsersGQL: GetUsersGQL) {}
getUsers(filter: UserFilter): Observable<GetUsersQuery> {
return this.getUsersGQL.fetch({ filter }).pipe(
map(result => result.data)
);
}
}
// apollo.config.ts
import { InMemoryCache } from '@apollo/client/core';
export const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
users: {
// Merge paginated results
keyArgs: ['filter'],
merge(existing = { nodes: [] }, incoming) {
return {
...incoming,
nodes: [...existing.nodes, ...incoming.nodes],
};
},
},
},
},
},
});
updateUser(id: string, input: UserInput) {
return this.apollo.mutate({
mutation: UPDATE_USER,
variables: { id, input },
optimisticResponse: {
__typename: 'Mutation',
updateUser: {
__typename: 'User',
id,
...input,
},
},
update: (cache, { data }) => {
// Update cache manually if needed
cache.modify({
id: cache.identify({ __typename: 'User', id }),
fields: {
name: () => data.updateUser.name,
},
});
},
});
}
this.apollo.query({ query: GET_USERS }).subscribe({
next: (result) => {
if (result.errors) {
// GraphQL errors
console.error('GraphQL errors:', result.errors);
}
this.users = result.data.users;
},
error: (error) => {
// Network errors
console.error('Network error:', error);
this.handleError(error);
},
});
console.log or commented-out codeany types (use proper TypeScript types)ChangeDetectionStrategy.OnPush usedtrackBy used in *ngFor// environment.prod.ts - NEVER commit secrets
export const environment = {
production: true,
apiUrl: '${API_URL}', // Injected at build time
graphqlEndpoint: '${GRAPHQL_ENDPOINT}',
version: '${APP_VERSION}',
sentryDsn: '${SENTRY_DSN}',
};
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main, develop]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm run test:ci
- name: Build
run: npm run build:prod
env:
API_URL: ${{ secrets.API_URL }}
GRAPHQL_ENDPOINT: ${{ secrets.GRAPHQL_ENDPOINT }}
- name: Deploy
run: npm run deploy
if: success()
// package.json
{
"scripts": {
"start": "ng serve",
"build": "ng build",
"build:prod": "ng build --configuration production",
"test": "jest",
"test:ci": "jest --ci --coverage --maxWorkers=2",
"lint": "ng lint",
"lint:fix": "ng lint --fix",
"analyze": "ng build --stats-json && webpack-bundle-analyzer dist/stats.json"
}
}
// angular.json - production configuration
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
],
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
| Category | Tool | Version | Purpose |
|---|---|---|---|
| Framework | Angular | v17+ | Core framework |
| UI Library | Angular Material / Fuse | Latest | Component library |
| State Management | NGXS / NGRX | Latest | Global state |
| GraphQL Client | Apollo Angular | Latest | API communication |
| HTTP Client | Angular HttpClient | Built-in | REST APIs |
| Styling | SCSS / TailwindCSS | Latest | Styling solution |
| Category | Tool | Purpose |
|---|---|---|
| Code Quality | ESLint + Prettier | Linting & formatting |
| Git Hooks | Husky + Commitlint | Enforce commit standards |
| Testing | Jest + Playwright | Unit & E2E testing |
| API Mocking | MSW (Mock Service Worker) | API mocking |
| Documentation | Compodoc | Auto-generate docs |
| Type Safety | TypeScript | Type checking |
| Tool | Purpose |
|---|---|
| GitHub Actions / GitLab CI | Continuous integration |
| Docker | Containerization |
| Nginx | Web server |
| AWS S3 / Vercel / Netlify | Hosting |
| Category | Tool | Purpose |
|---|---|---|
| Error Tracking | Sentry / LogRocket | Runtime error monitoring |
| Analytics | Google Analytics / Mixpanel | User behavior tracking |
| Performance | Lighthouse / Web Vitals | Performance monitoring |
| APM | New Relic / Datadog | Application performance |
| Tool | Purpose |
|---|---|
| Figma | Design system & mockups |
| Storybook | Component documentation |
| Chromatic | Visual regression testing |
// .vscode/extensions.json
{
"recommendations": [
"angular.ng-template",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-playwright.playwright",
"graphql.vscode-graphql",
"bradlc.vscode-tailwindcss",
"usernamehw.errorlens",
"streetsidesoftware.code-spell-checker"
]
}
# .husky/commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no-install commitlint --edit $1
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint
npm run test:changed
// core/services/logger.service.ts
import { Injectable } from '@angular/core';
import { environment } from '@environments/environment';
export enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
}
@Injectable({ providedIn: 'root' })
export class LoggerService {
private logLevel: LogLevel = environment.production ? LogLevel.Warn : LogLevel.Debug;
debug(message: string, context?: string, data?: any): void {
this.log(LogLevel.Debug, message, context, data);
}
info(message: string, context?: string, data?: any): void {
this.log(LogLevel.Info, message, context, data);
}
warn(message: string, context?: string, data?: any): void {
this.log(LogLevel.Warn, message, context, data);
}
error(message: string, context?: string, error?: any): void {
this.log(LogLevel.Error, message, context, error);
// Send to error tracking service
if (environment.production) {
this.sendToSentry(message, context, error);
}
}
private log(level: LogLevel, message: string, context?: string, data?: any): void {
if (level < this.logLevel) return;
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}][${LogLevel[level]}]${context ? `[${context}]` : ''} ${message}`;
switch (level) {
case LogLevel.Debug:
console.log(logMessage, data);
break;
case LogLevel.Info:
console.info(logMessage, data);
break;
case LogLevel.Warn:
console.warn(logMessage, data);
break;
case LogLevel.Error:
console.error(logMessage, data);
break;
}
}
private sendToSentry(message: string, context?: string, error?: any): void {
// Implement Sentry integration
// Sentry.captureException(error, { contexts: { context } });
}
}
// app.module.ts
import * as Sentry from '@sentry/angular';
import { ErrorHandler } from '@angular/core';
Sentry.init({
dsn: environment.sentryDsn,
environment: environment.production ? 'production' : 'development',
tracesSampleRate: 1.0,
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.routingInstrumentation,
}),
],
});
@NgModule({
providers: [
{
provide: ErrorHandler,
useValue: Sentry.createErrorHandler({
showDialog: false,
}),
},
],
})
export class AppModule {}
// core/services/performance.service.ts
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class PerformanceService {
measureComponentLoad(componentName: string): void {
performance.mark(`${componentName}-start`);
}
measureComponentLoadEnd(componentName: string): void {
performance.mark(`${componentName}-end`);
performance.measure(
`${componentName}-load`,
`${componentName}-start`,
`${componentName}-end`
);
const measure = performance.getEntriesByName(`${componentName}-load`)[0];
console.log(`${componentName} load time:`, measure.duration, 'ms');
}
trackWebVitals(): void {
// Track Core Web Vitals
// Implement web-vitals library
}
}
// core/services/analytics.service.ts
import { Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class AnalyticsService {
constructor(private router: Router) {
this.initRouteTracking();
}
private initRouteTracking(): void {
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
this.trackPageView(event.urlAfterRedirects);
});
}
trackPageView(url: string): void {
// Google Analytics
if (typeof gtag !== 'undefined') {
gtag('config', 'GA_MEASUREMENT_ID', {
page_path: url,
});
}
}
trackEvent(category: string, action: string, label?: string, value?: number): void {
if (typeof gtag !== 'undefined') {
gtag('event', action, {
event_category: category,
event_label: label,
value: value,
});
}
}
trackUserAction(action: string, data?: any): void {
console.log('User action:', action, data);
this.trackEvent('user_action', action, JSON.stringify(data));
}
}
| Metric | Tool | Alert Threshold |
|---|---|---|
| Error Rate | Sentry | > 1% of requests |
| Page Load Time | Web Vitals | > 3 seconds |
| API Response Time | New Relic | > 2 seconds |
| Bundle Size | Webpack Analyzer | > 500KB |
| Test Coverage | Jest | < 80% |
git clone <repo-url>npm cienvironment.example.ts to environment.tsnpx husky installnpm run lint && npm run testnpm startng generate component <name>ng generate service <name>ng generate module <name>npm testnpm run build:prod# Development server
npm start # Start dev server at http://localhost:4200
# Code quality
npm run lint # Run ESLint
npm run lint:fix # Auto-fix linting issues
npm run format # Format code with Prettier
# Testing
npm test # Run unit tests
npm run test:watch # Run tests in watch mode
npm run test:coverage # Generate coverage report
npm run e2e # Run E2E tests
# Build
npm run build # Development build
npm run build:prod # Production build
npm run analyze # Analyze bundle size
# Documentation
npm run docs # Generate Compodoc documentation
Problem: Module not found errors
# Solution: Clear node_modules and reinstall
rm -rf node_modules package-lock.json
npm install
Problem: NG0100: ExpressionChangedAfterItHasBeenCheckedError
// Solution: Use ChangeDetectorRef or move logic to ngAfterViewInit
constructor(private cdr: ChangeDetectorRef) {}
ngAfterViewInit() {
this.someValue = 'new value';
this.cdr.detectChanges();
}
Problem: Circular dependency warnings
// Solution: Extract shared interfaces to separate file
// ❌ BAD - circular dependency
// user.service.ts imports project.service.ts
// project.service.ts imports user.service.ts
// ✅ GOOD - extract to shared models
// shared/models/user.model.ts
// shared/models/project.model.ts
Problem: Memory leaks from subscriptions
// Solution: Always unsubscribe
// Use async pipe, takeUntil, or takeUntilDestroyed
export class MyComponent implements OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.service.getData()
.pipe(takeUntil(this.destroy$))
.subscribe();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Problem: Cannot read property of undefined
// Solution: Use optional chaining and nullish coalescing
// ❌ BAD
const name = user.profile.name;
// ✅ GOOD
const name = user?.profile?.name ?? 'Unknown';
Problem: Slow change detection
// Solution: Use OnPush change detection
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
Problem: Large bundle size
# Solution: Analyze and optimize
npm run analyze
# Remove unused dependencies
npm prune
# Implement lazy loading
Problem: Tests failing due to missing dependencies
// Solution: Mock all dependencies
TestBed.configureTestingModule({
declarations: [MyComponent],
providers: [
{ provide: MyService, useValue: mockMyService },
{ provide: Router, useValue: mockRouter }
],
imports: [HttpClientTestingModule, RouterTestingModule]
});
Problem: Async test timing issues
// Solution: Use fakeAsync and tick
it('should work', fakeAsync(() => {
component.loadData();
tick(1000); // Simulate time passing
expect(component.data).toBeDefined();
}));
# Check for outdated packages
npm outdated
# Update specific package
npm update <package-name>
# Update all patch and minor versions
npm update
# Check for security vulnerabilities
npm audit
# Fix vulnerabilities automatically
npm audit fix
# For major version updates, check migration guides first
# Use Angular Update Guide
# https://update.angular.io/
# Update Angular CLI globally
npm install -g @angular/cli@latest
# Update project Angular version
ng update @angular/core@17 @angular/cli@17
# Update Angular Material (if used)
ng update @angular/material@17
main or develop// Remove console.log statements
// ❌ Remove before merge
console.log('Debug info:', data);
// Remove commented code
// ❌ Remove before merge
// const oldFunction = () => {...};
// Remove unused imports
// ESLint will catch these
// Remove unused variables
// TypeScript strict mode will catch these
/**
* @deprecated Use newFunction() instead. Will be removed in v2.0
*/
export function oldFunction() {
if (!environment.production) {
console.warn('oldFunction is deprecated. Use newFunction instead.');
}
// ... implementation
}
# Development
ng serve # Start dev server
ng generate component name # Generate component
ng generate service name # Generate service
ng generate module name # Generate module
ng build # Build project
ng test # Run tests
ng lint # Lint code
# Shortcuts
ng g c name # Generate component
ng g s name # Generate service
ng g m name # Generate module
ng g d name # Generate directive
ng g p name # Generate pipe
ng g guard name # Generate guard
ng g interface name # Generate interface
| Type | Pattern | Example |
|---|---|---|
| Component | name.component.ts |
user-card.component.ts |
| Service | name.service.ts |
auth.service.ts |
| Module | name.module.ts |
shared.module.ts |
| Directive | name.directive.ts |
highlight.directive.ts |
| Pipe | name.pipe.ts |
currency.pipe.ts |
| Guard | name.guard.ts |
auth.guard.ts |
| Interface | name.interface.ts or name.model.ts |
user.model.ts |
| Test | name.spec.ts |
user.service.spec.ts |
constructor() - Dependency injectionngOnChanges() - When input properties changengOnInit() - Component initializationngDoCheck() - Custom change detectionngAfterContentInit() - After content projectionngAfterContentChecked() - After content checkedngAfterViewInit() - After view initializationngAfterViewChecked() - After view checkedngOnDestroy() - Cleanup before destruction| Operator | Use Case | Example |
|---|---|---|
map |
Transform values | .pipe(map(x => x * 2)) |
filter |
Filter values | .pipe(filter(x => x > 5)) |
tap |
Side effects | .pipe(tap(x => console.log(x))) |
switchMap |
Cancel previous, emit latest | Search autocomplete |
mergeMap |
Concurrent operations | Parallel requests |
concatMap |
Sequential operations | Ordered requests |
catchError |
Error handling | .pipe(catchError(err => of([]))) |
retry |
Retry on error | .pipe(retry(3)) |
debounceTime |
Delay emission | .pipe(debounceTime(300)) |
distinctUntilChanged |
Emit only if changed | Form value changes |
takeUntil |
Complete when | Unsubscribe pattern |
combineLatest |
Combine latest values | Multiple observables |
forkJoin |
Wait for all | Parallel requests |
// Basic types
string, number, boolean, null, undefined, void, any, unknown, never
// Array types
string[], Array<string>
// Object types
interface IUser { name: string; age: number; }
type TUser = { name: string; age: number; }
// Union types
type Status = 'active' | 'inactive' | 'pending';
// Intersection types
type Admin = User & { permissions: string[]; };
// Generic types
Array<T>, Promise<T>, Observable<T>
// Utility types
Partial<T>, Required<T>, Readonly<T>, Pick<T, K>, Omit<T, K>
| Code | Meaning | Action |
|---|---|---|
| 200 | OK | Success |
| 201 | Created | Resource created |
| 204 | No Content | Success, no data |
| 400 | Bad Request | Show validation errors |
| 401 | Unauthorized | Redirect to login |
| 403 | Forbidden | Show permission error |
| 404 | Not Found | Show not found message |
| 500 | Server Error | Show generic error |
| Shortcut | Action |
|---|---|
Ctrl+P |
Quick open file |
Ctrl+Shift+P |
Command palette |
Ctrl+B |
Toggle sidebar |
Ctrl+` |
Toggle terminal |
Ctrl+/ |
Toggle comment |
Alt+Up/Down |
Move line up/down |
Ctrl+D |
Select next occurrence |
Ctrl+Shift+L |
Select all occurrences |
F2 |
Rename symbol |
F12 |
Go to definition |
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- New feature in development
### Changed
- Updates to existing features
### Deprecated
- Features to be removed in future releases
### Removed
- Removed features
### Fixed
- Bug fixes
### Security
- Security patches
## [1.3.2] - 2025-10-14
### ✨ Added
- (dashboard): Add dynamic filter tabs for better data visualization [QA-121]
- (user): Implement profile picture upload functionality [QA-125]
### 🔧 Changed
- (ui): Update primary color scheme to match new branding [QA-122]
- (performance): Optimize bundle size by lazy loading feature modules [QA-123]
### 🐛 Fixed
- (auth): Fix token expiry issue causing unexpected logouts [QA-124]
- (forms): Resolve validation error message display bug [QA-126]
### 🔐 Security
- Update dependencies to patch security vulnerabilities
- Implement Content Security Policy headers
## [1.3.1] - 2025-10-01
### 🐛 Fixed
- (dashboard): Fix chart rendering issue in Safari
- (api): Handle network timeout gracefully
## [1.3.0] - 2025-09-15
### ✨ Added
- (analytics): Integrate Google Analytics 4
- (notifications): Add real-time notification system
### 💥 Breaking Changes
- Removed deprecated `UserService.getUser()` method
- Migration: Use `UserService.getUserById(id)` instead
| Last Updated | October 2025 |
| Version | 2.0 |
| Maintained by | Frontend Core Team |
| Applies to | All frontend applications built with Angular, Fuse, GraphQL, or related stack |
| Review Cycle | Quarterly |
| Next Review | January 2026 |
To propose changes to these guidelines:
docs/update-guidelines-<topic>FE_GUIDELINES.mdFor questions, suggestions, or feedback:
"Design for users, code for developers, ship with confidence."
Happy Coding! 🚀”