Angular Coding Standards
Introduction
Angular coding standards define a set of rules, guidelines, and best practices that developers should follow when building Angular applications. These standards help ensure code consistency, readability, maintainability, and performance across your project, especially when working in teams.
In this guide, we'll explore essential Angular coding standards that beginners should follow to write clean, efficient, and maintainable code. These practices align with the official Angular Style Guide and industry best practices.
Why Coding Standards Matter
Before diving into specific standards, let's understand why they're important:
- Consistency: Makes code predictable and easier to read
- Maintainability: Makes future updates and bug fixes simpler
- Onboarding: Helps new team members get up to speed quickly
- Performance: Encourages patterns that optimize application speed
- Bug Prevention: Helps avoid common pitfalls and errors
File Structure and Naming Conventions
Project Structure
Angular applications should follow a consistent project structure. Here's a recommended approach:
src/
├── app/
│ ├── core/ # Singleton services, app-level components
│ ├── shared/ # Shared components, directives, and pipes
│ ├── features/ # Feature modules
│ │ ├── feature1/
│ │ │ ├── components/
│ │ │ ├── services/
│ │ │ ├── models/
│ │ │ └── feature1.module.ts
│ │ └── feature2/
│ ├── app-routing.module.ts
│ └── app.module.ts
├── assets/
└── environments/
Naming Conventions
- File Names: Use kebab-case for file names with descriptive suffixes:
// Good
user-profile.component.ts
auth.service.ts
data-model.interface.ts
// Bad
userProfile.component.ts
AuthService.ts
- Component and Directive Selectors: Use kebab-case prefixed with a feature or app:
// Good
@Component({
selector: 'app-user-profile'
})
// Bad
@Component({
selector: 'userProfile'
})
- Class Names: Use PascalCase and descriptive suffixes:
// Good
export class UserProfileComponent { }
export class AuthenticationService { }
export class UserModel { }
// Bad
export class UserProfile { }
export class Authentication { }
Component Best Practices
Component Structure
Components should follow the "single responsibility principle" and have a clear structure:
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.scss']
})
export class UserListComponent implements OnInit, OnDestroy {
// Properties (public first, private next)
public users: User[] = [];
private subscription: Subscription = new Subscription();
// Constructor (for dependency injection only)
constructor(private userService: UserService) { }
// Lifecycle hooks
ngOnInit(): void {
this.loadUsers();
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
// Public methods
public refreshUsers(): void {
this.loadUsers();
}
// Private methods
private loadUsers(): void {
this.subscription.add(
this.userService.getUsers().subscribe(
(users) => {
this.users = users;
}
)
);
}
}
Component Communication
- Inputs and Outputs: Use
@Input()
and@Output()
for parent-child communication:
// Child component
@Component({
selector: 'app-user-card',
template: `
<div class="card" (click)="onSelect()">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
`
})
export class UserCardComponent {
@Input() user!: User;
@Output() selected = new EventEmitter<User>();
onSelect(): void {
this.selected.emit(this.user);
}
}
// Parent component template
<app-user-card
*ngFor="let user of users"
[user]="user"
(selected)="handleUserSelected($event)">
</app-user-card>
- Services for Unrelated Components: Use services with observables for communication between unrelated components:
// Notification service
@Injectable({
providedIn: 'root'
})
export class NotificationService {
private notificationSource = new Subject<string>();
notifications$ = this.notificationSource.asObservable();
sendNotification(message: string): void {
this.notificationSource.next(message);
}
}
// Component sending notification
@Component({ ... })
export class UserActionsComponent {
constructor(private notificationService: NotificationService) {}
addUser(): void {
// Add user logic
this.notificationService.sendNotification('User added successfully');
}
}
// Component receiving notification
@Component({ ... })
export class NotificationsComponent implements OnInit, OnDestroy {
messages: string[] = [];
private subscription = new Subscription();
constructor(private notificationService: NotificationService) {}
ngOnInit(): void {
this.subscription.add(
this.notificationService.notifications$.subscribe(message => {
this.messages.push(message);
})
);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
Template Best Practices
Keep Templates Clean
- Use Structural Directives Properly:
<!-- Good -->
<div *ngIf="users.length > 0; else noUsers">
<app-user-item *ngFor="let user of users; trackBy: trackByUserId" [user]="user"></app-user-item>
</div>
<ng-template #noUsers>
<p>No users found</p>
</ng-template>
<!-- Bad -->
<div *ngIf="users.length > 0">
<app-user-item *ngFor="let user of users" [user]="user"></app-user-item>
</div>
<div *ngIf="users.length === 0">
<p>No users found</p>
</div>
- *Implement trackBy with ngFor to improve performance:
@Component({
selector: 'app-user-list',
template: `
<app-user-item
*ngFor="let user of users; trackBy: trackByUserId"
[user]="user">
</app-user-item>
`
})
export class UserListComponent {
users: User[] = [];
trackByUserId(index: number, user: User): number {
return user.id;
}
}
- Move Complex Logic to Component Class:
<!-- Good -->
<button [disabled]="!canSubmitForm()">Submit</button>
<!-- Bad -->
<button [disabled]="!form.valid || isSubmitting || !hasChanges">Submit</button>
Services and Dependency Injection
Service Organization
- Create Feature Services: Group related functionality:
@Injectable({
providedIn: 'root'
})
export class UserService {
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users');
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
createUser(user: User): Observable<User> {
return this.http.post<User>('/api/users', user);
}
}
- Proper Error Handling in Services:
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(
private http: HttpClient,
private errorService: ErrorService
) {}
fetchData(): Observable<any> {
return this.http.get<any>('/api/data').pipe(
catchError(error => {
this.errorService.handleError('Failed to fetch data', error);
return throwError(() => error);
})
);
}
}
RxJS and Observable Patterns
Best Practices for Observable Usage
- Always Unsubscribe: Use
ngOnDestroy
to prevent memory leaks:
@Component({ ... })
export class DataComponent implements OnInit, OnDestroy {
data: any;
private subscription = new Subscription();
constructor(private dataService: DataService) {}
ngOnInit(): void {
this.subscription.add(
this.dataService.getData().subscribe(result => {
this.data = result;
})
);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
- Use Async Pipe When Possible: It automatically handles subscriptions and unsubscriptions:
@Component({
selector: 'app-user-list',
template: `
<div *ngIf="users$ | async as users; else loading">
<app-user-item *ngFor="let user of users" [user]="user"></app-user-item>
</div>
<ng-template #loading>Loading users...</ng-template>
`
})
export class UserListComponent {
users$: Observable<User[]>;
constructor(private userService: UserService) {
this.users$ = this.userService.getUsers();
}
}
- Use Appropriate Operators:
searchUsers(term: string): Observable<User[]> {
return this.http.get<User[]>('/api/users').pipe(
// Wait until user stops typing for 300ms
debounceTime(300),
// Only emit if the search term changed
distinctUntilChanged(),
// Filter users by search term
map(users => users.filter(user =>
user.name.toLowerCase().includes(term.toLowerCase())
)),
// Handle errors without breaking the observable
catchError(() => of([]))
);
}
Type Safety
Using TypeScript Effectively
- Define Interfaces for Models:
// models/user.interface.ts
export interface User {
id: number;
name: string;
email: string;
role: UserRole;
createdAt: Date;
}
export enum UserRole {
Admin = 'admin',
Editor = 'editor',
Viewer = 'viewer'
}
- Avoid
any
Type: Be specific with types:
// Bad
function processUser(user: any): any {
return { ...user, lastActive: new Date() };
}
// Good
function processUser(user: User): User {
return { ...user, lastActive: new Date() };
}
- Use Type Guards:
function isAdmin(user: User): user is User & { role: UserRole.Admin } {
return user.role === UserRole.Admin;
}
function handleUser(user: User): void {
if (isAdmin(user)) {
// TypeScript knows user.role is UserRole.Admin
console.log('Admin user detected:', user.name);
adminActions(user);
} else {
regularUserActions(user);
}
}
Real-World Example: User Management Module
Let's put these standards together in a real-world example of a user management module:
// models/user.interface.ts
export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
active: boolean;
}
// services/user.service.ts
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://api.example.com/users';
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl).pipe(
catchError(this.handleError('getUsers', []))
);
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/${id}`).pipe(
catchError(this.handleError<User>('getUserById'))
);
}
updateUser(user: User): Observable<User> {
return this.http.put<User>(`${this.apiUrl}/${user.id}`, user).pipe(
catchError(this.handleError<User>('updateUser'))
);
}
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(`${operation} failed: ${error.message}`);
// Return empty result so the application keeps running
return of(result as T);
};
}
}
// components/user-list/user-list.component.ts
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.scss']
})
export class UserListComponent implements OnInit {
users$: Observable<User[]>;
selectedUser: User | null = null;
constructor(private userService: UserService) {
this.users$ = this.userService.getUsers();
}
ngOnInit(): void {}
onUserSelected(user: User): void {
this.selectedUser = user;
}
trackById(index: number, user: User): number {
return user.id;
}
}
// components/user-list/user-list.component.html
<div class="user-list-container">
<h2>User Management</h2>
<div class="users-grid" *ngIf="users$ | async as users; else loading">
<app-user-card
*ngFor="let user of users; trackBy: trackById"
[user]="user"
(selected)="onUserSelected($event)">
</app-user-card>
<p *ngIf="users.length === 0">No users found</p>
</div>
<ng-template #loading>
<app-spinner message="Loading users..."></app-spinner>
</ng-template>
<app-user-details
*ngIf="selectedUser"
[user]="selectedUser">
</app-user-details>
</div>
Summary
Following Angular coding standards is crucial for building maintainable, efficient applications. In this guide, we've covered:
- File structure and naming conventions that create consistency
- Component best practices for clean, efficient components
- Template standards for readable, performant templates
- Service patterns for proper dependency injection
- RxJS best practices to manage asynchronous operations
- TypeScript usage to enhance type safety
- A real-world example showcasing these standards in action
Adhering to these standards will help you write cleaner code, avoid common pitfalls, and work more effectively in teams. As you grow more comfortable with Angular development, these practices will become second nature.
Additional Resources
Exercises
- Take an existing Angular component and refactor it to follow the coding standards described in this guide.
- Create a new service that follows proper error handling and Observable patterns.
- Implement proper unsubscribe patterns in a component with multiple Observable subscriptions.
- Define interfaces for your data models and update your components to use them.
- Configure ESLint in your project with Angular-specific rules to automatically enforce these standards.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)