メインコンテンツまでスキップ

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 HttpClientReact
クライアント組み込みfetch(標準)/ axios(外部)
戻り値Observable(遅延・キャンセル可能)Promise(即時実行)
JSON変換自動fetch は res.json() が必要
HTTPエラー4xx/5xx は自動でエラー扱いfetch は res.ok を自前で確認
キャンセル購読解除 / switchMapAbortController
キャッシュ・再取得標準では無し(自前または 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 InterceptorAxios interceptorsNext.js middleware
実行場所クライアント(SSR時はサーバー)クライアント/Nodeサーバー(Edge)、ページ到達前
対象HttpClient の送信リクエストaxios の送信リクエスト受信リクエスト
登録withInterceptors([...])axios.interceptors.request.usemiddleware.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 アダプターの導入も選択肢になります。