Angular基礎 #7 HttpClient で API 呼び出し

読了 7分

前回は Router で複数ページを構成する方法を学びました。これで最後のパズルピースが残っています。画面を出してルーティングまでしたら、結局は バックエンドサーバーからデータを受け取ってこそ 本当のアプリになります。今回は Angular の標準通信ツール HttpClient を扱っていきます。

HttpClient セットアップ #

Angular は fetch や axios の代わりに自前の HTTP クライアントである HttpClient を推奨します。インターセプタ、テストユーティリティ、RxJS 統合が一式入っており、Angular の SSR (サーバーサイドレンダリング) でも自然に動作します。

まずアプリ設定に 1 行追加する必要があります。Standalone パターンでは app.config.tsprovideHttpClient() を登録します。

src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
  ],
};

この 1 行でアプリ全体で HttpClient を注入して使えるようになります。

注記
昔の HttpClientModule を import する方式は NgModule 時代のパターンです。新しいプロジェクトでは provideHttpClient() を使ってください。Standalone 時代の標準です。

GET リクエスト #

もっとも基本的な GET リクエストから見ていきます。ユーザー一覧を取ってくるサービスを作ってみます。

src/app/user.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  private apiUrl = 'https://jsonplaceholder.typicode.com/users';

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }
}

3 つのポイントがあります。

  1. inject(HttpClient) で HttpClient を注入してもらいます。コンストラクタ注入 (constructor(private http: HttpClient)) も依然として動作しますが、最近の Angular では inject() の関数型注入が好まれます。
  2. get<User[]>(url) のように ジェネリクスでレスポンスの型 を指定します。そうすると getUsers() の戻り値の型が Observable<User[]> と推論され、型安全に扱えます。
  3. HttpClient のメソッドは すぐにリクエストを送らず Observable を返します。誰かが購読 (subscribe) して初めてリクエストが出ます。

Observable を扱う #

サービスが返した Observable をコンポーネントで受け取って画面に描画してみましょう。もっとも単純な方法は subscribe で直接結果を受け取ることです。

src/app/user-list.component.ts
import { Component, OnInit, inject, signal } from '@angular/core';
import { UserService, User } from './user.service';

@Component({
  selector: 'app-user-list',
  standalone: true,
  template: `
    <ul>
      @for (user of users(); track user.id) {
        <li>{{ user.name }} ({{ user.email }})</li>
      }
    </ul>
  `,
})
export class UserListComponent implements OnInit {
  private userService = inject(UserService);
  users = signal<User[]>([]);

  ngOnInit() {
    this.userService.getUsers().subscribe(data => {
      this.users.set(data);
    });
  }
}

subscribe のコールバックの中で受け取ったデータをシグナルに入れ、テンプレートは users() を呼び出して自動的に更新されます。

ただし subscribe を直接使うときには メモリリーク に注意する必要があります。コンポーネントが消えても購読が生き残ると、もはや存在しないコンポーネントの状態を更新しようとしてエラーやリークが発生する可能性があります。この問題は次のシリーズ (中級) で本格的に扱いますが、今から推奨できるより良いパターンが 1 つあります。

toSignal で Observable → Signal 変換 #

Angular 16 から入った toSignal は Observable をシグナルに自動変換してくれます。購読解除を気にしなくてもコンポーネントが消えると自動で整理され、テンプレートではただシグナルのように呼び出せば構いません。

src/app/user-list.component.ts
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-list',
  standalone: true,
  template: `
    @if (users(); as list) {
      <ul>
        @for (user of list; track user.id) {
          <li>{{ user.name }} ({{ user.email }})</li>
        }
      </ul>
    } @else {
      <p>読み込み中...</p>
    }
  `,
})
export class UserListComponent {
  private userService = inject(UserService);
  users = toSignal(this.userService.getUsers());
}

subscribe がなくなり、OnInit も必要なくなりました。toSignal が内部で自動的に購読・解除まで処理してくれるからです。最初は undefined を返すので、上のように @if (users(); as list) でロード状態を分岐できます。

ヒント
新しいコードを書くときは可能な限り toSignal を優先的に検討してください。直接 subscribe するパターンよりリークの危険性が少なく、シグナルベースのテンプレートともよく合います。

POST/PUT/DELETE リクエスト #

読み取りだけのアプリはほぼありません。データを作って修正して消すメソッドも似た形で書きます。

src/app/user.service.ts
// 作成
createUser(user: Omit<User, 'id'>): Observable<User> {
  return this.http.post<User>(this.apiUrl, user);
}

// 全体の修正
updateUser(user: User): Observable<User> {
  return this.http.put<User>(`${this.apiUrl}/${user.id}`, user);
}

// 削除
deleteUser(id: number): Observable<void> {
  return this.http.delete<void>(`${this.apiUrl}/${id}`);
}

