Skip to main content

Overview

The Rodando Passenger app uses Angular’s HttpClient to communicate with the backend REST API. All HTTP services follow consistent patterns for error handling, response unwrapping, and authentication.

Service Architecture

Base Patterns

All HTTP services in the app follow these patterns:
  1. Injectable Services: All services use providedIn: 'root' for singleton instances
  2. Environment Configuration: Base URLs come from environment.apiUrl
  3. Observable Returns: All methods return RxJS Observables
  4. Error Normalization: Errors are normalized to ApiError interface
  5. Response Unwrapping: Backend responses are unwrapped from the ApiResponse<T> wrapper

Service Structure

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { environment } from '@/environments/environment';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class ExampleService {
  private readonly http = inject(HttpClient);
  private readonly baseUrl = environment.apiUrl;

  // Methods here...
}

Environment Configuration

HTTP services use environment variables for base URLs:
export const environment = {
  apiUrl: 'https://api.rodando.cu',     // REST API base URL
  wsBase: 'https://api.rodando.cu',     // WebSocket base URL
  mapbox: {
    accessToken: 'pk.xxx...'
  }
};
Location: src/environments/environment.ts

Response Format

Standard API Response

All backend endpoints return a standardized wrapper:
interface ApiResponse<T> {
  success: boolean;        // Operation success status
  message: string;         // Human-readable message
  data: T;                 // Actual response payload
  error?: ApiError;        // Error details (if failed)
  meta?: PaginationMeta;   // Pagination metadata (if applicable)
}

Response Unwrapping

Services automatically unwrap the data field:
getProfile(): Observable<UserProfile> {
  return this.http
    .get<ApiResponse<UserProfile>>(`${this.baseUrl}/users/profile`)
    .pipe(
      map(res => {
        if (!res?.success || !res.data) {
          throw new Error('Profile response malformed');
        }
        return res.data; // Return unwrapped data
      })
    );
}

Error Handling

ApiError Interface

All errors are normalized to this interface:
export interface ApiError {
  status?: number;                      // HTTP status code (e.g., 404, 500)
  message: string;                      // Human-readable error message
  code?: string;                        // Application-specific error code
  validation?: Record<string, string[]>; // Field validation errors
  raw?: any;                            // Raw error response from backend
  url?: string | null;                  // Request URL that failed
}

Error Normalization

The AuthService implements comprehensive error normalization:
private handleErrorAsApiError(err: any, requestUrl?: string) {
  return from(normalizeAnyError(err, requestUrl)).pipe(
    mergeMap((apiErr: ApiError) => throwError(() => apiErr))
  );
}
This handles:
  • Network errors: status: 0, CORS issues, connection failures
  • HTTP errors: 4xx, 5xx status codes with parsed response bodies
  • Validation errors: 422 responses with field-level errors
  • Server errors: HTML responses, malformed JSON
  • Progress events: Failed requests that emit ProgressEvent

Error Handling Example

service.someMethod().subscribe({
  next: (data) => { /* handle success */ },
  error: (err: ApiError) => {
    switch (err.status) {
      case 0:
        console.error('Network error:', err.message);
        break;
      case 401:
        console.error('Unauthorized - please login');
        break;
      case 404:
        console.error('Resource not found:', err.url);
        break;
      case 422:
        // Validation errors
        if (err.validation) {
          Object.entries(err.validation).forEach(([field, errors]) => {
            console.error(`${field}: ${errors.join(', ')}`);
          });
        }
        break;
      case 500:
        console.error('Server error:', err.message);
        break;
      default:
        console.error('Error:', err.message);
        if (err.code) console.error('Code:', err.code);
    }
  }
});

Authentication

For web flows, include withCredentials: true to send HttpOnly cookies:
this.http.post(url, payload, { withCredentials: true })

Mobile Sessions (Token-based)

For mobile flows, the access token is included via HTTP Interceptor:
// Interceptor automatically adds:
// Authorization: Bearer <accessToken>
No additional configuration needed in service methods.

Available HTTP Services

Authentication & User

AuthService

Login, logout, token refresh, user profile

PassengerLocationApiService

Location pings and user profile with location (src/app/core/services/http/passenger-location-api.service.ts)

Mapbox Integration

MapboxDirectionsService

Route calculations and turn-by-turn directions

MapboxPlacesService

Geocoding, reverse geocoding, place search

Common Patterns

With Credentials (Web)

login(payload: LoginPayload): Observable<LoginResponse> {
  return this.http.post<ApiResponse<LoginResponse>>(
    `${this.baseUrl}/auth/login`,
    payload,
    { withCredentials: true } // Include cookies
  ).pipe(
    map(resp => resp.data),
    catchError(err => this.handleErrorAsApiError(err))
  );
}

Query Parameters

import { HttpParams } from '@angular/common/http';

getRoute(origin: LatLng, destination: LatLng): Observable<RouteResult> {
  const params = new HttpParams()
    .set('origin', `${origin.lat},${origin.lng}`)
    .set('destination', `${destination.lat},${destination.lng}`);

  return this.http.get<RouteResult>(`${this.baseUrl}/routes`, { params });
}

Response Validation

me(): Observable<UserProfile> {
  return this.http.get<ApiResponse<UserProfile>>(`${this.baseUrl}/users/profile`)
    .pipe(
      map(res => {
        if (!res?.success || !res.data) {
          throw new Error('Profile response malformed');
        }
        return res.data;
      }),
      catchError(err => this.handleErrorAsApiError(err))
    );
}

Unwrapping Helper

private unwrap<R extends { data: any }, T>(resp: R, url: string): T {
  if (!resp || typeof resp !== 'object' || !('data' in resp)) {
    throw new Error(`Unexpected response shape from ${url}`);
  }
  return resp.data as T;
}

// Usage
login(payload: LoginPayload): Observable<LoginResponse> {
  const url = `${this.baseUrl}/auth/login`;
  return this.http.post<ApiResponse<LoginResponse>>(url, payload).pipe(
    map(resp => this.unwrap<ApiResponse<LoginResponse>, LoginResponse>(resp, url)),
    catchError(err => this.handleErrorAsApiError(err, url))
  );
}

TypeScript Interfaces

ApiResponse

export interface ApiResponse<T> {
  success: boolean;
  message: string;
  data: T;
  error?: ApiError;
  meta?: PaginationMeta;
}

export type ApiPaginatedResponse<T> = ApiResponse<T[]>;

PaginationMeta

export interface PaginationMeta {
  page?: number;       // 1-based page number
  limit?: number;      // Items per page
  total?: number;      // Total items
  totalPages?: number; // Total pages
}

ApiError

export interface ApiError {
  status?: number;
  message: string;
  code?: string;
  validation?: Record<string, string[]>;
  raw?: any;
  url?: string | null;
}

Best Practices

1. Always Type Responses

// Good
getUser(id: string): Observable<UserProfile> {
  return this.http.get<ApiResponse<UserProfile>>(`${this.baseUrl}/users/${id}`);
}

// Bad
getUser(id: string): Observable<any> {
  return this.http.get(`${this.baseUrl}/users/${id}`);
}

2. Handle Errors Consistently

getData(): Observable<Data> {
  return this.http.get<ApiResponse<Data>>(url).pipe(
    map(res => res.data),
    catchError(err => this.handleErrorAsApiError(err, url))
  );
}

3. Validate Responses

map(res => {
  if (!res?.data) throw new Error('Invalid response');
  return res.data;
})

4. Use Environment Variables

// Good
private baseUrl = environment.apiUrl;

// Bad
private baseUrl = 'https://api.rodando.cu';

Build docs developers (and LLMs) love