Перейти к содержимому

14. HttpClient

Angular предоставляет мощный HttpClient для работы с HTTP-запросами. Он основан на RxJS Observable (а не Promise), что даёт отмену запросов, трансформации, retry и многое другое из коробки 🚀


Standalone приложение (Angular 17+):

main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app.component';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([authInterceptor, loggingInterceptor])
)
]
});

NgModule приложение:

app.module.ts
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [
BrowserModule,
HttpClientModule, // 👈 Добавляем один раз в корневом модуле
],
bootstrap: [AppComponent]
})
export class AppModule {}

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserService {
private readonly baseUrl = '/api/users';
// HttpClient внедряется через DI
constructor(private http: HttpClient) {}
getAll(): Observable<User[]> {
return this.http.get<User[]>(this.baseUrl);
}
}

⚠️ Никогда не вызывай new HttpClient() вручную. Только через DI!


@Injectable({ providedIn: 'root' })
export class UserService {
private readonly baseUrl = environment.apiUrl + '/users';
constructor(private http: HttpClient) {}
// Получить всех пользователей
getAll(): Observable<User[]> {
return this.http.get<User[]>(this.baseUrl);
}
// Получить конкретного пользователя
getById(id: number): Observable<User> {
return this.http.get<User>(this.baseUrl + '/' + id);
}
// С query параметрами: /api/users?page=1&limit=10&role=admin
getWithParams(page: number, limit: number, role?: string): Observable<PaginatedResponse<User>> {
let params = new HttpParams()
.set('page', page.toString())
.set('limit', limit.toString());
if (role) {
params = params.set('role', role);
}
return this.http.get<PaginatedResponse<User>>(this.baseUrl, { params });
}
// Получить полный ответ (статус, заголовки, тело)
getWithFullResponse(id: number): Observable<HttpResponse<User>> {
return this.http.get<User>(this.baseUrl + '/' + id, {
observe: 'response' // 👈 Возвращает HttpResponse<T>, а не просто T
});
}
}

@Injectable({ providedIn: 'root' })
export class UserService {
private readonly baseUrl = environment.apiUrl + '/users';
constructor(private http: HttpClient) {}
// POST — создание ресурса
create(data: CreateUserDto): Observable<User> {
return this.http.post<User>(this.baseUrl, data);
}
// PUT — полное обновление
update(id: number, data: UpdateUserDto): Observable<User> {
return this.http.put<User>(this.baseUrl + '/' + id, data);
}
// PATCH — частичное обновление
patch(id: number, changes: Partial<User>): Observable<User> {
return this.http.patch<User>(this.baseUrl + '/' + id, changes);
}
// DELETE — удаление
delete(id: number): Observable<void> {
return this.http.delete<void>(this.baseUrl + '/' + id);
}
// Загрузка файла (multipart/form-data)
uploadAvatar(userId: number, file: File): Observable<{ url: string }> {
const formData = new FormData();
formData.append('file', file, file.name);
return this.http.post<{ url: string }>(
this.baseUrl + '/' + userId + '/avatar',
formData
);
}
}

import { HttpHeaders } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class ApiService {
constructor(
private http: HttpClient,
private auth: AuthService
) {}
// Установка заголовков вручную
getSecureData(): Observable<SecureData> {
const headers = new HttpHeaders({
'Authorization': 'Bearer ' + this.auth.getToken(),
'Content-Type': 'application/json',
'X-Request-ID': crypto.randomUUID(),
});
return this.http.get<SecureData>('/api/secure', { headers });
}
// Цепочечный стиль
postWithCustomHeaders(data: unknown): Observable<unknown> {
const headers = new HttpHeaders()
.set('Authorization', 'Bearer ' + this.auth.getToken())
.set('Accept-Language', 'ru-RU')
.append('X-Client-Version', '2.0.0'); // append добавляет, set заменяет
return this.http.post('/api/data', data, { headers });
}
}

💡 HttpHeaders иммутабельный — каждый вызов .set() возвращает новый объект. Не забывай сохранять результат!


import { HttpParams } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class ProductService {
constructor(private http: HttpClient) {}
search(filters: ProductFilters): Observable<Product[]> {
// Строим параметры чисто, без манипуляций со строками
let params = new HttpParams();
if (filters.query) params = params.set('q', filters.query);
if (filters.category) params = params.set('category', filters.category);
if (filters.minPrice) params = params.set('minPrice', filters.minPrice.toString());
if (filters.maxPrice) params = params.set('maxPrice', filters.maxPrice.toString());
if (filters.inStock) params = params.set('inStock', 'true');
if (filters.sortBy) params = params.set('sortBy', filters.sortBy);
// Результат: /api/products?q=angular&category=books&minPrice=10&sortBy=price
return this.http.get<Product[]>('/api/products', { params });
}
}