postput の 2 番目の引数が リクエストの body です。JSON のシリアライズは HttpClient が自動でやってくれます。呼び出す側は GET と同じく Observable を受け取って処理します。

コンポーネントから呼び出し
this.userService
  .createUser({ name: '太郎', email: 'taro@example.com' })
  .subscribe(created => console.log('作成されました:', created));

エラー処理 #

ネットワークが常にうまく動作すると仮定してはいけません。Angular は RxJS の catchError 演算子でエラーを横取りする方式を推奨します。

src/app/user.service.ts
import { catchError, EMPTY } from 'rxjs';

getUsers(): Observable<User[]> {
  return this.http.get<User[]>(this.apiUrl).pipe(
    catchError(err => {
      console.error('ユーザー一覧の取得に失敗しました:', err);
      return EMPTY;
    })
  );
}

pipe は Observable に演算子を並べて適用するための通り道です。catchError はエラーが発生したときに元の流れを差し替える新しい Observable を返さなければなりません。上の例のように EMPTY (何も値を流さずすぐに終了) を返すと、購読者にはただデータが来なかったように見えます。

デフォルト値を流したければ of([]) を使えば構いません。

import { of } from 'rxjs';

return this.http.get<User[]>(this.apiUrl).pipe(
  catchError(() => of([])) // エラー時は空配列でフォールバック
);

ヘッダーとオプション #

API に認証トークンを送ったり query parameter を付ける必要があるときが多いです。HttpClient の 2 (または 3) 番目の引数にオプションオブジェクトを渡せば構いません。

ヘッダーと query params
import { HttpHeaders, HttpParams } from '@angular/common/http';

getPosts(userId: number, token: string): Observable<Post[]> {
  const headers = new HttpHeaders({
    Authorization: `Bearer ${token}`,
  });

  const params = new HttpParams().set('userId', userId);

  return this.http.get<Post[]>('/api/posts', { headers, params });
}

HttpParams を使わず単にオブジェクトとして渡す短い形式も可能です。

this.http.get<Post[]>('/api/posts', {
  params: { userId, limit: 10 },
});

Interceptor 一行紹介 #

リクエストごとにトークンを直接付けるのは面倒です。Angular はすべての HTTP リクエストを横取りして共通処理を差し込む Interceptor を提供します。

src/app/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = localStorage.getItem('token');
  if (!token) return next(req);

  const cloned = req.clone({
    setHeaders: { Authorization: `Bearer ${token}` },
  });
  return next(cloned);
};

app.config.ts に登録すれば終わりです。

src/app/app.config.ts
provideHttpClient(withInterceptors([authInterceptor])),

リクエストのロギング、トークンの更新、共通エラーハンドリング、キャッシュ — これらすべてがインターセプタ 1 箇所で整理されます。詳しいパターンは中級シリーズで扱う予定なので、今は「こういうものがある」程度に覚えておいてください。

まとめ #

今回の記事では HttpClient でバックエンドと通信する方法を整理しました。

  • provideHttpClient() でセットアップして inject(HttpClient) で注入してもらう
  • get<T> / post<T> / put<T> / delete<T> — ジェネリクスで型安全に
  • 結果は Observable。直接 subscribe するか toSignal でシグナルに変換して扱う
  • catchError + EMPTY / of でエラーフォールバック
  • HttpHeadersHttpParams あるいはオブジェクトでヘッダーと query params
  • Interceptor で共通処理 (認証・ロギング・エラー) を 1 箇所にまとめる

ここまでが Angular基礎 の最後の記事です。1 編で Angular とは何かから始まり、コンポーネントとテンプレート構文、データバインディング、Directive と Pipe、Service と依存性注入、Router、そして今日の HttpClient まで — 小さな Angular アプリを最初から最後まで作るのに必要な核心ツールはすべて 1 度ずつ扱いました。これで皆さんは Angular で「データを受け取って画面に描画して、ユーザーの入力を処理して、複数のページを行き来する」基本を備えたことになります。

次のステップは 「Angular 中級講座」 です。基礎では意図的に後回しにしたテーマを本格的に扱う予定です。

  • Reactive Forms — 大きなフォーム・検証・動的フォームをきれいに扱うモデルベースのフォーム
  • RxJS 深化switchMapdebounceTimecombineLatest などよく使う演算子と検索・自動補完・リトライのような実戦パターン
  • Lifecycle と Change DetectionOnInit を超えるライフサイクル、OnPush 戦略、シグナルと Zone.js の関係
  • HTTP Interceptor パターン — トークンの更新、キャッシュ、共通エラーハンドリング
  • Standalone Routing 深化 — Lazy loading、Route Guards、Resolver、ルートデータ

基礎が「ツールの使い方」だったとすれば、中級は「このツールで実際の製品をどう作るか」の領域です。基礎講座を最後まで一緒についてきてくださった皆さん、本当にお疲れさまでした。中級講座でより深い話で再びお会いしましょう。

X