Angular example of a team style guide with best practices
Why This Guide Matters: Consistency, maintainability, and readability are key to writing code that’s easy to understand and work with—both for you and your team. This guide lays out best practices that will help you create cleaner, faster, and more scalable code. It's a living document, so feel free to add your own insights as we continue to improve together! General Best Practices 1. Take Time to Review and Improve Why? Spend as much time reviewing and refining your code as you did writing it. This helps improve readability, reduces complexity, and catches potential issues before they become problems. Practice: After writing your code, schedule time to review, clean up, and improve it. 2. Consistency Over Complexity Why? Keeping a consistent coding style across the project is more important than adding complex but "clever" solutions. Consistent code is easier to read, understand, and maintain by others. Example: Stick to established patterns, even if there's a more complex solution that might optimize a small part of your code. 3. Use Derived State Why? Derived state allows you to compute values based on existing state without storing redundant data. It ensures your state stays consistent and minimal, reducing complexity by relying on computed values. This approach aligns with the Single Source of Truth principle, ensuring that data is derived from the source state rather than duplicated across your application. Example: Scenario: You have a list of users and you want to derive and display only the users that are currently active. Steps: Create the Source State: Store the list of users in your state using a service or store (like NgRx or a local store). export class UserService { private usersSubject = new BehaviorSubject([ { name: 'John', active: true }, { name: 'Jane', active: false }, { name: 'Mark', active: true }, ]); users$ = this.usersSubject.asObservable(); } Create the Derived State: Instead of storing the active users separately, derive this list from the users$ stream. export class UserComponent { activeUsers$: Observable; constructor(private userService: UserService) { this.activeUsers$ = this.userService.users$.pipe( map(users => users.filter(user => user.active)) ); } } Use the Derived State in the Template: {{ user.name }} Key Points: No Redundant State: You don't need a separate variable or property to store active users; you compute it on the fly from the existing state. Consistency: Changes to the original state automatically reflect in the derived state. Efficiency: You avoid the risk of having stale or inconsistent data by deriving from a single source of truth. When to Use Derived State: Filtering: Filter items (e.g., active/inactive users) based on existing state. Aggregation: Compute summaries (e.g., totals, averages) from arrays of data. Transformation: Convert or map data to a different format based on state. By leveraging derived state, your application will be easier to maintain and more predictable, as you're reducing the amount of state to manage manually! Data Flow 1. Early Return for if Statements Why? It keeps your code clean, reduces unnecessary nesting, and makes the data flow easier to read. Example: if (!user) return; // Rest of the function logic for valid users 2. Favor Pure Functions Why? Pure functions ensure that data flow is consistent and predictable. They always return the same output for the same input and don’t modify anything outside of themselves (no side effects!). This ensures that your data flow remains pure, reliable, and easy to debug. Example: function calculateTotal(price: number, tax: number): number { return price + tax; // No external state is affected. } 3. Avoid switch Statements Why? While switch statements can seem simple, they can become hard to maintain as your application grows. Consider using alternative, more scalable patterns. Alternatives to switch: Factory Pattern Why? The Factory pattern allows you to encapsulate logic and easily extend behavior without modifying existing code. Example: class NotificationFactory { static getNotification(type: string): Notification { const notifications = { email: new EmailNotification(), sms: new SmsNotification(), push: new PushNotification(), }; return notifications[type] || new DefaultNotification(); } } const notification = NotificationFactory.getNotification('email'); notification.send(); Object Map Why? An object map is a simple and readable alternative to switch statements, especially when handling actions or conditions. Example: const actionMap = { admin: () => console.log('Admin Panel'), editor: () => console.log('Editor Panel'), user: () => console.l

