Angularで複数のAPIを呼び出す方法とエラー処理まとめ

# はじめに

Angularで、1回の画面表示で複数のAPIを呼び出したい時がそれなりにある。
また、エラー処理に関する情報が少ない気がするので、エラー処理も含めて、どのようにするのがよいのか整理してみた。
Angularのバージョンは6.1.0（RxJS 6）。

# 前提

呼び出すAPIは次の形式とする。

```ts:xxx.service.ts
export class XxxService {
  api1(): Observable<Entity1>;
  api2(key: string): Observable<Entity2>;
}
```

`api1` を呼び出した後、 `api2` を呼び出す想定。

また、API呼び出し中の画面は全画面ブロックではなく、部分的にローディング中メッセージ（もしくはアイコン）を表示する想定。

# 2つのAPIが独立な場合

もし `api1` の戻り値を使用せず `api2` を呼び出せるのであれば、単純に別々に呼び出せばよい。

```ts:xxx.component.ts
@Component({ /* 省略 */ })
export class XxxComponent implements OnInit {
  result1: Observable<Entity1 | { error: any }>;
  result2: Observable<Entity2 | { error: any }>;

  constructor(private xxxService: XxxService) { }

  ngOnInit() {
    this.result1 = this.xxxService.api1().pipe(
      catchError(e => of({ error: e }))
    );
    this.result2 = this.xxxService.api2('key2').pipe(
      catchError(e => of({ error: e }))
    );
  }
}
```

上記のように `catchError` でエラー内容を `Observable` に変換しておくと、画面側で「正常」「エラー」「読み込み中」の表示切替が可能になる。

```html:xxx.component.html
<ng-container *ngIf="result1 | async; let result1; else loading">
  <ng-container *ngIf="!result1.error else result1Error">
    {{result1}}
  <ng-container>
  <ng-template #result1Error>
    result1の取得に失敗しました。詳細：{{result1.error}}
  </ng-template>
</ng-container>
<ng-container *ngIf="result2 | async; let result2; else loading">
  <ng-container *ngIf="!result2.error else result2Error">
    {{result2}}
  <ng-container>
  <ng-template #result2Error>
    result2の取得に失敗しました。詳細：{{result2.error}}
  </ng-template>
</ng-container>
<ng-template #loading>
  読み込み中
</ng-template>
```

# 2つのAPIが同期的動作を行う場合

`api1` の呼び出し後、その結果を用いて `api2` を呼び出す必要がある場合、 `pipe` でつなげて処理する。

## 画面表示に2つ目のAPIの結果のみが必要な場合

`flatMap` を用いて、 `api2` の結果を取得する。

```ts:xxx.component.ts
@Component({ /* 省略 */ })
export class XxxComponent implements OnInit {
  result2: Observable<Entity2 | { error: any }>;

  constructor(private xxxService: XxxService) { }

  ngOnInit() {
    this.result2 = this.xxxService.api1().pipe(
      flatMap(result1 => this.xxxService.api2(result1.key)),
      // api1とapi2のエラー処理
      catchError(e => of({ error: e }))
    );
  }
}
```

`api1` で発生したエラーも `api2` で発生したエラーも、1つの `catchError` で処理することになるため、エラーの形式は揃えておく必要がある（通常は規約で揃っているはず）。

もしエラー処理を別々に行いたい場合は、いったん `subscribe` する。

```ts:xxx.component.ts
@Component({ /* 省略 */ })
export class XxxComponent implements OnInit {
  result2: Observable<Entity2 | { error: any }>;

  constructor(private xxxService: XxxService) { }

  ngOnInit() {
    this.xxxService.api1().subscribe(
      result1 => {
        this.result2 = this.xxxService.api2(result1.key).pipe(
          // api2のエラー処理
          catchError(e => of({ error: e }))
        );
      },
      // api1のエラー処理
      e => console.error(e)
    );
  }
}
```

## 2つのAPIの結果を画面側で表示する場合

### 変数の型をObservableにしない場合

先の例で、 `subscribe` で得られる `result1` を変数に入れればよい。
`api1` と `api2` のエラーを個別に処理したい場合も、この方法での実装になる。

```ts:xxx.component.ts
@Component({ /* 省略 */ })
export class XxxComponent implements OnInit {
  result1: Entity1;
  result2: Observable<Entity2 | { error: any }>;

  constructor(private xxxService: XxxService) { }

  ngOnInit() {
    this.xxxService.api1().subscribe(
      result1 => {
        this.result1 = result1;
        this.result2 = this.xxxService.api2(result1.key).pipe(
          // api2のエラー処理
          catchError(e => of({ error: e }))
        );
      },
      // api1のエラー処理
      e => console.error(e)
    );
  }
}
```

### 2つの結果をObservableで受け取る場合

`forkJoin` を用いて結合する。エラー処理が共通化できるのであれば、コードはすっきりする。
もちろん、 `api1` でエラーが発生した場合は、 `api2` は実行されない。

```ts:xxx.component.ts
@Component({ /* 省略 */ })
export class XxxComponent implements OnInit {
  result: Observable<any>;

  constructor(private xxxService: XxxService) { }

  ngOnInit() {
    this.result = this.xxxService.api1().pipe(
      flatMap(result1 => forkJoin(of(result1), this.xxxService.api2(result1.key))),
      // api1とapi2のエラー処理
      catchError(e => of({ error: e }))
    );
  }
}
```

ただ、上記のように実装すると、結果は配列で取得されるため、テンプレート側ではインデックスで参照することになる。

```html:xxx.component.html
<ng-container *ngIf="result | async; let result; else loading">
  <ng-container *ngIf="!result.error else resultError">
    {{result[0]}}<br>
    {{result[1]}}
  <ng-container>
  <ng-template #resultError>
    取得に失敗しました。詳細：{{result.error}}
  </ng-template>
</ng-container>
<ng-template #loading>
  読み込み中
</ng-template>
```

# 終わりに

全画面ローディングを入れずにエラー処理も入れるとなると、一工夫必要。
もっと良いやり方があるかもしれないため、ぜひ情報をいただければ。