Services & Dependency Injection

Angular HttpClient

Learn how to use Angular's HttpClient to make HTTP requests, handle responses, set headers, manage errors, and work with interceptors.

Setting Up HttpClient

Angular's HttpClient is the standard way to communicate with backend APIs. First, provide it in your application config:

typescript
// app.config.ts
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
  ],
};

Making HTTP Requests

GET Request

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

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

@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  private apiUrl = 'https://api.example.com/users';

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }

  getUserById(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }
}

POST Request

typescript
createUser(user: Omit<User, 'id'>): Observable<User> {
  return this.http.post<User>(this.apiUrl, user);
}

PUT Request

typescript
updateUser(id: number, user: Partial<User>): Observable<User> {
  return this.http.put<User>(`${this.apiUrl}/${id}`, user);
}

DELETE Request

typescript
deleteUser(id: number): Observable<void> {
  return this.http.delete<void>(`${this.apiUrl}/${id}`);
}

PATCH Request

typescript
patchUser(id: number, changes: Partial<User>): Observable<User> {
  return this.http.patch<User>(`${this.apiUrl}/${id}`, changes);
}

Subscribing to HTTP Observables

HTTP methods return cold Observables — the request is not made until you subscribe:

typescript
@Component({
  selector: 'app-user-list',
  standalone: true,
  template: `
    @if (loading) {
      <p>Loading...</p>
    } @else if (error) {
      <p class="error">{{ error }}</p>
    } @else {
      @for (user of users; track user.id) {
        <div>{{ user.name }} — {{ user.email }}</div>
      }
    }
  `,
})
export class UserListComponent implements OnInit {
  private userService = inject(UserService);
  users: User[] = [];
  loading = true;
  error = '';

  ngOnInit() {
    this.userService.getUsers().subscribe({
      next: (users) => {
        this.users = users;
        this.loading = false;
      },
      error: (err) => {
        this.error = 'Failed to load users';
        this.loading = false;
        console.error(err);
      },
    });
  }
}

Using AsyncPipe (Preferred)

typescript
@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @if (users$ | async; as users) {
      @for (user of users; track user.id) {
        <div>{{ user.name }}</div>
      }
    } @else {
      <p>Loading...</p>
    }
  `,
})
export class UserListComponent {
  private userService = inject(UserService);
  users$ = this.userService.getUsers();
}

Tip: The async pipe automatically subscribes and unsubscribes, preventing memory leaks.

Query Parameters

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

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

  return this.http.get<User[]>(this.apiUrl, { params });
  // GET /users?q=john&page=1&limit=20
}

Custom Headers

typescript
import { HttpHeaders } from '@angular/common/http';

createUser(user: Omit<User, 'id'>): Observable<User> {
  const headers = new HttpHeaders({
    'Content-Type': 'application/json',
    'X-Custom-Header': 'BigXStar',
  });

  return this.http.post<User>(this.apiUrl, user, { headers });
}

Error Handling

Using catchError

typescript
import { catchError, throwError } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';

getUsers(): Observable<User[]> {
  return this.http.get<User[]>(this.apiUrl).pipe(
    catchError(this.handleError),
  );
}

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

  if (error.status === 0) {
    errorMessage = 'Network error — please check your connection';
  } else if (error.status === 404) {
    errorMessage = 'Resource not found';
  } else if (error.status === 401) {
    errorMessage = 'Unauthorized — please log in';
  } else if (error.status === 500) {
    errorMessage = 'Server error — please try again later';
  }

  console.error(`Error ${error.status}: ${error.message}`);
  return throwError(() => new Error(errorMessage));
}

Retry on Failure

typescript
import { retry, catchError } from 'rxjs';

getUsers(): Observable<User[]> {
  return this.http.get<User[]>(this.apiUrl).pipe(
    retry(3), // Retry up to 3 times on failure
    catchError(this.handleError),
  );
}

HTTP Interceptors

Interceptors let you modify every HTTP request or response globally.

Functional Interceptor (Modern)

typescript
// auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = localStorage.getItem('auth_token');

  if (token) {
    const clonedReq = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`,
      },
    });
    return next(clonedReq);
  }

  return next(req);
};

Logging Interceptor

typescript
import { HttpInterceptorFn } from '@angular/common/http';
import { tap } from 'rxjs';

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  const started = Date.now();
  console.log(`→ ${req.method} ${req.url}`);

  return next(req).pipe(
    tap({
      next: () => {
        const elapsed = Date.now() - started;
        console.log(`← ${req.method} ${req.url} (${elapsed}ms)`);
      },
      error: (err) => {
        const elapsed = Date.now() - started;
        console.error(`✕ ${req.method} ${req.url} failed (${elapsed}ms)`, err);
      },
    }),
  );
};

Registering Interceptors

typescript
// app.config.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';

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

Complete CRUD Service Example

typescript
@Injectable({ providedIn: 'root' })
export class ProductService {
  private http = inject(HttpClient);
  private baseUrl = 'https://api.example.com/products';

  getAll(): Observable<Product[]> {
    return this.http.get<Product[]>(this.baseUrl).pipe(
      catchError(this.handleError),
    );
  }

  getById(id: number): Observable<Product> {
    return this.http.get<Product>(`${this.baseUrl}/${id}`).pipe(
      catchError(this.handleError),
    );
  }

  create(product: Omit<Product, 'id'>): Observable<Product> {
    return this.http.post<Product>(this.baseUrl, product).pipe(
      catchError(this.handleError),
    );
  }

  update(id: number, product: Partial<Product>): Observable<Product> {
    return this.http.put<Product>(`${this.baseUrl}/${id}`, product).pipe(
      catchError(this.handleError),
    );
  }

  delete(id: number): Observable<void> {
    return this.http.delete<void>(`${this.baseUrl}/${id}`).pipe(
      catchError(this.handleError),
    );
  }

  search(query: string): Observable<Product[]> {
    return this.http.get<Product[]>(this.baseUrl, {
      params: new HttpParams().set('q', query),
    }).pipe(
      catchError(this.handleError),
    );
  }

  private handleError(error: HttpErrorResponse) {
    console.error('API Error:', error);
    return throwError(() => error);
  }
}

Next Steps

With HTTP communication working, you need a way to navigate between different views. Next, we'll explore Angular routing — how to map URLs to components and build multi-page navigation.