14. HttpClient
🌐 HttpClient в Angular
Заголовок раздела «🌐 HttpClient в Angular»Angular предоставляет мощный HttpClient для работы с HTTP-запросами. Он основан на RxJS Observable (а не Promise), что даёт отмену запросов, трансформации, retry и многое другое из коробки 🚀
🔧 Подключение HttpClient
Заголовок раздела «🔧 Подключение HttpClient»Standalone приложение (Angular 17+):
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 приложение:
import { HttpClientModule } from '@angular/common/http';
@NgModule({ imports: [ BrowserModule, HttpClientModule, // 👈 Добавляем один раз в корневом модуле ], bootstrap: [AppComponent]})export class AppModule {}💉 Внедрение HttpClient в сервис
Заголовок раздела «💉 Внедрение HttpClient в сервис»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!
📡 GET запросы с типизацией
Заголовок раздела «📡 GET запросы с типизацией»@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 }); }}📤 POST, PUT, PATCH, DELETE
Заголовок раздела «📤 POST, PUT, PATCH, DELETE»@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 ); }}🔑 HttpHeaders — заголовки запроса
Заголовок раздела «🔑 HttpHeaders — заголовки запроса»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()возвращает новый объект. Не забывай сохранять результат!
🔍 HttpParams — query-параметры
Заголовок раздела «🔍 HttpParams — query-параметры»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; } }); }}🔄 Retry логика
Заголовок раздела «🔄 Retry логика»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 — перехватчики запросов
Заголовок раздела «🛡️ Interceptors — перехватчики запросов»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; }) );};🏗️ Полный CRUD сервис
Заголовок раздела «🏗️ Полный CRUD сервис»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)); }}🌍 Environment URLs
Заголовок раздела «🌍 Environment URLs»// 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}