- Introduction
- Project Structure
- Component Architecture
- Modules
- Services and Dependency Injection
- State Management
- Routing
- Forms
- HTTP and API Communication
- RxJS Usage
- Performance Optimization
- Testing
- Styling
- Internationalization
- Accessibility
- Security Best Practices
- Build and Deployment
- Documentation
This document outlines the standard conventions and best practices for Angular application development at Bayat. These guidelines aim to ensure consistency, maintainability, and scalability across all Angular projects.
src/
├── app/
│ ├── core/ # Singleton services, app-level components, guards, interceptors
│ │ ├── guards/
│ │ ├── interceptors/
│ │ ├── services/
│ │ └── core.module.ts
│ ├── features/ # Feature modules
│ │ ├── feature-a/
│ │ │ ├── components/
│ │ │ ├── services/
│ │ │ ├── models/
│ │ │ └── feature-a.module.ts
│ │ └── feature-b/
│ ├── shared/ # Shared components, directives, pipes
│ │ ├── components/
│ │ ├── directives/
│ │ ├── pipes/
│ │ └── shared.module.ts
│ ├── app-routing.module.ts
│ ├── app.component.ts
│ └── app.module.ts
├── assets/ # Static assets
├── environments/ # Environment configurations
└── styles/ # Global styles
- Files: Use kebab-case for all files
- Example:
user-profile.component.ts
- Example:
- Classes: Use PascalCase for class names
- Example:
UserProfileComponent
- Example:
- Methods and Properties: Use camelCase
- Example:
getUserData()
- Example:
- Constants: Use UPPER_SNAKE_CASE for constants
- Example:
MAX_RETRY_COUNT
- Example:
- Interfaces: Use PascalCase with 'I' prefix
- Example:
IUserProfile
- Example:
- Enums: Use PascalCase
- Example:
UserRole
- Example:
Follow the Angular style guide for file naming:
feature-name.component.ts
feature-name.service.ts
feature-name.module.ts
feature-name.directive.ts
feature-name.pipe.ts
feature-name.guard.ts
feature-name.interceptor.ts
feature-name.model.ts
- Follow the single responsibility principle
- Create smart (container) and presentational (dumb) components
- Keep components small and focused
- Limit component files to 400 lines of code
- Extract reusable logic into services
@Component({
selector: 'app-feature-name',
templateUrl: './feature-name.component.html',
styleUrls: ['./feature-name.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
- Use
OnPush
change detection strategy by default - Use template and style URLs instead of inline templates and styles for components with significant content
- Prefix component selectors with 'app-' or a project-specific prefix
- Implement interfaces for lifecycle hooks explicitly
- Clean up subscriptions in
ngOnDestroy
- Initialize data in
ngOnInit
, not in the constructor - Use appropriate lifecycle hooks for different tasks
export class FeatureComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
constructor(private dataService: DataService) {}
ngOnInit(): void {
this.dataService.getData()
.pipe(takeUntil(this.destroy$))
.subscribe(data => {
// Handle data
});
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
- Create feature modules for distinct functional areas
- Use shared modules for reusable components, directives, and pipes
- Use core module for singleton services and app-level components
- Lazy load feature modules when possible
@NgModule({
declarations: [
FeatureComponent,
FeatureListComponent,
FeatureDetailComponent
],
imports: [
CommonModule,
SharedModule,
FeatureRoutingModule
],
providers: [
FeatureService
]
})
export class FeatureModule { }
- Only declare components, directives, and pipes that belong to the module
- Import only what is needed
- Export components, directives, and pipes that should be used by other modules
- Create services for reusable business logic and data access
- Use the
@Injectable({ providedIn: 'root' })
syntax for app-wide singleton services - Provide feature-specific services in their respective feature modules
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private http: HttpClient) { }
getData(): Observable<Data[]> {
return this.http.get<Data[]>('/api/data');
}
}
- Use constructor injection for dependencies
- Keep the number of dependencies manageable (< 7)
- Use interfaces with injection tokens for better abstraction
- Consider using factory providers for complex service creation
- Use component properties for local state
- Use
BehaviorSubject
orRxJS
streams for more complex local state
For small to medium applications:
- Use services with
BehaviorSubject
for simple global state
For larger applications:
- Use NgRx for complex state management
- Follow the Redux pattern (actions, reducers, selectors, effects)
- Use the facade pattern to abstract state management from components
- Create a clear state interface for each feature
- Use strongly typed actions
- Keep reducers pure and simple
- Use selectors for derived state
- Handle side effects in Effects classes
- Consider using the Entity pattern for collections
- Use lazy loading for feature modules
- Implement route guards for protected routes
- Use route resolvers for pre-fetching data
- Define clear route parameters and query parameters
const routes: Routes = [
{
path: 'features',
loadChildren: () => import('./features/feature.module').then(m => m.FeatureModule),
canActivate: [AuthGuard]
},
{
path: 'feature/:id',
component: FeatureDetailComponent,
resolve: {
feature: FeatureResolver
}
},
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
- Use the Router service for programmatic navigation
- Use routerLink directive for template-based navigation
- Pass state using route parameters, query parameters, or the router state
- Use Reactive Forms for complex forms (preferred)
- Use Template-driven forms only for very simple forms
- Create form models in the component class
- Use form builders to create form groups
- Implement custom validators for complex validation
- Use form arrays for dynamic form elements
export class UserFormComponent implements OnInit {
userForm: FormGroup;
constructor(private fb: FormBuilder) { }
ngOnInit(): void {
this.userForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
address: this.fb.group({
street: ['', Validators.required],
city: ['', Validators.required],
zipCode: ['', [Validators.required, zipCodeValidator]]
})
});
}
onSubmit(): void {
if (this.userForm.valid) {
// Process form data
}
}
}
- Display validation messages clearly
- Use consistent validation styling
- Implement cross-field validation when needed
- Disable submit buttons for invalid forms
- Create dedicated service classes for API communication
- Return Observables from service methods
- Handle errors appropriately
- Use HTTP interceptors for common tasks (auth, logging, etc.)
@Injectable({
providedIn: 'root'
})
export class ApiService {
private apiUrl = environment.apiUrl;
constructor(private http: HttpClient) { }
getItems(): Observable<Item[]> {
return this.http.get<Item[]>(`${this.apiUrl}/items`)
.pipe(
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
// Log error and transform to user-friendly message
return throwError(() => new Error('An error occurred. Please try again later.'));
}
}
- Define interfaces for all API models
- Use type guards for runtime type checking
- Consider using class-transformer for complex transformations
- Use appropriate operators for different tasks
- Avoid nested subscriptions
- Use the async pipe in templates when possible
- Always unsubscribe from subscriptions
- Use
shareReplay()
for sharing HTTP responses
- Use
combineLatest
for combining multiple streams - Use
switchMap
for dependent HTTP requests - Use
forkJoin
for parallel HTTP requests - Use
Subject
orBehaviorSubject
for event buses
// Good example
this.route.paramMap.pipe(
map(params => params.get('id')),
filter(id => !!id),
switchMap(id => this.userService.getUser(id)),
takeUntil(this.destroy$)
).subscribe(user => {
this.user = user;
});
- Use OnPush change detection strategy
- Avoid direct DOM manipulation
- Use pure pipes for transformations in templates
- Implement trackBy functions for ngFor directives
<div *ngFor="let item of items; trackBy: trackByFn">
{{ item.name }}
</div>
// In component
trackByFn(index: number, item: any): number {
return item.id;
}
- Lazy load feature modules
- Use dynamic imports for large libraries
- Implement virtual scrolling for long lists
- Use image lazy loading for media-heavy pages
- Configure proper production builds
- Use tree-shakable providers
- Analyze bundle size regularly
- Consider using web workers for CPU-intensive tasks
- Test components, services, pipes, and directives
- Use TestBed for Angular-specific testing
- Mock dependencies appropriately
- Focus on behavior, not implementation details
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
it('should retrieve users', () => {
const mockUsers = [{ id: 1, name: 'John' }];
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
const req = httpMock.expectOne(`${environment.apiUrl}/users`);
expect(req.request.method).toBe('GET');
req.flush(mockUsers);
});
});
- Test component interactions
- Test routing and navigation
- Test form validation and submission
- Use page objects for complex component testing
- Use Cypress or Protractor for E2E testing
- Focus on critical user flows
- Test across different viewports
- Implement stable selectors for elements
- Use SCSS for styling
- Implement a consistent naming convention (BEM recommended)
- Create reusable mixins and functions
- Use variables for colors, spacing, and typography
- Use component-specific styles with encapsulation
- Create a consistent theme system
- Implement responsive designs with media queries
- Use Angular Material or custom design system components
// Example SCSS with BEM
.user-card {
display: flex;
padding: 1rem;
&__avatar {
width: 50px;
height: 50px;
border-radius: 50%;
}
&__info {
margin-left: 1rem;
}
&--highlighted {
background-color: var(--highlight-color);
}
}
- Use Angular i18n or ngx-translate for translations
- Externalize all user-facing strings
- Support right-to-left languages when needed
- Format dates, numbers, and currencies according to locale
// Using ngx-translate
<h1>{{ 'WELCOME.TITLE' | translate }}</h1>
<p>{{ 'WELCOME.TEXT' | translate:{ name: username } }}</p>
- Follow WCAG 2.1 AA standards
- Use semantic HTML elements
- Implement proper ARIA attributes
- Ensure keyboard navigation works
- Test with screen readers
- Support high contrast modes
- Sanitize user input
- Protect against XSS attacks
- Implement proper authentication and authorization
- Use HTTPS for all API communication
- Follow the principle of least privilege
- Keep dependencies updated
// Using Angular's built-in sanitization
import { DomSanitizer } from '@angular/platform-browser';
constructor(private sanitizer: DomSanitizer) { }
// In component
safeHtml = this.sanitizer.bypassSecurityTrustHtml(userProvidedHtml);
- Use environment-specific configuration files
- Optimize production builds
- Implement proper source maps for debugging
- Configure bundle analysis
- Implement CI/CD pipelines
- Use containerization when appropriate
- Configure proper caching strategies
- Implement feature flags for controlled rollouts
- Use JSDoc comments for public APIs
- Document complex logic and algorithms
- Create README files for modules and features
- Maintain up-to-date architecture diagrams
/**
* Retrieves user data from the API
* @param id The unique identifier of the user
* @returns An Observable that emits the user data
* @throws An error if the user is not found
*/
getUser(id: string): Observable<User> {
return this.http.get<User>(`${this.apiUrl}/users/${id}`);
}
- Maintain up-to-date README files
- Document environment setup procedures
- Create onboarding guides for new developers
- Document architectural decisions and patterns
Version | Date | Description |
---|---|---|
1.0 | 2025-03-20 | Initial version |