Skip to main content

Overview

The Happy Habitat frontend uses Angular’s HttpClient for all API communications. The application implements a centralized HTTP client architecture with a chain of interceptors for authentication, logging, and error handling.

HTTP Client Configuration

Setup

The HTTP client is configured in app.config.ts with functional interceptors:
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './interceptors/auth.interceptor';
import { loggingInterceptor } from './interceptors/logging.interceptor';
import { errorInterceptor } from './interceptors/error.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([loggingInterceptor, errorInterceptor, authInterceptor])
    )
  ]
};

API Base URL

The API base URL is configured in environment files:
// environment.ts
export const environment = {
  production: false,
  apiUrl: 'http://localhost:5080/api',
  apiVersion: 'v1'
};
Services construct their endpoints using this base URL:
private readonly API_URL = `${environment.apiUrl}/users`;

Interceptors

Execution Order

Interceptors execute in the following order:
  1. Logging Interceptor - Logs outgoing requests and incoming responses
  2. Error Interceptor - Handles HTTP errors and displays notifications
  3. Auth Interceptor - Adds authentication tokens to requests

Logging Interceptor

Location: app/interceptors/logging.interceptor.ts:1 Logs all HTTP requests and responses with detailed metadata: Features:
  • Request logging with method, URL, and sanitized headers
  • Response logging with status, duration, and size
  • Error logging with full error details
  • Slow request detection (warns if duration > 5 seconds)
  • Sensitive header redaction (Authorization, Cookie, etc.)
Example Log Output:
[INFO] HTTP GET /api/users - 200 (342ms)
[WARN] Slow HTTP request: GET /api/residents took 5234ms

Error Interceptor

Location: app/interceptors/error.interceptor.ts:1 Centralizes HTTP error handling and user notifications: Status Code Handling:
Status CodeTypeAction
400ValidationShow notification with validation errors
401UnauthorizedRedirect to login (if not already there)
403ForbiddenShow permission denied notification
404Not FoundLog silently (no notification)
422ValidationShow validation error notification
429Rate LimitShow rate limiting notification
500+Server ErrorShow server error notification
Error Flow:
HTTP RequestErrorNormalize ErrorHandle by StatusShow NotificationLogPropagate

Auth Interceptor

Location: app/interceptors/auth.interceptor.ts:1 Manages authentication tokens and automatic token refresh: Features:
  • Automatic token injection into request headers
  • Automatic token refresh on 401 responses
  • Request retry after token refresh
  • Graceful logout on refresh failure
Token Injection:
if (token) {
  req = req.clone({
    setHeaders: {
      Authorization: `Bearer ${token}`
    }
  });
}
Token Refresh Flow:
401 ErrorCheck Refresh TokenRefresh TokenRetry Original RequestSuccess
FailureLogout

Service Architecture

Base Service Pattern

All API services follow a consistent pattern:
@Injectable({
  providedIn: 'root'
})
export class UsersService {
  private http = inject(HttpClient);
  private logger = inject(LoggerService);
  private errorService = inject(ErrorService);
  
  private readonly API_URL = `${environment.apiUrl}/users`;
  
  getAllUsers(): Observable<UserDto[]> {
    return this.http.get<UserDto[]>(this.API_URL).pipe(
      catchError((error) => {
        this.logger.error('Error fetching users', error, 'UsersService');
        this.errorService.handleError(error);
        return throwError(() => error);
      })
    );
  }
}

Common Patterns

Query Parameters

let params = new HttpParams();
params = params.set('page', String(page));
params = params.set('pageSize', String(pageSize));

return this.http.get<ResultDto>(url, { params });

Request Body

return this.http.post<UserDto>(this.API_URL, request).pipe(
  catchError((error) => {
    this.logger.error('Error creating user', error, 'UsersService');
    return throwError(() => error);
  })
);

Response Transformation

return this.http.get<ResidentDto[]>(url).pipe(
  map(dtos => mapResidentDtosToResidentes(dtos)),
  catchError(this.handleError)
);

Caching Strategy

Some services implement local caching to reduce API calls:

Cache Implementation

private readonly CACHE_KEY = 'hh_residents_community_';
private readonly CACHE_TTL_MS = 15 * 60 * 1000; // 15 minutes

private getFromCache(key: string): Data[] | null {
  const raw = localStorage.getItem(key);
  if (!raw) return null;
  
  const parsed = JSON.parse(raw) as { data: Data[]; savedAt: number };
  const expired = Date.now() - parsed.savedAt > this.CACHE_TTL_MS;
  
  return expired ? null : parsed.data;
}

private setCache(key: string, data: Data[]): void {
  localStorage.setItem(key, JSON.stringify({ 
    data, 
    savedAt: Date.now() 
  }));
}

Cache Invalidation

invalidateCache(): void {
  const keysToRemove: string[] = [];
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    if (key?.startsWith(this.CACHE_PREFIX)) {
      keysToRemove.push(key);
    }
  }
  keysToRemove.forEach(k => localStorage.removeItem(k));
}
Cache is invalidated after mutations (create, update, delete operations).

Error Handling

Service-Level Error Handling

Each service handles errors in a standard way:
catchError((error) => {
  this.logger.error('Operation failed', error, 'ServiceName');
  this.errorService.handleError(error);
  return throwError(() => error);
})

Error Service Integration

The ErrorService normalizes and handles errors:
  • Normalizes different error types (HTTP, JavaScript, Generic)
  • Logs errors with appropriate severity
  • Shows user-friendly notifications
  • Tracks active errors
  • Supports custom error handlers
The error service is defined in src/app/services/error.service.ts and handles global error logging and display.

Best Practices

1. Use RxJS Operators

// Good
return this.http.get<Data[]>(url).pipe(
  map(data => transform(data)),
  tap(data => this.cache(data)),
  catchError(this.handleError)
);

// Avoid
this.http.get<Data[]>(url).subscribe(data => {
  const transformed = transform(data);
  this.cache(transformed);
});

2. Consistent Error Handling

// Always catch errors at service level
catchError((error) => {
  this.logger.error('Description', error, 'Context');
  this.errorService.handleError(error);
  return throwError(() => error);
})

3. Type Safety

// Use TypeScript interfaces for request/response
interface CreateUserRequest {
  firstName: string;
  lastName: string;
  email: string;
}

createUser(request: CreateUserRequest): Observable<UserDto> {
  return this.http.post<UserDto>(this.API_URL, request);
}

4. Logging

// Log significant operations
this.logger.info('Creating user', 'UsersService', { username: request.username });
this.logger.debug('Fetching users', 'UsersService', { includeInactive });

Build docs developers (and LLMs) love