Skip to content

HTTP Client and API Communication

Course Objective

Master Angular's HTTP client for consuming REST APIs, handling responses, implementing error handling, and managing HTTP requests effectively in real-world applications.

1. Introduction to Angular HTTP Client

Angular's HTTP client is built on top of the Fetch API and provides a powerful, type-safe way to communicate with backend services. It returns Observables, making it perfect for handling asynchronous operations.

Key Features

  • Type-safe HTTP requests
  • Request and response interceptors
  • Automatic JSON parsing
  • Error handling
  • Request cancellation
  • Progress monitoring

2. Setting Up HTTP Client

Modern Setup (Standalone Components)

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withInterceptorsFromDi()),
    // other providers
  ]
});

Traditional Module Setup

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

3. Making HTTP Requests

Basic HTTP Service

// services/api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface User {
  id: number;
  name: string;
  email: string;
  created_at: string;
}

export interface CreateUserRequest {
  name: string;
  email: string;
}

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private readonly baseUrl = 'https://api.example.com';

  constructor(private http: HttpClient) {}

  // GET request
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(`${this.baseUrl}/users`);
  }

  // GET with parameters
  getUserById(id: number): Observable<User> {
    return this.http.get<User>(`${this.baseUrl}/users/${id}`);
  }

  // GET with query parameters
  searchUsers(query: string, page: number = 1): Observable<User[]> {
    const params = new HttpParams()
      .set('q', query)
      .set('page', page.toString());

    return this.http.get<User[]>(`${this.baseUrl}/users/search`, { params });
  }

  // POST request
  createUser(userData: CreateUserRequest): Observable<User> {
    const headers = new HttpHeaders({
      'Content-Type': 'application/json'
    });

    return this.http.post<User>(`${this.baseUrl}/users`, userData, { headers });
  }

  // PUT request
  updateUser(id: number, userData: Partial<CreateUserRequest>): Observable<User> {
    return this.http.put<User>(`${this.baseUrl}/users/${id}`, userData);
  }

  // DELETE request
  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.baseUrl}/users/${id}`);
  }

  // Custom headers
  getProtectedData(): Observable<any> {
    const headers = new HttpHeaders({
      'Authorization': 'Bearer ' + this.getAuthToken(),
      'Content-Type': 'application/json'
    });

    return this.http.get<any>(`${this.baseUrl}/protected`, { headers });
  }

  private getAuthToken(): string {
    return localStorage.getItem('authToken') || '';
  }
}

Using the Service in Components

// components/user-list.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ApiService, User } from '../services/api.service';
import { Subject } from 'rxjs';
import { takeUntil, finalize } from 'rxjs/operators';

@Component({
  selector: 'app-user-list',
  template: `
    <div class="user-list">
      <h2>Users</h2>

      <div *ngIf="loading" class="loading">Loading users...</div>

      <div *ngIf="error" class="error">
        Error: {{ error }}
        <button (click)="retry()">Retry</button>
      </div>

      <div *ngIf="users.length > 0" class="users">
        <div *ngFor="let user of users" class="user-card">
          <h3>{{ user.name }}</h3>
          <p>{{ user.email }}</p>
          <button (click)="deleteUser(user.id)">Delete</button>
        </div>
      </div>

      <button (click)="loadUsers()" [disabled]="loading">
        Refresh
      </button>
    </div>
  `,
  styles: [`
    .user-list { padding: 20px; }
    .user-card {
      border: 1px solid #ddd;
      padding: 15px;
      margin: 10px 0;
      border-radius: 4px;
    }
    .loading, .error {
      padding: 15px;
      border-radius: 4px;
    }
    .loading { background-color: #e3f2fd; }
    .error { background-color: #ffebee; color: #c62828; }
  `]
})
export class UserListComponent implements OnInit, OnDestroy {
  users: User[] = [];
  loading = false;
  error: string | null = null;
  private destroy$ = new Subject<void>();

  constructor(private apiService: ApiService) {}

  ngOnInit(): void {
    this.loadUsers();
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  loadUsers(): void {
    this.loading = true;
    this.error = null;

    this.apiService.getUsers()
      .pipe(
        takeUntil(this.destroy$),
        finalize(() => this.loading = false)
      )
      .subscribe({
        next: (users) => {
          this.users = users;
        },
        error: (error) => {
          this.error = 'Failed to load users';
          console.error('Error loading users:', error);
        }
      });
  }

  deleteUser(id: number): void {
    if (confirm('Are you sure you want to delete this user?')) {
      this.apiService.deleteUser(id)
        .pipe(takeUntil(this.destroy$))
        .subscribe({
          next: () => {
            this.users = this.users.filter(user => user.id !== id);
          },
          error: (error) => {
            this.error = 'Failed to delete user';
            console.error('Error deleting user:', error);
          }
        });
    }
  }

  retry(): void {
    this.loadUsers();
  }
}

4. Handling HTTP Responses

Response Types

// services/advanced-api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse, HttpEventType } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, filter } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class AdvancedApiService {
  constructor(private http: HttpClient) {}

  // Get full response (including headers)
  getUserWithHeaders(id: number): Observable<HttpResponse<User>> {
    return this.http.get<User>(`/api/users/${id}`, {
      observe: 'response'
    });
  }

  // Get response body only (default)
  getUser(id: number): Observable<User> {
    return this.http.get<User>(`/api/users/${id}`);
  }

  // Get events (for progress monitoring)
  uploadFile(file: File): Observable<any> {
    const formData = new FormData();
    formData.append('file', file);

    return this.http.post('/api/upload', formData, {
      observe: 'events',
      reportProgress: true
    }).pipe(
      map(event => {
        switch (event.type) {
          case HttpEventType.UploadProgress:
            const progress = Math.round(100 * event.loaded / event.total!);
            return { type: 'progress', progress };
          case HttpEventType.Response:
            return { type: 'complete', body: event.body };
          default:
            return { type: 'other', event };
        }
      })
    );
  }

  // Working with response headers
  getDataWithEtag(): Observable<{ data: any; etag: string }> {
    return this.http.get<any>('/api/data', { observe: 'response' })
      .pipe(
        map(response => ({
          data: response.body,
          etag: response.headers.get('ETag') || ''
        }))
      );
  }
}

5. Error Handling

Comprehensive Error Handling

// services/error-handling.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError, of, timer } from 'rxjs';
import { catchError, retry, retryWhen, switchMap, take } from 'rxjs/operators';

export interface ApiError {
  message: string;
  status: number;
  timestamp: Date;
}

@Injectable({
  providedIn: 'root'
})
export class ErrorHandlingService {
  constructor(private http: HttpClient) {}

  // Basic error handling
  getDataWithErrorHandling(): Observable<any> {
    return this.http.get('/api/data')
      .pipe(
        catchError(this.handleError)
      );
  }

  // Retry on failure
  getDataWithRetry(): Observable<any> {
    return this.http.get('/api/data')
      .pipe(
        retry(3), // Retry up to 3 times
        catchError(this.handleError)
      );
  }

  // Advanced retry with delay
  getDataWithAdvancedRetry(): Observable<any> {
    return this.http.get('/api/data')
      .pipe(
        retryWhen(errors =>
          errors.pipe(
            switchMap((error, index) => {
              // Retry only for certain errors and limited times
              if (index >= 3 || error.status === 404) {
                return throwError(error);
              }
              // Exponential backoff: 1s, 2s, 4s
              return timer(Math.pow(2, index) * 1000);
            })
          )
        ),
        catchError(this.handleError)
      );
  }

  // Fallback data on error
  getDataWithFallback(): Observable<any> {
    return this.http.get('/api/data')
      .pipe(
        catchError(error => {
          console.warn('API failed, using fallback data:', error);
          return of({ message: 'Fallback data', items: [] });
        })
      );
  }

  private handleError = (error: HttpErrorResponse): Observable<never> => {
    let errorMessage = 'An unknown error occurred';

    if (error.error instanceof ErrorEvent) {
      // Client-side error
      errorMessage = `Client Error: ${error.error.message}`;
    } else {
      // Server-side error
      switch (error.status) {
        case 400:
          errorMessage = 'Bad Request: ' + this.extractErrorMessage(error);
          break;
        case 401:
          errorMessage = 'Unauthorized: Please log in again';
          this.handleUnauthorized();
          break;
        case 403:
          errorMessage = 'Forbidden: You do not have permission';
          break;
        case 404:
          errorMessage = 'Not Found: The requested resource was not found';
          break;
        case 500:
          errorMessage = 'Internal Server Error: Please try again later';
          break;
        case 503:
          errorMessage = 'Service Unavailable: Server is temporarily unavailable';
          break;
        default:
          errorMessage = `Server Error: ${error.status} - ${error.message}`;
      }
    }

    // Log error to console (in production, send to logging service)
    console.error('HTTP Error:', {
      message: errorMessage,
      status: error.status,
      url: error.url,
      error: error.error
    });

    return throwError({
      message: errorMessage,
      status: error.status,
      timestamp: new Date()
    } as ApiError);
  }

  private extractErrorMessage(error: HttpErrorResponse): string {
    if (error.error?.message) {
      return error.error.message;
    }
    if (error.error?.error) {
      return error.error.error;
    }
    return error.message;
  }

  private handleUnauthorized(): void {
    // Clear auth token and redirect to login
    localStorage.removeItem('authToken');
    // In a real app, you might use Router to navigate
    // this.router.navigate(['/login']);
  }
}

6. HTTP Interceptors

Authentication Interceptor

// interceptors/auth.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Get auth token
    const authToken = localStorage.getItem('authToken');

    // Clone request and add authorization header
    if (authToken) {
      const authReq = req.clone({
        setHeaders: {
          Authorization: `Bearer ${authToken}`
        }
      });
      return next.handle(authReq);
    }

    return next.handle(req);
  }
}

Loading Interceptor

// interceptors/loading.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { LoadingService } from '../services/loading.service';

@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
  constructor(private loadingService: LoadingService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    // Show loading indicator
    this.loadingService.show();

    return next.handle(req).pipe(
      finalize(() => {
        // Hide loading indicator when request completes
        this.loadingService.hide();
      })
    );
  }
}

Error Interceptor

// interceptors/error.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { NotificationService } from '../services/notification.service';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  constructor(private notificationService: NotificationService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        // Show user-friendly error messages
        if (error.status >= 400 && error.status < 500) {
          this.notificationService.showError('Client error occurred');
        } else if (error.status >= 500) {
          this.notificationService.showError('Server error occurred');
        }

        return throwError(error);
      })
    );
  }
}

Registering Interceptors

// Modern setup (main.ts)
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AuthInterceptor, LoadingInterceptor, ErrorInterceptor } from './interceptors';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([AuthInterceptor, LoadingInterceptor, ErrorInterceptor])
    )
  ]
});

// Traditional setup (app.module.ts)
import { HTTP_INTERCEPTORS } from '@angular/common/http';

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: LoadingInterceptor,
      multi: true
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: ErrorInterceptor,
      multi: true
    }
  ]
})
export class AppModule {}

7. Request and Response Transformation

Custom HTTP Service with Transformation

// services/transformed-api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';

export interface ApiUser {
  id: number;
  first_name: string;
  last_name: string;
  email_address: string;
  created_timestamp: string;
}

export interface User {
  id: number;
  fullName: string;
  email: string;
  createdAt: Date;
}

@Injectable({
  providedIn: 'root'
})
export class TransformedApiService {
  constructor(private http: HttpClient) {}

  // Transform API response to match client model
  getUsers(): Observable<User[]> {
    return this.http.get<ApiUser[]>('/api/users')
      .pipe(
        map(apiUsers => apiUsers.map(this.transformApiUser)),
        tap(users => console.log('Transformed users:', users))
      );
  }

  // Transform single user
  getUser(id: number): Observable<User> {
    return this.http.get<ApiUser>(`/api/users/${id}`)
      .pipe(
        map(this.transformApiUser)
      );
  }

  // Transform client model to API format
  createUser(user: Omit<User, 'id' | 'createdAt'>): Observable<User> {
    const apiUser = this.transformToApiUser(user);

    return this.http.post<ApiUser>('/api/users', apiUser)
      .pipe(
        map(this.transformApiUser)
      );
  }

  private transformApiUser = (apiUser: ApiUser): User => ({
    id: apiUser.id,
    fullName: `${apiUser.first_name} ${apiUser.last_name}`,
    email: apiUser.email_address,
    createdAt: new Date(apiUser.created_timestamp)
  });

  private transformToApiUser(user: Partial<User>): Partial<ApiUser> {
    const [firstName, ...lastNameParts] = (user.fullName || '').split(' ');

    return {
      first_name: firstName || '',
      last_name: lastNameParts.join(' ') || '',
      email_address: user.email || ''
    };
  }
}

8. Caching Strategies

HTTP Cache Service

// services/cache.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of, timer } from 'rxjs';
import { tap, shareReplay, switchMap } from 'rxjs/operators';

interface CacheEntry<T> {
  data: T;
  timestamp: number;
  expiry: number;
}

@Injectable({
  providedIn: 'root'
})
export class CacheService {
  private cache = new Map<string, CacheEntry<any>>();
  private readonly DEFAULT_CACHE_TIME = 5 * 60 * 1000; // 5 minutes

  constructor(private http: HttpClient) {}

  // Get data with caching
  get<T>(url: string, cacheTime?: number): Observable<T> {
    const cacheKey = url;
    const cached = this.cache.get(cacheKey);
    const now = Date.now();

    // Return cached data if valid
    if (cached && now < cached.expiry) {
      return of(cached.data);
    }

    // Fetch fresh data
    return this.http.get<T>(url).pipe(
      tap(data => {
        this.cache.set(cacheKey, {
          data,
          timestamp: now,
          expiry: now + (cacheTime || this.DEFAULT_CACHE_TIME)
        });
      }),
      shareReplay(1)
    );
  }

  // Clear specific cache entry
  clearCache(url: string): void {
    this.cache.delete(url);
  }

  // Clear all cache
  clearAllCache(): void {
    this.cache.clear();
  }

  // Auto-refresh cache
  getWithAutoRefresh<T>(url: string, refreshInterval: number): Observable<T> {
    return timer(0, refreshInterval).pipe(
      switchMap(() => {
        this.clearCache(url);
        return this.get<T>(url);
      })
    );
  }
}

9. Testing HTTP Services

Testing HTTP Service

// services/api.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ApiService, User } from './api.service';

describe('ApiService', () => {
  let service: ApiService;
  let httpTestingController: HttpTestingController;
  const baseUrl = 'https://api.example.com';

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [ApiService]
    });

    service = TestBed.inject(ApiService);
    httpTestingController = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    // Verify that no unmatched requests are outstanding
    httpTestingController.verify();
  });

  describe('getUsers', () => {
    it('should fetch users', () => {
      const mockUsers: User[] = [
        { id: 1, name: 'John Doe', email: 'john@example.com', created_at: '2023-01-01' },
        { id: 2, name: 'Jane Smith', email: 'jane@example.com', created_at: '2023-01-02' }
      ];

      service.getUsers().subscribe(users => {
        expect(users).toEqual(mockUsers);
      });

      const req = httpTestingController.expectOne(`${baseUrl}/users`);
      expect(req.request.method).toBe('GET');
      req.flush(mockUsers);
    });
  });

  describe('createUser', () => {
    it('should create a user', () => {
      const newUser = { name: 'New User', email: 'new@example.com' };
      const createdUser: User = {
        id: 3,
        ...newUser,
        created_at: '2023-01-03'
      };

      service.createUser(newUser).subscribe(user => {
        expect(user).toEqual(createdUser);
      });

      const req = httpTestingController.expectOne(`${baseUrl}/users`);
      expect(req.request.method).toBe('POST');
      expect(req.request.body).toEqual(newUser);
      expect(req.request.headers.get('Content-Type')).toBe('application/json');
      req.flush(createdUser);
    });
  });

  describe('error handling', () => {
    it('should handle HTTP errors', () => {
      const errorMessage = 'Server error';

      service.getUsers().subscribe({
        next: () => fail('Expected error'),
        error: (error) => {
          expect(error.status).toBe(500);
        }
      });

      const req = httpTestingController.expectOne(`${baseUrl}/users`);
      req.flush(errorMessage, { status: 500, statusText: 'Internal Server Error' });
    });
  });
});

Testing Interceptors

// interceptors/auth.interceptor.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http';
import { AuthInterceptor } from './auth.interceptor';

describe('AuthInterceptor', () => {
  let httpClient: HttpClient;
  let httpTestingController: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        {
          provide: HTTP_INTERCEPTORS,
          useClass: AuthInterceptor,
          multi: true
        }
      ]
    });

    httpClient = TestBed.inject(HttpClient);
    httpTestingController = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpTestingController.verify();
  });

  it('should add authorization header when token exists', () => {
    const token = 'test-token';
    spyOn(localStorage, 'getItem').and.returnValue(token);

    httpClient.get('/api/data').subscribe();

    const req = httpTestingController.expectOne('/api/data');
    expect(req.request.headers.get('Authorization')).toBe(`Bearer ${token}`);
    req.flush({});
  });

  it('should not add authorization header when token does not exist', () => {
    spyOn(localStorage, 'getItem').and.returnValue(null);

    httpClient.get('/api/data').subscribe();

    const req = httpTestingController.expectOne('/api/data');
    expect(req.request.headers.has('Authorization')).toBeFalsy();
    req.flush({});
  });
});

10. Best Practices

1. Service Organization

// Good: Organized API service with clear structure
@Injectable({
  providedIn: 'root'
})
export class UserApiService {
  private readonly baseUrl = environment.apiUrl + '/users';

  constructor(private http: HttpClient) {}

  // Group related methods
  getAll(): Observable<User[]> { /* ... */ }
  getById(id: number): Observable<User> { /* ... */ }
  create(user: CreateUserRequest): Observable<User> { /* ... */ }
  update(id: number, user: UpdateUserRequest): Observable<User> { /* ... */ }
  delete(id: number): Observable<void> { /* ... */ }
}

2. Type Safety

// Always use TypeScript interfaces for requests and responses
export interface ApiResponse<T> {
  data: T;
  message: string;
  success: boolean;
}

export interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
}

// Use generic methods for reusability
class BaseApiService {
  protected get<T>(url: string): Observable<T> {
    return this.http.get<T>(url);
  }

  protected post<T>(url: string, body: any): Observable<T> {
    return this.http.post<T>(url, body);
  }
}

3. Error Handling Strategy

// Consistent error handling across services
abstract class BaseService {
  protected handleError<T>(operation = 'operation', result?: T) {
    return (error: any): Observable<T> => {
      console.error(`${operation} failed:`, error);

      // Let the app keep running by returning an empty result
      return of(result as T);
    };
  }
}

4. Performance Optimization

// Use shareReplay to avoid multiple requests
@Injectable({
  providedIn: 'root'
})
export class ConfigService {
  private config$ = this.http.get<Config>('/api/config').pipe(
    shareReplay(1) // Cache the result and share with all subscribers
  );

  getConfig(): Observable<Config> {
    return this.config$;
  }
}

5. Request Cancellation

// Component with request cancellation
export class SearchComponent implements OnDestroy {
  private destroy$ = new Subject<void>();
  private searchTerms = new Subject<string>();

  constructor(private apiService: ApiService) {
    this.searchTerms.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap(term =>
        term ? this.apiService.searchUsers(term) : of([])
      ),
      takeUntil(this.destroy$)
    ).subscribe(results => {
      this.searchResults = results;
    });
  }

  search(term: string): void {
    this.searchTerms.next(term);
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Common Pitfalls to Avoid

  1. Not unsubscribing from HTTP requests - Use takeUntil pattern
  2. Making multiple identical requests - Use shareReplay
  3. Not handling errors properly - Always implement error handling
  4. Ignoring loading states - Show loading indicators
  5. Not using TypeScript types - Always type your HTTP responses
  6. Blocking the UI with synchronous operations - Use async patterns
  7. Not testing HTTP services - Use HttpClientTestingModule

Next Steps

After mastering HTTP client and API communication, you'll be ready to dive into:

  • Observables and RxJS: Learn advanced reactive programming patterns
  • State Management: Manage application state effectively
  • Forms: Build complex forms with validation
  • Routing: Implement navigation and route guards

The HTTP client is fundamental to most Angular applications, so practice these patterns until they become second nature!