ДокументацияAngularHTTP-клиент в Angular
Средний 11 мин чтения

HTTP-клиент в Angular

Работа с HTTP-запросами в Angular: HttpClient, типизированные запросы, интерцепторы для авторизации и логирования, загрузка файлов, обработка ошибок.

AngularHttpClientinterceptorsRESTuploaderror handling

Настройка HttpClient

HttpClient — встроенный сервис Angular для HTTP-запросов. В standalone-приложении подключается через провайдер:

// app.config.ts
import { ApplicationConfig } from '@angular/core'
import { provideHttpClient } from '@angular/common/http'

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

В组件е — просто инжектим:

import { HttpClient } from '@angular/common/http'
import { inject } from '@angular/core'

export class UserService {
  private http = inject(HttpClient)
}

Базовые запросы

HttpClient возвращает Observable — результат нужно получить через .subscribe() или async pipe:

GET

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

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

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

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

  getWithParams(page: number, limit: number): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl, {
      params: { page: page.toString(), limit: limit.toString() }
      // или через HttpParams:
      // params: new HttpParams().set('page', page).set('limit', limit)
    })
  }
}

POST, PUT, PATCH, DELETE

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

update(id: number, data: Partial<User>): Observable<User> {
  return this.http.patch<User>(`${this.apiUrl}/${id}`, data)
}

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

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

Полный набор опций

this.http.get<User[]>('/api/users', {
  headers: {
    'X-Custom-Header': 'value',
    'Accept-Language': 'ru',
  },
  params: {
    page: '1',
    sort: 'name',
  },
  observe: 'response',     // Получить полный HttpResponse
  responseType: 'json',    // 'blob', 'text', 'arraybuffer'
  withCredentials: true,   // Отправлять cookies
})

observe: 'response' — доступ к заголовкам

getUsersPage(page: number): Observable<HttpResponse<User[]>> {
  return this.http.get<User[]>('/api/users', {
    params: { page: page.toString() },
    observe: 'response',
  })
}

// Использование
this.userService.getUsersPage(1).subscribe(response => {
  const users = response.body!
  const total = response.headers.get('X-Total-Count')
  console.log(`Получено ${users.length} из ${total}`)
})

Использование в компонентах

Способ 1: toSignal (рекомендуется с Signals)

import { toSignal } from '@angular/core/rxjs-interop'

@Component({
  template: `
    @for (user of users(); track user.id) {
      <p>{{ user.name }}</p>
    }
  `
})
export class UserListComponent {
  private userService = inject(UserService)

  users = toSignal(this.userService.getAll(), { initialValue: [] })
}

Способ 2: async pipe

@Component({
  template: `
    @for (user of users$ | async; track user.id) {
      <p>{{ user.name }}</p>
    }
  `
})
export class UserListComponent {
  private userService = inject(UserService)

  users$ = this.userService.getAll()
}

Способ 3: подписка вручную

export class UserListComponent implements OnInit, OnDestroy {
  private userService = inject(UserService)
  users: User[] = []

  ngOnInit() {
    this.userService.getAll()
      .pipe(takeUntilDestroyed())
      .subscribe(users => this.users = users)
  }
}

Интерцепторы

Интерцепторы — функции, которые перехватывают каждый запрос и ответ. Используются для добавления токена, логирования, обработки ошибок.

Авторизационный интерцептор

// interceptors/auth.interceptor.ts
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn } from '@angular/common/http'
import { inject } from '@angular/core'
import { AuthService } from '../services/auth.service'

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const auth = inject(AuthService)
  const token = auth.token()

  if (token) {
    req = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    })
  }

  return next(req)
}

Интерцептор логирования

// interceptors/logging.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http'
import { tap } from 'rxjs/operators'

export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  const start = performance.now()

  return next(req).pipe(
    tap({
      next: () => {
        const elapsed = Math.round(performance.now() - start)
        console.log(`[HTTP] ${req.method} ${req.url}${elapsed}ms`)
      },
      error: (err) => {
        const elapsed = Math.round(performance.now() - start)
        console.error(`[HTTP] ${req.method} ${req.url}${err.status} (${elapsed}ms)`)
      }
    })
  )
}

Интерцептор обработки ошибок

// interceptors/error.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http'
import { inject } from '@angular/core'
import { catchError, throwError } from 'rxjs'
import { Router } from '@angular/router'
import { NotificationService } from '../services/notification.service'

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  const router = inject(Router)
  const notify = inject(NotificationService)

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      switch (error.status) {
        case 401:
          router.navigate(['/login'])
          break
        case 403:
          notify.error('Доступ запрещён')
          break
        case 404:
          notify.error('Ресурс не найден')
          break
        case 500:
          notify.error('Ошибка сервера. Попробуйте позже.')
          break
        default:
          notify.error(error.error?.message || 'Произошла ошибка')
      }

      return throwError(() => error)
    })
  )
}

Регистрация интерцепторов

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

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

Порядок важен — интерцепторы выполняются в том порядке, в котором указаны.

Интерцепторы для кэширования

// interceptors/cache.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http'
import { of } from 'rxjs'

const cache = new Map<string, { data: unknown; timestamp: number }>()
const CACHE_TTL = 5 * 60 * 1000 // 5 минут

export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
  if (req.method !== 'GET') {
    return next(req)
  }

  const cached = cache.get(req.urlWithParams)
  if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
    return of(cached.data as any)
  }

  return next(req).pipe(
    tap(response => {
      cache.set(req.urlWithParams, { data: response, timestamp: Date.now() })
    })
  )
}

