- Angular
Angular HttpClientの使い方|GET・POSTとエラー処理
Angular の HttpClient とは、サーバーの API と HTTP でやり取りするための標準サービスで、inject(HttpClient) で受け取り、http.get('/api/tasks') のようにメソッドを呼んで使います。 戻り値は Observable で、subscribe・firstValueFrom・toSignal のいずれかで結果を受け取ります。応答が 4xx・5xx だったときや通信できなかったときは、HttpErrorResponse がエラーとして届きます。
この記事では、GET・POST・PUT・PATCH・DELETE を型付きで書いたサービスの完成コード、params・headers・observe・responseType などのオプション早見表、status 0 / 4xx / 5xx それぞれの HttpErrorResponse の実際の中身、「No provider for HttpClient」の再現と直し方、subscribe・firstValueFrom・toSignal の使い分けをまとめました。掲載している出力は、ローカルに立てた小さな API サーバー(Node.js)に対して、ブラウザ(Chromium)から実際にリクエストを送って得たものです。
確認環境:Ubuntu 24.04 / Node.js 24.21.0 / Angular 22.2.1(@angular/common・@angular/cli)/ RxJS 7.8.2 / TypeScript 6.0.3 / Playwright 1.56.0 + Chromium 141 / Vitest 5.0.3。プロバイダーに関するエラーの再現だけ、Angular 20.3.33 のプロジェクトも使っています。
HttpClient の早見表
| やりたいこと | 書き方 | 戻り値の型 |
|---|---|---|
| 一覧を取得する | http.get<Task[]>('/api/tasks') |
Observable<Task[]> |
| 条件を付けて取得する | http.get<Task[]>('/api/tasks', { params: { done: false } }) |
Observable<Task[]> |
| 1件取得する | http.get<Task>('/api/tasks/1') |
Observable<Task> |
| 新規作成する | http.post<Task>('/api/tasks', { title: '...' }) |
Observable<Task> |
| 全体を置き換える | http.put<Task>('/api/tasks/1', task) |
Observable<Task> |
| 一部を更新する | http.patch<Task>('/api/tasks/1', { done: true }) |
Observable<Task> |
| 削除する | http.delete<void>('/api/tasks/1') |
Observable<void> |
| ヘッダーを付ける | { headers: { 'X-Request-Id': 'r-1' } } |
— |
| ステータスやヘッダーも受け取る | { observe: 'response' } |
Observable<HttpResponse<Task>> |
| 文字列で受け取る | { responseType: 'text' } |
Observable<string> |
| ファイルとして受け取る | { responseType: 'blob' } |
Observable<Blob> |
| 時間切れを設定する | { timeout: 300 }(ミリ秒) |
— |
| エラーを受け取る | subscribe({ error: (e: HttpErrorResponse) => ... }) または try / catch |
— |
| エラーを別の値に置き換える | .pipe(catchError(() => of([]))) |
— |
| 自動でやり直す | .pipe(retry({ count: 2, delay: 100 })) |
— |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angular HttpClientの使い方|GET・POSTとエラー処理」
準備:provideHttpClient の設定
HttpClient の設定は、app.config.ts の providers に provideHttpClient() を書いて行います。
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { authInterceptor } from './http/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor])),
],
};
Angular のバージョンによって、必要な書き方が少し変わります。公式ドキュメント(HTTP のセットアップ)と Angular 22.2.1 の型定義で確認した内容は次のとおりです。
| 項目 | Angular 22.2.1 での扱い |
|---|---|
| HttpClient の注入 | 公式ドキュメントに「Angular v21 以降は既定で注入できる」とあり、provideHttpClient() を書かなくても動く |
| 通信の方式 | 既定で fetch API を使う |
withFetch() |
型定義で非推奨(withFetch is not required anymore)。書いてあっても動く |
withXhr() |
XMLHttpRequest を使う方式に切り替える。アップロードの進捗を取りたいときに使う |
withInterceptors([...]) |
関数形式のインターセプターを登録する |
Angular 22 では、インターセプターなどの追加設定がなければ provideHttpClient() 自体を省けます。 実際に、provideHttpClient を一切書かない状態でこの記事の CRUD をすべて実行でき、Vitest でも provideHttpClient(withFetch()) と書いた設定が引き続き通ることを確かめました。一方、Angular 20 までのプロジェクトでは provideHttpClient() が必要です(後半の「No provider for HttpClient」の節で再現します)。どのバージョンでも動く形にしておきたいなら、provideHttpClient() を書いておくのが分かりやすい選択です。
型付きの完成コード:GET・POST・PUT・PATCH・DELETE
HTTP の呼び出しは、コンポーネントに直接書かずにサービスにまとめるのが基本です。次の TaskApi は、タスクの一覧・取得・作成・置き換え・一部更新・削除をひととおり備えています。
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
export interface Task {
id: number;
title: string;
done: boolean;
}
export type NewTask = Pick<Task, 'title'>;
@Injectable({ providedIn: 'root' })
export class TaskApi {
private readonly http = inject(HttpClient);
private readonly base = '/api/tasks';
/** GET /api/tasks?done=false */
list(filter?: { done?: boolean }): Observable<Task[]> {
let params = new HttpParams();
if (filter?.done !== undefined) params = params.set('done', filter.done);
return this.http.get<Task[]>(this.base, { params });
}
/** GET /api/tasks/1 */
get(id: number): Observable<Task> {
return this.http.get<Task>(`${this.base}/${id}`);
}
/** POST /api/tasks(本文は JSON に変換されて送られる) */
create(task: NewTask): Observable<Task> {
return this.http.post<Task>(this.base, task);
}
/** POST でステータスやヘッダーも受け取る */
createWithResponse(task: NewTask): Observable<HttpResponse<Task>> {
return this.http.post<Task>(this.base, task, { observe: 'response' });
}
/** PUT /api/tasks/1(全体を置き換える) */
replace(task: Task): Observable<Task> {
return this.http.put<Task>(`${this.base}/${task.id}`, task);
}
/** PATCH /api/tasks/1(一部だけ変える) */
update(id: number, changes: Partial<Omit<Task, 'id'>>): Observable<Task> {
return this.http.patch<Task>(`${this.base}/${id}`, changes);
}
/** DELETE /api/tasks/1(204 No Content なので本文なし) */
remove(id: number): Observable<void> {
return this.http.delete<void>(`${this.base}/${id}`);
}
}
http.get<Task[]>(...) の山かっこの中が、応答の本文の型です。この型は「こういう形で返ってくるはず」という宣言で、実行時に中身を検査するものではありません。 サーバーの応答と合っていることは、API の仕様やテストで確かめます。
実行結果
ブラウザから順に呼んだときの戻り値です。
list: [{"id":1,"title":"備品を発注する","done":false},{"id":2,"title":"議事録を書く","done":true}]
list(done=false): [{"id":1,"title":"備品を発注する","done":false}]
get(1): {"id":1,"title":"備品を発注する","done":false}
create: {"id":3,"title":"在庫を数える","done":false}
createWithResponse: status=201 ok=true Location=/api/tasks/4 body={"id":4,"title":"棚を整理する","done":false}
replace: {"id":3,"title":"在庫を数え直す","done":false}
update: {"id":3,"title":"在庫を数え直す","done":true}
remove: null
delete observe=response: status=204 body=null
サーバー側に届いたリクエストは次のとおりでした。
GET /api/tasks
GET /api/tasks?done=false
GET /api/tasks/1
POST /api/tasks
POST /api/tasks
PUT /api/tasks/3
PATCH /api/tasks/3
DELETE /api/tasks/4
DELETE /api/tasks/3
| メソッド | 第2引数 | ポイント |
|---|---|---|
get(url, options) |
オプション | 本文は送らない。条件は params で渡す |
post(url, body, options) |
本文 | オブジェクトを渡すと JSON に変換して送られる |
put(url, body, options) |
本文 | 対象の全体を、送った内容で置き換える |
patch(url, body, options) |
本文 | 渡した項目だけ変える(update の結果で title はそのまま) |
delete(url, options) |
オプション | 204 No Content のときは null が流れる。型は void にしておくと扱いやすい |
post・put・patch は第2引数が本文、第3引数がオプションです。get と delete はオプションが第2引数なので、書き換えるときに位置を間違えやすい点です。
オプション早見表:params・headers・observe・responseType
メソッドの最後の引数に渡すオプションです。Angular 22.2.1 の型定義にある項目を一覧にしました。
| オプション | 指定できる値 | 働き |
|---|---|---|
params |
オブジェクト、または HttpParams |
クエリ文字列を付ける。値はエンコードされる |
headers |
オブジェクト、または HttpHeaders |
リクエストヘッダーを付ける |
observe |
'body'(既定) / 'response' / 'events' |
何を受け取るか。本文だけ、応答全体、途中経過を含むイベント |
responseType |
'json'(既定) / 'text' / 'blob' / 'arraybuffer' |
本文の読み取り方 |
timeout |
ミリ秒 | 指定時間で応答がなければ中断してエラーにする |
withCredentials |
true / false |
別オリジンへのリクエストに Cookie などを含める |
reportProgress |
true / false |
observe: 'events' と組み合わせて進捗イベントを受け取る |
context |
HttpContext |
インターセプターに渡す目印を付ける |
transferCache |
オブジェクト、または真偽値 | サーバー側レンダリングで取得した応答の引き継ぎを調整する |
keepalive |
true / false |
ページを離れたあともリクエストを続ける |
cache |
'no-cache' など |
ブラウザの HTTP キャッシュとの関わり方 |
priority |
'high' / 'low' / 'auto' |
リクエストの優先度の目安 |
mode |
'cors' / 'same-origin' / 'no-cors' |
別オリジンの扱い |
redirect |
'follow' / 'error' / 'manual' |
リダイレクト応答の扱い |
credentials |
'include' など |
認証情報を送る範囲 |
referrer / referrerPolicy |
URL / ポリシー名 | リファラーの内容と送る範囲 |
integrity |
ハッシュ文字列 | 応答が改ざんされていないかの検証 |
keepalive から下は fetch API 由来のオプションで、意味は公式ドキュメント(リクエストの作成)の説明に沿っています。
params と headers の実際の送られ方
受け取ったクエリ文字列とヘッダーをそのまま返す API に送って確かめました。
http.get('/api/echo', { params: { q: '備品 A&B', page: 2, tag: ['x', 'y'], done: false } });
http.get('/api/echo', { headers: new HttpHeaders({ Authorization: 'Bearer abc' }).set('X-Request-Id', 'r-1') });
http.get('/api/echo', { headers: { 'X-Request-Id': 'r-2' } });
params object: {"query":"?q=%E5%82%99%E5%93%81%20A%26B&page=2&tag=x&tag=y&done=false","authorization":null,"xRequestId":null,"accept":"application/json, text/plain, */*"}
headers: {"query":"","authorization":"Bearer abc","xRequestId":"r-1","accept":"application/json, text/plain, */*"}
headers object: {"query":"","authorization":null,"xRequestId":"r-2","accept":"application/json, text/plain, */*"}
- 日本語・空白・
&は自動でエンコードされる(備品 A&B→%E5%82%99%E5%93%81%20A%26B)。URL を文字列の連結で組み立てるより、paramsに任せるほうが確実です。 - 配列は同じキーの繰り返しになる(
tag=x&tag=y)。数値や真偽値もそのまま渡せます。 HttpParamsとHttpHeadersは変更できないオブジェクトです。set()は新しいオブジェクトを返すので、params = params.set('done', true)のように受け直します(TaskApi.listの書き方)。- 何も指定しなくても、
Accept: application/json, text/plain, */*が付きます。
observe と responseType の実際の戻り値
responseType text: string "v1.2.3"
responseType blob: size=33 type=text/csv; charset=utf-8
responseType arraybuffer: byteLength=33
observe events: ["Sent","ResponseHeader","DownloadProgress","Response"]
| 指定 | 受け取れるもの | 使いどころ |
|---|---|---|
| 指定なし | JSON を変換した本文 | ほとんどの API |
observe: 'response' |
HttpResponse(status・headers・body) |
201 の Location ヘッダー、ページ送り用のヘッダーなどを読みたい |
observe: 'events' + reportProgress: true |
送信・ヘッダー受信・ダウンロード進捗・完了のイベント | 進捗バーを出したい |
responseType: 'text' |
文字列 | JSON 以外のテキストを返す API |
responseType: 'blob' |
Blob |
CSV・PDF・画像などのダウンロード |
responseType に 'text' などを指定するときは、get<string>(...) のような型引数は付けません。戻り値の型は指定に合わせて決まります。
エラー処理:HttpErrorResponse の中身を status ごとに見る
HttpClient のエラーは、HttpErrorResponse というオブジェクトで届きます。 まず、状況ごとに各プロパティが実際にどうなったかを一覧にします。
| 状況 | status | statusText | error の中身 |
|---|---|---|---|
| 接続できない(サーバーが動いていないポート) | 0 |
Unknown Error |
TypeError: Failed to fetch |
| CORS で止められた(別オリジンで許可ヘッダーなし) | 0 |
Unknown Error |
TypeError: Failed to fetch |
timeout: 300 で時間切れ |
0 |
Unknown Error |
TimeoutError: signal timed out |
| 400(入力エラー) | 400 |
Bad Request |
{"message":"title は必須です","field":"title"} |
| 401(ログインが必要) | 401 |
Unauthorized |
{"message":"ログインが必要です"} |
| 404(見つからない) | 404 |
Not Found |
{"message":"タスク 999 は見つかりません"} |
| 500(本文がテキスト) | 500 |
Internal Server Error |
"Internal Server Error"(文字列) |
| 200 だが JSON として読めない | 200 |
OK |
SyntaxError: Expected double-quoted property name in JSON ... |
| 200 のテキストを JSON として受けた | 200 |
OK |
SyntaxError: Unexpected token 'v', "v1.2.3" is not valid JSON |
message プロパティは、次のような文になります。
404: Http failure response for http://127.0.0.1:8760/api/tasks/999: 404 Not Found
500: Http failure response for http://127.0.0.1:8760/api/error500: 500 Internal Server Error
接続できない: Http failure response for http://127.0.0.1:8769/api/tasks: 0 undefined
JSON として読めない: Http failure during parsing for http://127.0.0.1:8760/api/broken
どの場合も name は HttpErrorResponse、ok は false でした。
3つのグループに分けて考える
| グループ | 見分け方 | 何が起きているか | 画面での案内の例 |
|---|---|---|---|
| status 0 | e.status === 0 |
応答を受け取れていない(接続・CORS・時間切れ) | 通信環境の確認と再試行をお願いする |
| 4xx | e.status >= 400 && e.status < 500 |
リクエストの内容に理由がある | e.error の内容をもとに、直す箇所を伝える |
| 5xx | e.status >= 500 |
サーバー側で処理できなかった | 時間をおいての再試行をお願いする |
status 0 のときは、接続できないのか CORS なのかを HttpErrorResponse からは区別できません(どちらも TypeError: Failed to fetch)。原因を調べるときは、ブラウザの開発者ツールのコンソールを見ます。今回の実行では、接続できない場合は net::ERR_CONNECTION_REFUSED、CORS の場合は has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present と、別々のメッセージが出ていました。
4xx・5xx のとき、e.error にはサーバーが返した本文が入ります。 JSON ならオブジェクト、テキストなら文字列です。サーバーが {"message": "..."} の形で返す約束になっていれば、e.error?.message をそのまま画面に出せます。
ステータスが 200 でも、本文を JSON として読めなければエラーになります。 テキストを返す API を responseType の指定なしで呼んだ場合がこれにあたり、responseType: 'text' を付けると解決します。
エラーを文章に変える関数
status ごとの分岐は、1つの関数にまとめておくと各画面で使い回せます。
import { HttpErrorResponse } from '@angular/common/http';
/** HttpErrorResponse を、画面に出す文章に変える */
export function toMessage(e: unknown): string {
if (!(e instanceof HttpErrorResponse)) return '予期しないエラーが発生しました。';
if (e.status === 0) return 'サーバーに接続できませんでした。通信環境を確認して、もう一度お試しください。';
if (e.status === 200) return 'サーバーからの応答を読み取れませんでした。';
if (e.status === 400) return e.error?.message ?? '入力内容を確認してください。';
if (e.status === 401) return 'ログインが必要です。';
if (e.status === 404) return e.error?.message ?? '対象が見つかりませんでした。';
if (e.status >= 500) return 'サーバー側で問題が起きています。時間をおいてお試しください。';
return `エラーが発生しました(${e.status})。`;
}
subscribe の error で受け取る
api.get(999).subscribe({
next: (t) => console.log('next: ' + JSON.stringify(t)),
error: (e: HttpErrorResponse) => console.log(`error: ${e.status} ${JSON.stringify(e.error)}`),
complete: () => console.log('complete'),
});
存在する ID(1)と存在しない ID(999)で呼んだ結果です。
next: {"id":1,"title":"備品を発注する","done":false}
complete
error: 404 {"message":"タスク 999 は見つかりません"}
成功すると next が1回、続けて complete が呼ばれます。エラーのときは error だけが呼ばれ、complete は呼ばれません。 「読み込み中」の表示を消す処理は、complete だけでなく error 側にも必要です(RxJS の finalize を使うと1か所にまとめられます)。
catchError と retry で流れの中で処理する
// 404 のときは null に置き換え、それ以外のエラーはそのまま流す
api.get(999).pipe(
catchError((e: HttpErrorResponse) => (e.status === 404 ? of(null) : throwError(() => e))),
);
// エラーになったら 100 ミリ秒あけて最大2回やり直す
http.get('/api/flaky').pipe(retry({ count: 2, delay: 100 }));
catchError で null に置き換え: null
retry({count: 2, delay: 100}): {"attempt":3}
/api/flaky は「3回に1回だけ成功する」API です。サーバーのログには GET /api/flaky が3回並び、3回目の応答(attempt: 3)が届きました。
| 演算子 | 働き | 向いている場面 |
|---|---|---|
catchError(() => of(代わりの値)) |
エラーを通常の値に置き換える | 一覧が取れなければ空の配列で表示を続ける |
catchError((e) => throwError(() => e)) |
記録などをしてから、エラーのまま流す | サービスでログを取り、画面側でメッセージを出す |
retry({ count, delay }) |
同じリクエストをやり直す | 一時的な混雑や通信の揺らぎ |
timeout オプション |
指定時間で中断する | 応答を待ち続けたくない処理 |
retry は GET のような「何度送っても結果が同じ」リクエストに使うのが基本です。POST をやり直すと、サーバー側の作りによっては同じデータが2件できることがあります。
catchError(() => EMPTY) で「何も流さずに終わる」形にした場合、firstValueFrom と組み合わせると EmptyError: no elements in sequence になりました。firstValueFrom を使う箇所では、of(null) のように何か1つ値を流す形にしておきます。
subscribe・firstValueFrom・toSignal の使い分け
HttpClient が返す Observable は、購読されるまで何も送りません。 また、購読するたびにリクエストが送られます。同じ Observable を2回 subscribe したところ、サーバーには2回届きました。
GET /api/echo?cold=1
GET /api/echo?cold=1
「メソッドを呼んだのにリクエストが飛ばない」ときは、購読を書き忘れていないかを確認しましょう。受け取り方は3つあります。
| 方法 | 書き方 | 向いている場面 | 気を付ける点 |
|---|---|---|---|
toSignal |
tasks = toSignal(api.list(), { initialValue: [] }) |
画面を開いたときに読み込んで表示するデータ | 注入コンテキスト(フィールド初期化やコンストラクター)で呼ぶ。エラーは catchError で値に変えておく |
subscribe |
api.list().subscribe({ next, error }) |
成功時とエラー時で別々の処理をしたい | コンポーネントで使うときは takeUntilDestroyed() を挟む |
firstValueFrom |
const t = await firstValueFrom(api.create(...)) |
ボタン操作など、1回送って結果を待つ処理 | エラーは try / catch で受け取る |
3つを使ったコンポーネントの完成コード
import { Component, DestroyRef, inject, signal } from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { firstValueFrom, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { toMessage } from './http-error-message';
import { Task, TaskApi } from './task-api';
@Component({
selector: 'app-task-list',
template: `
<h2>タスク</h2>
<p id="count">完了済み(初回読み込み時): {{ doneTasks().length }}件</p>
<input #title id="new-title" placeholder="タスク名" />
<button id="add" (click)="add(title.value); title.value = ''" [disabled]="saving()">追加</button>
@if (message(); as m) {
<p id="message" role="alert">{{ m }}</p>
}
<ul id="tasks">
@for (t of tasks(); track t.id) {
<li>
{{ t.title }}
<button class="remove" (click)="remove(t.id)">削除</button>
</li>
}
</ul>
`,
})
export class TaskList {
private readonly api = inject(TaskApi);
private readonly destroyRef = inject(DestroyRef);
protected readonly tasks = signal<Task[]>([]);
protected readonly saving = signal(false);
protected readonly message = signal('');
// ① toSignal:読み込んで表示するだけのデータ。購読と解除を任せられる
protected readonly doneTasks = toSignal(
this.api.list({ done: true }).pipe(catchError(() => of([] as Task[]))),
{ initialValue: [] as Task[] },
);
constructor() {
// ② subscribe:next / error を分けて書く。コンポーネント破棄時に自動で解除
this.api
.list()
.pipe(takeUntilDestroyed())
.subscribe({
next: (list) => this.tasks.set(list),
error: (e) => this.message.set(toMessage(e)),
});
}
// ③ firstValueFrom:ボタン操作など「1回送って結果を待つ」処理を async/await で書く
protected async add(title: string): Promise<void> {
this.saving.set(true);
this.message.set('');
try {
const created = await firstValueFrom(this.api.create({ title }));
this.tasks.update((list) => [...list, created]);
} catch (e) {
this.message.set(toMessage(e));
} finally {
this.saving.set(false);
}
}
protected remove(id: number): void {
this.api
.remove(id)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: () => this.tasks.update((list) => list.filter((t) => t.id !== id)),
error: (e) => this.message.set(toMessage(e)),
});
}
}
ブラウザで、表示→追加→タスク名を空のまま追加→削除→通信を遮断して追加、の順に操作した結果です。
表示直後: ["備品を発注する","議事録を書く"] / 完了済み(初回読み込み時): 1件 / message=null
追加後: ["備品を発注する","議事録を書く","在庫を数える"] / 完了済み(初回読み込み時): 1件 / message=null
空で追加: ["備品を発注する","議事録を書く","在庫を数える"] / 完了済み(初回読み込み時): 1件 / message="title は必須です"
削除後: ["議事録を書く","在庫を数える"] / 完了済み(初回読み込み時): 1件 / message="title は必須です"
通信を遮断して追加: ["議事録を書く","在庫を数える"] / 完了済み(初回読み込み時): 1件 / message="サーバーに接続できませんでした。通信環境を確認して、もう一度お試しください。"
400 のときはサーバーが返した message が、通信できなかったとき(status 0)は toMessage の文章が表示されています。どちらの場合も一覧はそのまま残り、利用者はすぐに操作を続けられます。
公式ドキュメントでは、購読を解除すると進行中のリクエストが取り消されること、async パイプや toSignal を使うと購読の後始末を任せられることが説明されています。画面を離れたあとに届いた応答で処理が走らないよう、コンポーネント内の subscribe には takeUntilDestroyed() を付けておくと安心です。
インターセプターで共通の処理をまとめる
すべてのリクエストに同じヘッダーを付けたい、エラーを1か所で記録したい、というときはインターセプターを使います。関数として書き、provideHttpClient(withInterceptors([...])) で登録します(登録は冒頭の app.config.ts を参照)。
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';
/** すべてのリクエストにトークンを付け、エラーを1か所で記録する */
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const withToken = req.clone({ setHeaders: { Authorization: 'Bearer demo-token' } });
return next(withToken).pipe(
catchError((e: HttpErrorResponse) => {
console.warn(`[http] ${req.method} ${req.url} -> ${e.status}`);
return throwError(() => e);
}),
);
};
先ほどの画面操作の裏で、ブラウザから送られたリクエスト(上の6行)と、コンソールに出た警告(下の2行)は次のとおりでした。
GET /api/tasks?done=true Authorization=Bearer demo-token
GET /api/tasks Authorization=Bearer demo-token
POST /api/tasks Authorization=Bearer demo-token
POST /api/tasks Authorization=Bearer demo-token
DELETE /api/tasks/1 Authorization=Bearer demo-token
POST /api/tasks Authorization=Bearer demo-token
[console.warn] [http] POST /api/tasks -> 400
[console.warn] [http] POST /api/tasks -> 0
リクエストは変更できないオブジェクトなので、req.clone({ setHeaders: {...} }) で複製してから next に渡します。 サービス側のコードを変えずに、全リクエストへ同じ処理を足せるのが利点です。
No provider for HttpClient の再現と直し方
NullInjectorError: No provider for HttpClient! は、HttpClient を注入しようとしたが、プロバイダーが登録されていないときのエラーです。公式ドキュメントでは、エラーコード NG0201(No Provider Found)として「プロバイダーを宣言していないサービスを注入しようとすると発生する」と説明されています。
Angular 20 で再現する
Angular 20.3.33 のプロジェクトで、app.config.ts に provideHttpClient を書かずに、コンポーネントで inject(HttpClient) だけを行いました。
import { HttpClient } from '@angular/common/http';
import { Component, inject } from '@angular/core';
@Component({
selector: 'app-root',
template: `<h1>tasks</h1>`,
})
export class App {
private readonly http = inject(HttpClient);
}
ng build(本番用ビルド)は通り、ブラウザで開くとコンソールに次のエラーが出て、画面には何も表示されませんでした。
[console.error] ERROR m: NG0201
at Et.get (http://127.0.0.1:8762/main-VCTJNSPX.js:4:6413)
...
本番用ビルドではメッセージが短縮され、エラーコードの NG0201 だけが表示されます。コンソールに NG0201 と出て画面が真っ白なら、プロバイダーの登録もれを最初に確認しましょう。
直し方:app.config.ts に provideHttpClient を足す
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(withFetch())
]
};
この1行を足してビルドし直すと、エラーは消えて tasks の見出しが表示されました。
バージョンごとの整理
| 状況 | 結果 | 対応 |
|---|---|---|
Angular 20.3.33 で provideHttpClient なし |
NG0201 で画面が表示されない |
providers に provideHttpClient()(fetch を使うなら withFetch() も)を足す |
Angular 22.2.1 で provideHttpClient なし |
そのまま動く | 対応不要。インターセプターなどを使うときに書く |
Angular 22.2.1 のテストで HttpTestingController を注入 |
NG0201: No provider found for HttpTestingController. |
テストの providers に provideHttpClientTesting() を足す |
3行目は Angular 22.2.1 のテストで実際に出たメッセージで、全文は次のとおりです。
NG0201: No provider found for `HttpTestingController`. Find more at https://v22.angular.dev/errors/NG0201
「No provider」と出たら、名前に入っているサービスを提供する provide〜() 関数を providers に足す、と覚えておけば、HttpClient 以外でも同じ手順で解決できます。
HttpClient を使うサービスのテスト
provideHttpClientTesting() を使うと、実際の通信をせずに「どんなリクエストが送られたか」と「応答を受けたときの動き」を確かめられます。次のテストは Vitest(ng test)で通ったものです。
import { HttpErrorResponse, provideHttpClient, withInterceptors } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { firstValueFrom } from 'rxjs';
import { authInterceptor } from './auth.interceptor';
import { TaskApi } from './task-api';
describe('TaskApi', () => {
let api: TaskApi;
let http: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideHttpClient(withInterceptors([authInterceptor])), provideHttpClientTesting()],
});
api = TestBed.inject(TaskApi);
http = TestBed.inject(HttpTestingController);
});
afterEach(() => http.verify());
it('list は done を params に付ける', async () => {
const promise = firstValueFrom(api.list({ done: false }));
const req = http.expectOne('/api/tasks?done=false');
expect(req.request.method).toBe('GET');
expect(req.request.headers.get('Authorization')).toBe('Bearer demo-token');
req.flush([{ id: 1, title: '備品を発注する', done: false }]);
expect(await promise).toHaveLength(1);
});
it('create は JSON を POST する', async () => {
const promise = firstValueFrom(api.create({ title: '在庫を数える' }));
const req = http.expectOne({ method: 'POST', url: '/api/tasks' });
expect(req.request.body).toEqual({ title: '在庫を数える' });
req.flush({ id: 3, title: '在庫を数える', done: false }, { status: 201, statusText: 'Created' });
expect((await promise).id).toBe(3);
});
it('404 は HttpErrorResponse として届く', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
const promise = firstValueFrom(api.get(999));
http.expectOne('/api/tasks/999').flush({ message: '見つかりません' }, { status: 404, statusText: 'Not Found' });
await expect(promise).rejects.toBeInstanceOf(HttpErrorResponse);
await expect(promise).rejects.toMatchObject({ status: 404, error: { message: '見つかりません' } });
expect(warn).toHaveBeenCalledWith('[http] GET /api/tasks/999 -> 404');
});
});
✓ demo src/app/http/task-api.spec.ts > TaskApi > list は done を params に付ける 23ms
✓ demo src/app/http/task-api.spec.ts > TaskApi > create は JSON を POST する 12ms
✓ demo src/app/http/task-api.spec.ts > TaskApi > 404 は HttpErrorResponse として届く 6ms
flush の第2引数に { status: 404, statusText: 'Not Found' } を渡すと、エラー応答も再現できます。画面のエラー表示をテストするときにも同じ方法が使えます。
つまずきやすい点のチェックリスト
| 起きること | 確認するところ | 直し方 |
|---|---|---|
| メソッドを呼んでもリクエストが送られない | 購読しているか | subscribe・firstValueFrom・toSignal・async パイプのいずれかで受け取る |
| 同じリクエストが2回送られる | 同じ Observable を2か所で購読していないか | 購読を1か所にまとめ、結果を signal に入れて共有する |
| 200 なのにエラーになる | 応答が JSON か | テキストなら responseType: 'text' を付ける |
| status が 0 で原因がわからない | 開発者ツールのコンソール | 接続先の URL・ポート、サーバー側の CORS 設定を確認する |
post に渡したオプションが効かない |
引数の位置 | 第2引数は本文、オプションは第3引数 |
params.set() したのに反映されない |
戻り値を受け直しているか | params = params.set(...) と書く |
| エラーのあと「読み込み中」が消えない | error 側の処理 |
finalize か try / finally で後始末する |
NG0201 で画面が出ない |
providers |
provideHttpClient() を足す(Angular 20 まで) |
よくある質問
Q. HttpClientModule を imports する書き方との違いは何ですか。
A. provideHttpClient() は、app.config.ts の providers に書く関数形式の設定で、standalone 構成の Angular で使う形です。withInterceptors() などの機能を引数で足していけます。Angular 22.2.1 では HttpClient が既定で注入できるため、追加の設定がなければ何も書かなくても使えます。
Q. fetch API を直接使うのと何が違いますか。
A. HttpClient は、JSON の変換、型の指定、4xx・5xx をエラーとして扱うこと、インターセプター、テスト用の HttpTestingController をまとめて提供します。Angular 22.2.1 の HttpClient は内部で fetch API を使っているので、keepalive や priority などの fetch 由来のオプションも渡せます。
Q. エラー処理はサービスとコンポーネントのどちらに書きますか。
A. 「全リクエスト共通の処理(記録、トークンの付与)」はインターセプター、「この API 固有の置き換え(404 なら null)」はサービス、「利用者へのメッセージ表示」はコンポーネント、と分けると見通しがよくなります。この記事の authInterceptor・TaskApi・TaskList はその分け方にしています。
Q. 画面に表示するためのデータ取得なら、ほかの選択肢はありますか。
A. Angular 22.2.1 の @angular/common/http には、取得したデータをシグナルとして扱う httpResource も入っています。読み込みは httpResource、作成・更新・削除は HttpClient、という組み合わせも選べます。
まとめ
- HttpClient は
inject(HttpClient)で受け取り、get・post・put・patch・deleteに型引数を付けて呼ぶ。呼び出しはサービスにまとめる。 - Angular 22 では HttpClient が既定で使え、通信は fetch API。
provideHttpClient()はインターセプターなどの設定を足すときに書く。Angular 20 までは必須。 - オプションは
params・headers・observe・responseTypeが中心。post系は第3引数に渡す。 - エラーは
HttpErrorResponse。status 0 は応答なし、4xx はリクエスト側、5xx はサーバー側。e.errorにサーバーの本文が入る。 - 200 でも JSON として読めなければエラー。テキストは
responseType: 'text'で受け取る。 - 表示用は
toSignal、分岐したいときはsubscribe、1回の操作はfirstValueFrom。購読しなければリクエストは送られない。 NG0201(No provider)はprovideHttpClient()の登録を確認。テストではprovideHttpClientTesting()。
参考
- Angular 公式ドキュメント「Setting up HttpClient」 https://angular.dev/guide/http/setup
- Angular 公式ドキュメント「Making HTTP requests」 https://angular.dev/guide/http/making-requests
- Angular 公式ドキュメント「NG0201: No Provider Found」 https://angular.dev/errors/NG0201
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Angular HttpClientの使い方|GET・POSTとエラー処理」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。