HTTPクライアントと通信
AngularにはHTTP通信のための組み込みクライアントがあり、Reactのようにfetchやaxiosを別途導入する必要がありません。
1. HttpClientの基本設定
provideHttpClient() の設定
スタンドアロン構成では、app.config.ts の providers に provideHttpClient() を追加します(従来の HttpClientModule は非推奨です)。
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { routes } from './app.routes';
import { authInterceptor } from './auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(
withFetch(), // 内部で fetch API を使用(SSR 利用時に推奨)
withInterceptors([authInterceptor]),
),
],
};
| 機能フラグ | 役割 |
|---|---|
withInterceptors([...]) | 関数型インターセプターを登録 |
withFetch() | XHRの代わりに fetch を使用(SSRで推奨) |
withXsrfConfiguration() | XSRFトークンの設定 |
withInterceptorsFromDi() | クラスベースの旧インターセプターを併用 |
HttpClient の注入
inject() 関数で注入するのが現在の標準的な書き方です。通常はコンポーネントから直接ではなく、サービスに閉じ込めます。
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class TodoApi {
private readonly http = inject(HttpClient);
private readonly baseUrl = '/api/todos';
}
2. CRUD操作
GET / POST / PUT / DELETE リクエスト
export interface Todo {
id: number;
title: string;
done: boolean;
}
@Injectable({ providedIn: 'root' })
export class TodoApi {
private readonly http = inject(HttpClient);
private readonly baseUrl = '/api/todos';
list(params?: { done?: boolean }): Observable<Todo[]> {
return this.http.get<Todo[]>(this.baseUrl, {
params: params?.done === undefined ? {} : { done: params.done },
});
}
get(id: number): Observable<Todo> {
return this.http.get<Todo>(`${this.baseUrl}/${id}`);
}
create(todo: Omit<Todo, 'id'>): Observable<Todo> {
return this.http.post<Todo>(this.baseUrl, todo);
}
update(todo: Todo): Observable<Todo> {
return this.http.put<Todo>(`${this.baseUrl}/${todo.id}`, todo);
}
remove(id: number): Observable<void> {
return this.http.delete<void>(`${this.baseUrl}/${id}`);
}
}
型付きレスポンスの取得
ジェネリクス(get<Todo[]>)で型を指定すると、戻り値が型付けされます。ただしこれはコンパイル時の型付けにすぎず、実行時の検証は行われません。信頼できないAPIでは Zod などで検証してください。HTTPヘッダーやステータスが必要な場合は observe: 'response' を指定します。
this.http
.get<Todo[]>(this.baseUrl, { observe: 'response' })
.subscribe((res) => {
console.log(res.status, res.headers.get('X-Total-Count'), res.body);
});
Observable としてのレスポンス
HttpClient のメソッドは Observable を返し、購読(subscribe)されるまでリクエストは発行されません(コールド)。また、購読ごとにリクエストが発行される点に注意してください。
@Component({
selector: 'app-todo-list',
standalone: true,
imports: [AsyncPipe],
template: `
@if (todos$ | async; as todos) {
<ul>
@for (t of todos; track t.id) {
<li>{{ t.title }}</li>
}
</ul>
}
`,
})
export class TodoListComponent {
todos$ = inject(TodoApi).list();
}
シグナルと組み合わせる場合は toSignal が便利です。
import { toSignal } from '@angular/core/rxjs-interop';
export class TodoListComponent {
todos = toSignal(inject(TodoApi).list(), { initialValue: [] });
}
@for (t of todos(); track t.id) {
<li>{{ t.title }}</li>
}
Reactの fetch / axios / SWR / TanStack Query との対比
| 観点 | Angular HttpClient | React |
|---|---|---|
| クライアント | 組み込み | fetch(標準)/ axios(外部) |
| 戻り値 | Observable(遅延・キャンセル可能) | Promise(即時実行) |
| JSON変換 | 自動 | fetch は res.json() が必要 |
| HTTPエラー | 4xx/5xx は自動でエラー扱い | fetch は res.ok を自前で確認 |
| キャンセル | 購読解除 / switchMap | AbortController |
| キャッシュ・再取得 | 標準では無し(自前または RxJS) | SWR / TanStack Query が担当 |
| ローディング/エラー状態 | 自前で管理 | SWR/TanStack Query が提供 |
SWR や TanStack Query が担うサーバー状態管理(キャッシュ、再検証、ローディング状態)は、Angular の HttpClient 単体には含まれません。必要なら @tanstack/angular-query-experimental を導入するか、RxJS とシグナルで実装します。
3. インターセプター(Interceptors)
インターセプターは、リクエストとレスポンスを横断的に加工する仕組みです。
関数型インターセプター(新しい書き方)
現在は HttpInterceptorFn による関数型が推奨で、クラスベースの HttpInterceptor よりも簡潔です。関数内では inject() が使えます。
import { HttpInterceptorFn } from '@angular/common/http';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
// req は不変。clone() で変更したコピーを作る
return next(req.clone({ setHeaders: { 'X-App-Version': '1.0.0' } }));
};
provideHttpClient(withInterceptors([a, b, c])) で登録します。リクエスト時は配列の順に、レスポンス時は逆順に通過します。
認証トークンの自動付与
import { inject } from '@angular/core';
import { HttpInterceptorFn } from '@angular/common/http';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).token();
// 自社APIにだけトークンを付与(外部サイトへの漏洩を防ぐ)
if (!token || !req.url.startsWith('/api/')) {
return next(req);
}
return next(
req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }),
);
};
ローディング状態の管理
リクエスト数をシグナルでカウントし、グローバルなローディング表示に使えます。
// loading.service.ts
@Injectable({ providedIn: 'root' })
export class LoadingService {
private readonly count = signal(0);
readonly isLoading = computed(() => this.count() > 0);
start(): void { this.count.update((n) => n + 1); }
stop(): void { this.count.update((n) => Math.max(0, n - 1)); }
}
// loading.interceptor.ts
import { finalize } from 'rxjs';
export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
const loading = inject(LoadingService);
loading.start();
return next(req).pipe(finalize(() => loading.stop()));
};
finalize は成功・失敗・キャンセルのいずれでも実行されるため、カウンタがずれません。
リクエスト/レスポンスのログ
import { HttpResponse } from '@angular/common/http';
import { tap } from 'rxjs';
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
const started = performance.now();
return next(req).pipe(
tap({
next: (event) => {
if (event instanceof HttpResponse) {
const ms = Math.round(performance.now() - started);
console.log(`${req.method} ${req.urlWithParams} -> ${event.status} (${ms}ms)`);
}
},
error: (err) => console.error(`${req.method} ${req.urlWithParams} failed`, err),
}),
);
};
Next.js の middleware や Axios interceptors との対比
| 観点 | Angular Interceptor | Axios interceptors | Next.js middleware |
|---|---|---|---|
| 実行場所 | クライアント(SSR時はサーバー) | クライアント/Node | サーバー(Edge)、ページ到達前 |
| 対象 | HttpClient の送信リクエスト | axios の送信リクエスト | 受信リクエスト |
| 登録 | withInterceptors([...]) | axios.interceptors.request.use | middleware.ts |
| 用途 | トークン付与、エラー共通処理 | 同左 | 認証リダイレクト、ヘッダー操作 |
Axios interceptors と役割はほぼ同じですが、Next.js の middleware は「受信リクエスト」を処理する点で向きが逆である点に注意してください。
4. エラーハンドリング
catchError オペレータの使用
import { catchError, of, throwError } from 'rxjs';
list(): Observable<Todo[]> {
return this.http.get<Todo[]>(this.baseUrl).pipe(
// 失敗時に空配列でフォールバック
catchError(() => of([])),
);
}
フォールバックせず、変換して再スローすることもできます。
return this.http.get<Todo>(url).pipe(
catchError((err) => throwError(() => new Error('Todoの取得に失敗しました', { cause: err }))),
);
HttpErrorResponse の処理
HTTPエラーは HttpErrorResponse として届きます。status が 0 の場合はネットワーク障害やCORSなど、レスポンスを受け取れなかったことを示します。
import { HttpErrorResponse } from '@angular/common/http';
function toMessage(err: HttpErrorResponse): string {
if (err.status === 0) return 'ネットワークに接続できません。';
switch (err.status) {
case 400: return '入力内容に誤りがあります。';
case 401: return '認証が必要です。';
case 403: return '権限がありません。';
case 404: return '対象が見つかりません。';
default: return err.status >= 500
? 'サーバーエラーが発生しました。'
: '予期しないエラーが発生しました。';
}
}
リトライロジック(retry, retryWhen)
retry で単純な再試行ができます。retryWhen は非推奨のため、retry({ delay }) の形式で指数バックオフを書くのが現在の方法です。
import { retry, timer } from 'rxjs';
this.http.get<Todo[]>(url).pipe(
retry({
count: 3,
delay: (error: HttpErrorResponse, retryCount) => {
// 5xx とネットワークエラーのみ再試行
if (error.status !== 0 && error.status < 500) {
throw error;
}
return timer(2 ** retryCount * 500); // 1s, 2s, 4s
},
}),
);
POST など副作用のあるリクエストを無条件で再試行すると二重実行の恐れがあるため、冪等な GET を中心に使ってください。
グローバルエラーハンドリング
共通処理はインターセプターに集約します。
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
const router = inject(Router);
const toast = inject(ToastService);
return next(req).pipe(
catchError((err: HttpErrorResponse) => {
if (err.status === 401) {
router.navigate(['/login']);
} else if (err.status >= 500 || err.status === 0) {
toast.error(toMessage(err));
}
return throwError(() => err); // 呼び出し側でも処理できるよう再スロー
}),
);
};
HTTP以外を含む未処理のエラーは、ErrorHandler を差し替えて一括で捕捉します。
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
handleError(error: unknown): void {
// 例: 監視サービスへ送信
console.error('Unhandled error', error);
}
}
// app.config.ts の providers に追加
{ provide: ErrorHandler, useClass: GlobalErrorHandler }
5. キャッシュとパフォーマンス
shareReplay によるキャッシュ
HttpClient はデフォルトでキャッシュしません。shareReplay を使うと、結果を保持して複数の購読者に再利用できます。
@Injectable({ providedIn: 'root' })
export class CountryApi {
private readonly http = inject(HttpClient);
readonly countries$ = this.http.get<Country[]>('/api/countries').pipe(
shareReplay({ bufferSize: 1, refCount: false }),
);
}
何度購読しても、リクエストは最初の1回だけ発行されます。マスタデータのように変更頻度が低いものに向いています。更新したい場合は Subject と switchMap を組み合わせて再取得のトリガーを用意します。
private readonly refresh$ = new BehaviorSubject<void>(undefined);
readonly countries$ = this.refresh$.pipe(
switchMap(() => this.http.get<Country[]>('/api/countries')),
shareReplay(1),
);
reload(): void { this.refresh$.next(); }
重複リクエストの防止
同じ Observable を複数箇所で購読すると、その数だけリクエストが飛びます(コールドなため)。shareReplay / share で共有するほか、検索入力などでは switchMap で古いリクエストを破棄します。
results$ = this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((q) => this.api.search(q)), // 前のリクエストはキャンセルされる
);
SSR を使う場合は、サーバーで取得した HttpClient のレスポンスがクライアントへ自動で引き継がれ、再取得を避けられます。provideClientHydration(withHttpTransferCacheOptions(...)) で挙動を調整できます。
また、インターセプターで GET のレスポンスを Map に保持するシンプルなキャッシュも実装できます。
const cache = new Map<string, HttpResponse<unknown>>();
export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
if (req.method !== 'GET') return next(req);
const hit = cache.get(req.urlWithParams);
if (hit) return of(hit.clone());
return next(req).pipe(
tap((event) => {
if (event instanceof HttpResponse) cache.set(req.urlWithParams, event);
}),
);
};
Reactの useSWR, TanStack Query のキャッシュ機能との対比
| 機能 | Angular(標準) | SWR / TanStack Query |
|---|---|---|
| キャッシュ | shareReplay / 自前実装 | キーベースで自動 |
| 重複排除 | share / shareReplay | 同一キーを自動で統合 |
| 再検証(stale-while-revalidate) | 自前 | 標準機能 |
| フォーカス時の再取得 | 自前 | 標準機能 |
| リトライ | retry | 設定で指定 |
| ローディング/エラー状態 | 自前(シグナル等) | フックが返す |
Angular の標準機能は低レベルな部品の組み合わせです。キャッシュ無効化や再検証が多い大規模なアプリでは、TanStack Query の Angular アダプターの導入も選択肢になります。