Загрузка файлов

Загрузка одного файла

uploadFile(file: File): Observable<{ url: string }> {
  const formData = new FormData()
  formData.append('file', file)

  return this.http.post<{ url: string }>('/api/upload', formData, {
    reportProgress: true,
    observe: 'events',
  }).pipe(
    filter(event => event.type === HttpEventType.Response),
    map(event => (event as HttpResponse<{ url: string }>).body!)
  )
}

Загрузка с прогрессом

import { HttpEventType, HttpResponse } from '@angular/common/http'

@Component({ ... })
export class UploadComponent {
  private http = inject(HttpClient)
  progress = signal(0)
  isUploading = signal(false)

  onFileSelected(event: Event) {
    const file = (event.target as HTMLInputElement).files?.[0]
    if (!file) return

    this.isUploading.set(true)

    const formData = new FormData()
    formData.append('avatar', file)

    this.http.post('/api/upload/avatar', formData, {
      reportProgress: true,
      observe: 'events',
    }).subscribe({
      next: (event) => {
        if (event.type === HttpEventType.UploadProgress) {
          const percent = Math.round((event.loaded / (event.total ?? 1)) * 100)
          this.progress.set(percent)
        }
        if (event instanceof HttpResponse) {
          this.isUploading.set(false)
          console.log('Файл загружен:', event.body)
        }
      },
      error: () => this.isUploading.set(false),
    })
  }
}
<!-- Шаблон -->
<input type="file" (change)="onFileSelected($event)" accept="image/*">
@if (isUploading()) {
  <progress [value]="progress()" max="100"></progress>
  <span>{{ progress() }}%</span>
}

Загрузка нескольких файлов

uploadFiles(files: FileList): Observable<string[]> {
  const formData = new FormData()
  Array.from(files).forEach((file, i) => {
    formData.append(`files`, file)
  })

  return this.http.post<string[]>('/api/upload/batch', formData)
}

Скачивание файла

downloadReport(id: string): Observable<Blob> {
  return this.http.get(`/api/reports/${id}/download`, {
    responseType: 'blob',
  }).pipe(
    tap(blob => {
      const url = URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      a.download = `report-${id}.pdf`
      a.click()
      URL.revokeObjectURL(url)
    })
  )
}

Обработка ошибок в сервисе

@Injectable({ providedIn: 'root' })
export class ApiService {
  private http = inject(HttpClient)

  get<T>(url: string): Observable<T> {
    return this.http.get<T>(url).pipe(
      retry({ count: 2, delay: 1000 }),
      catchError(this.handleError)
    )
  }

  private handleError(error: HttpErrorResponse): Observable<never> {
    let message = 'Произошла ошибка'

    if (error.status === 0) {
      message = 'Нет связи с сервером'
    } else if (error.status === 422) {
      const errors = error.error?.errors
      message = Object.values(errors ?? {}).join(', ')
    } else {
      message = error.error?.message || `Ошибка ${error.status}`
    }

    return throwError(() => new Error(message))
  }
}

Практический пример — CRUD-сервис

// services/task.service.ts
import { Injectable, inject, signal } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { Observable, switchMap, tap } from 'rxjs'

export interface Task {
  id: string
  title: string
  description: string
  difficulty: 'easy' | 'medium' | 'hard'
  status: 'open' | 'in_progress' | 'done'
  createdAt: string
}

@Injectable({ providedIn: 'root' })
export class TaskService {
  private http = inject(HttpClient)
  private apiUrl = '/api/tasks'

  private tasks = signal<Task[]>([])
  readonly tasksList = this.tasks.asReadonly()

  loadAll(params?: { difficulty?: string; status?: string }): Observable<Task[]> {
    return this.http.get<Task[]>(this.apiUrl, { params }).pipe(
      tap(tasks => this.tasks.set(tasks))
    )
  }

  loadOne(id: string): Observable<Task> {
    return this.http.get<Task>(`${this.apiUrl}/${id}`)
  }

  create(data: Omit<Task, 'id' | 'createdAt'>): Observable<Task> {
    return this.http.post<Task>(this.apiUrl, data).pipe(
      tap(task => this.tasks.update(list => [task, ...list]))
    )
  }

  update(id: string, data: Partial<Task>): Observable<Task> {
    return this.http.patch<Task>(`${this.apiUrl}/${id}`, data).pipe(
      tap(updated => this.tasks.update(list =>
        list.map(t => t.id === id ? updated : t)
      ))
    )
  }

  remove(id: string): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`).pipe(
      tap(() => this.tasks.update(list => list.filter(t => t.id !== id)))
    )
  }
}

Итог

ВозможностьДля чего
HttpClient.get/post/...Типизированные HTTP-запросы
provideHttpClient()Регистрация в standalone-приложении
ИнтерцепторыТокен, логирование, кэш, обработка ошибок
HttpParamsQuery-параметры
observe: 'response'Доступ к заголовкам и статусу
reportProgressПрогресс загрузки файла
responseType: 'blob'Скачивание файлов
retryПовтор запроса при ошибке
toSignalПревратить HTTP-вызов в Signal

HttpClient — зрелый и продуманный HTTP-клиент. Интерцепторы позволяют вынести сквозную логику (авторизация, логирование, ошибки) из сервисов и компонентов, а типизация делает работу с ответами безопасной.