Why This Guide Matters:
Consistency, maintainability, and readability are key to writing code that’s easy to understand and work with—both for you and your team. This guide lays out best practices that will help you create cleaner, faster, and more scalable code. It's a living document, so feel free to add your own insights as we continue to improve together!
General Best Practices
1. Take Time to Review and Improve
- Why? Spend as much time reviewing and refining your code as you did writing it. This helps improve readability, reduces complexity, and catches potential issues before they become problems.
- Practice: After writing your code, schedule time to review, clean up, and improve it.
2. Consistency Over Complexity
- Why? Keeping a consistent coding style across the project is more important than adding complex but "clever" solutions. Consistent code is easier to read, understand, and maintain by others.
- Example: Stick to established patterns, even if there's a more complex solution that might optimize a small part of your code.
3. Use Derived State
Why? Derived state allows you to compute values based on existing state without storing redundant data. It ensures your state stays consistent and minimal, reducing complexity by relying on computed values. This approach aligns with the Single Source of Truth principle, ensuring that data is derived from the source state rather than duplicated across your application.
Example:
Scenario: You have a list of users and you want to derive and display only the users that are currently active.
Steps:
-
Create the Source State:
Store the list of users in your state using a service or store (like NgRx or a local store).
export class UserService { private usersSubject = new BehaviorSubject<User[]>([ { name: 'John', active: true }, { name: 'Jane', active: false }, { name: 'Mark', active: true }, ]); users$ = this.usersSubject.asObservable(); }
-
Create the Derived State:
Instead of storing the active users separately, derive this list from theusers$
stream.
export class UserComponent { activeUsers$: Observable<User[]>; constructor(private userService: UserService) { this.activeUsers$ = this.userService.users$.pipe( map(users => users.filter(user => user.active)) ); } }
-
Use the Derived State in the Template:
- *ngFor="let user of activeUsers$ | async"> {{ user.name }}
Key Points:
- No Redundant State: You don't need a separate variable or property to store active users; you compute it on the fly from the existing state.
- Consistency: Changes to the original state automatically reflect in the derived state.
- Efficiency: You avoid the risk of having stale or inconsistent data by deriving from a single source of truth.
When to Use Derived State:
- Filtering: Filter items (e.g., active/inactive users) based on existing state.
- Aggregation: Compute summaries (e.g., totals, averages) from arrays of data.
- Transformation: Convert or map data to a different format based on state.
By leveraging derived state, your application will be easier to maintain and more predictable, as you're reducing the amount of state to manage manually!
Data Flow
1. Early Return for if
Statements
- Why? It keeps your code clean, reduces unnecessary nesting, and makes the data flow easier to read.
-
Example:
if (!user) return; // Rest of the function logic for valid users
2. Favor Pure Functions
- Why? Pure functions ensure that data flow is consistent and predictable. They always return the same output for the same input and don’t modify anything outside of themselves (no side effects!). This ensures that your data flow remains pure, reliable, and easy to debug.
-
Example:
function calculateTotal(price: number, tax: number): number { return price + tax; // No external state is affected. }
3. Avoid switch
Statements
-
Why? While
switch
statements can seem simple, they can become hard to maintain as your application grows. Consider using alternative, more scalable patterns.
Alternatives to switch
:
-
Factory Pattern
- Why? The Factory pattern allows you to encapsulate logic and easily extend behavior without modifying existing code.
-
Example:
class NotificationFactory { static getNotification(type: string): Notification { const notifications = { email: new EmailNotification(), sms: new SmsNotification(), push: new PushNotification(), }; return notifications[type] || new DefaultNotification(); } } const notification = NotificationFactory.getNotification('email'); notification.send();
-
Object Map
-
Why? An object map is a simple and readable alternative to
switch
statements, especially when handling actions or conditions. -
Example:
const actionMap = { admin: () => console.log('Admin Panel'), editor: () => console.log('Editor Panel'), user: () => console.log('User Profile'), }; const userRole = 'admin'; actionMap[userRole]?.() || console.log('Unknown role');
-
Why? An object map is a simple and readable alternative to
-
Strategy Pattern
- Why? The Strategy pattern allows you to encapsulate different algorithms and behaviors, making them interchangeable and easier to maintain.
-
Example:
interface PaymentStrategy { pay(amount: number): void; } class PayPalStrategy implements PaymentStrategy { pay(amount: number) { console.log(`Paying ${amount} using PayPal.`); } } class CreditCardStrategy implements PaymentStrategy { pay(amount: number) { console.log(`Paying ${amount} using Credit Card.`); } } const payment = new PayPalStrategy(); payment.pay(100);
4. Prefer Interfaces Over Classes
- Why? Many of us are taught to use classes first, especially in bootcamps, but in TypeScript, interfaces provide more flexibility and are a better choice for defining the shape of an object. They allow for easier type-checking, can be extended, and make your code more maintainable in the long run. Interfaces only exist at compile time, meaning they don’t add unnecessary weight to your runtime code, unlike classes.
-
Example:
interface User { name: string; age: number; } // Prefer this over: class User { constructor(public name: string, public age: number) {} }
- Where to Put It: This rule belongs in the Naming Conventions section because it deals with the way we define and describe objects in TypeScript. --- ### 5. Large Interfaces Should Be Composed of Smaller Ones
- Why? Large interfaces can become difficult to manage and extend. Breaking them down into smaller, more focused interfaces improves readability and maintainability. This also adheres to the Single Responsibility Principle—each interface should represent a specific aspect of an object, and complex objects can be built from composing these smaller interfaces.
- Example:
interface User {
name: string;
age: number;
}
interface UserProfile extends User {
avatarUrl: string;
}
interface UserSettings {
theme: string;
notificationsEnabled: boolean;
}
interface FullUser extends UserProfile, UserSettings {}
- Where to Put It: This rule should go in the Naming Conventions section as well since it focuses on how we design and structure our interfaces in a clean and scalable way.
Naming Conventions
1. Use Descriptive Variable Names
- Why? Longer, descriptive variable names make it easier to understand the purpose of a variable at a glance.
-
Example:
const currentLoggedInUser = 'John Doe';
2. Use ALL CAPS for Constants
- Why? Constants should be clearly identifiable and stand out as fixed, unchangeable values.
-
Example:
const MAX_USERS = 100;
3. Positive Boolean States
-
Why? Positive boolean names (e.g.,
isEnabled
overisDisabled
) make your code more intuitive and easier to reason about. -
Example:
const isUserActive = true;
Functions
1. Function Length
- Why? Keep your functions small (5 lines or fewer) to improve readability and maintainability. If a function exceeds 10-15 lines, it's likely doing too much and should be broken up into smaller functions.
-
Example:
function getTotalPrice(items: Item[]): number { return items.reduce((total, item) => total + item.price, 0); }
2. Pure Functions
- Why? Pure functions are easier to test, debug, and reason about. They don't modify external states and always return the same output for the same input.
-
Example:
function multiply(a: number, b: number): number { return a * b; }
3. Use Early Returns to Avoid Deep Nesting
- Why? Deeply nested code is harder to read and maintain. Use early returns to exit functions when conditions are met.
-
Example:
function processOrder(order: Order) { if (!order) return; if (!order.isPaid) return; // Proceed with processing the paid order }
Files and Structure
1. Keep Files Short
- Why? Files that are longer than one page can become difficult to navigate and maintain. Aim to break large files into smaller, more focused files.
- Practice: If you find yourself scrolling a lot, consider refactoring the file into smaller modules.
2. Project Folder Layout
- Why? Having a clear, consistent project structure makes it easier to navigate and understand the project at a glance.
Folder Structure:
-
Features Folder
- Houses feature-specific components, views, helpers, and state management files.
- Example structure:
-
views/
: For component templates. -
components/
: For UI components. -
state/
: For state management (actions, reducers, effects). -
helpers/
: Utility functions specific to the feature.
-
-
Shared Folder
- Contains reusable components, directives, and pipes used across multiple features.
-
Sentinels (APIs/Services)
- Handles communication with Firebase and other APIs. If you need data, this is where it comes from.
-
Ark Folder
- This is our shared code library. Everything here should be unit tested and reusable across different parts of the project.
Component Best Practices
1. Keep Components Small
- Why? Components should ideally be less than 300-400 lines of code. This makes them easier to read, maintain, and test.
- Practice: Split complex components into smaller ones, each responsible for a single piece of functionality.
2. Handle Side Effects with RxJS tap()
-
Why? Side effects should be clearly separated from the core logic of a stream. Use
tap()
in RxJS to handle side effects. -
Example:
someObservable$.pipe( tap((value) => console.log('Side effect:', value)) );
3. Use Selectors for Data Access
- Why? Selectors allow efficient, maintainable access to state and help decouple components from state management logic.
- Practice: Use selectors wherever possible to retrieve and manage state.
4. OnPush Change Detection
- Why? OnPush change detection improves performance by updating the component only when its inputs change.
-
Example:
@Component({ selector: 'app-example', changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './example.component.html', }) export class ExampleComponent { }
1️⃣ 5. Components Should Only Handle UI Interactions
Components should be lightweight and focused solely on UI interactions. Any logic related to:
• Forms
• Database interactions
• State management
• Business logic
• APIs and services
• Side effects (e.g., RxJS subscriptions)
…should be delegated to a facade, service, or store.
2️⃣ Use a Facade or Service for Business Logic
Instead of putting logic inside the component, use a facade to handle:
✅ Fetching and modifying data (e.g., calling APIs, interacting with Firebase).
✅ Managing complex state and derived state.
✅ Handling form initialization, validation, and submission.
✅ Orchestrating side effects (e.g., WebSocket subscriptions).
This keeps components stateless, making them easier to test and reuse.
Example:
export class UserFacade {
users$ = this.userService.getUsers();
constructor(private userService: UserService) {}
addUser(user: User) {
return this.userService.createUser(user);
}
}
Component (UI only, delegates logic):
export class UserListComponent {
users$ = this.userFacade.users$;
constructor(private userFacade: UserFacade) {}
addUser(user: User) {
this.userFacade.addUser(user).subscribe();
}
}
3️⃣ Components Should Not Modify Data Directly
❌ Avoid this in the component:
export class UserListComponent {
users: User[] = [];
constructor(private userService: UserService) {
this.userService.getUsers().subscribe(users => (this.users = users));
}
addUser(user: User) {
this.userService.createUser(user).subscribe(() => {
this.userService.getUsers().subscribe(users => (this.users = users));
});
}
}
✅ Instead, use a Facade:
export class UserListComponent {
users$ = this.userFacade.users$;
constructor(private userFacade: UserFacade) {}
addUser(user: User) {
this.userFacade.addUser(user).subscribe();
}
}
This keeps your components clean and UI-focused.
4️⃣ Keep Components Small
Components should be less than 300-400 lines. If a component is growing too large:
• Extract logic into a service or facade.
• Break down UI into smaller, reusable components.
5️⃣ Use Standalone Components
All new components should be standalone:
@Component({
standalone: true,
templateUrl: './user-list.component.html',
imports: [CommonModule, MatTableModule],
})
export class UserListComponent {}
This improves modularity and makes it easier to manage dependencies.
6️⃣ Change Detection: Use OnPush
Always use OnPush change detection to improve performance.
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
})
This ensures components only re-render when inputs change, preventing unnecessary re-renders.
7️⃣ Avoid Business Logic in Templates
❌ Avoid putting logic directly in the template:
*ngIf="users?.length > 0">
✅ Move it to the component:
get hasUsers(): boolean {
return this.users?.length > 0;
}
*ngIf="hasUsers">
This improves readability and maintainability.
By following these component best practices, your Angular app will be faster, more maintainable, and scalable.