import { catchError, throwError } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(
private http: HttpClient,
private notifications: NotificationService
) {}
getAll(): Observable<User[]> {
return this.http.get<User[]>('/api/users').pipe(
catchError(err => this.handleError(err))
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
let message: string;
if (error.status === 0) {
// Сетевая ошибка (нет интернета, CORS и т.д.)
message = 'Нет подключения к серверу. Проверьте интернет.';
} else if (error.status === 401) {
message = 'Сессия истекла. Войдите снова.';
// Редирект на страницу входа
} else if (error.status === 403) {
message = 'Недостаточно прав для этого действия.';
} else if (error.status === 404) {
message = 'Ресурс не найден.';
} else if (error.status >= 500) {
message = 'Ошибка сервера. Попробуйте позже.';
} else {
// Парсим ответ сервера если он есть
message = error.error?.message ?? 'Неизвестная ошибка.';
}
this.notifications.error(message);
return throwError(() => new Error(message));
}
}

@Component({
template: `
<div *ngIf="isLoading" class="spinner">Загрузка...</div>
<div *ngIf="error" class="error">{{ error }}</div>
<ul *ngIf="!isLoading && !error">
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
`
})
export class UsersComponent implements OnInit {
users: User[] = [];
isLoading = false;
error: string | null = null;
constructor(private userService: UserService) {}
ngOnInit(): void {
this.loadUsers();
}
loadUsers(): void {
this.isLoading = true;
this.error = null;
this.userService.getAll().subscribe({
next: users => {
this.users = users;
this.isLoading = false;
},
error: err => {
this.error = err.message;
this.isLoading = false;
}
});
}
}

import { retry, retryWhen, delay, take } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class ResilientApiService {
constructor(private http: HttpClient) {}
// Простой retry — 3 попытки
getData(): Observable<Data> {
return this.http.get<Data>('/api/data').pipe(
retry(3) // Повторить 3 раза при ошибке
);
}
// Retry с задержкой — умнее
getDataWithBackoff(): Observable<Data> {
return this.http.get<Data>('/api/data').pipe(
retryWhen(errors =>
errors.pipe(
delay(1000), // Ждём 1 секунду между попытками
take(3) // Максимум 3 попытки
)
)
);
}
// Retry только для сетевых ошибок (статус 0)
getDataSmart(): Observable<Data> {
return this.http.get<Data>('/api/data').pipe(
retry({
count: 3,
delay: (error, retryCount) => {
// Не повторяем при клиентских ошибках (4xx)
if (error instanceof HttpErrorResponse && error.status >= 400 && error.status < 500) {
throw error;
}
return timer(retryCount * 1000); // Экспоненциальная задержка
}
})
);
}
}

Interceptors позволяют трансформировать все запросы и ответы в одном месте:

import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
// Автоматически добавляет Authorization header ко всем запросам
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.getToken();
if (token) {
const authReq = req.clone({
headers: req.headers.set('Authorization', 'Bearer ' + token)
});
return next(authReq);
}
return next(req);
};
// Логирует все запросы
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const logger = inject(LoggingService);
const start = Date.now();
logger.log('→ ' + req.method + ' ' + req.url);
return next(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
const duration = Date.now() - start;
logger.log('← ' + event.status + ' ' + req.url + ' (' + duration + 'ms)');
}
}),
catchError(err => {
logger.error('✗ ' + req.method + ' ' + req.url, err);
throw err;
})
);
};

export interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'viewer';
createdAt: string;
}
export type CreateUserDto = Omit<User, 'id' | 'createdAt'>;
export type UpdateUserDto = Partial<CreateUserDto>;
@Injectable({ providedIn: 'root' })
export class UserCrudService {
private readonly url = environment.apiUrl + '/users';
constructor(private http: HttpClient) {}
getAll(params?: { page?: number; limit?: number }): Observable<User[]> {
const httpParams = new HttpParams({ fromObject: params ?? {} });
return this.http.get<User[]>(this.url, { params: httpParams }).pipe(
catchError(this.handleError)
);
}
getOne(id: number): Observable<User> {
return this.http.get<User>(this.url + '/' + id).pipe(
catchError(this.handleError)
);
}
create(dto: CreateUserDto): Observable<User> {
return this.http.post<User>(this.url, dto).pipe(
catchError(this.handleError)
);
}
update(id: number, dto: UpdateUserDto): Observable<User> {
return this.http.patch<User>(this.url + '/' + id, dto).pipe(
catchError(this.handleError)
);
}
remove(id: number): Observable<void> {
return this.http.delete<void>(this.url + '/' + id).pipe(
catchError(this.handleError)
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
const msg = error.error?.message ?? 'Ошибка сервера';
return throwError(() => new Error(msg));
}
}

// src/environments/environment.ts (разработка)
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
wsUrl: 'ws://localhost:3000',
};
// src/environments/environment.prod.ts (продакшен)
export const environment = {
production: true,
apiUrl: 'https://api.myapp.com/v1',
wsUrl: 'wss://ws.myapp.com',
};
// В сервисе — используем environment
@Injectable({ providedIn: 'root' })
export class OrderService {
private readonly apiUrl = environment.apiUrl + '/orders';
// При сборке ng build --configuration=production
// Angular автоматически подменит environment.ts на environment.prod.ts
}