OpenAPI 定義ファイルから自動生成された Flutter (Dart) の API クライアントの便利な使い方

この記事はカバー株式会社 Advent Calendar 2023 15日目の記事になります。

カバー株式会社エンジニアのSです。よろしくお願いいたします。

前回の記事は @ys-cover による「[カバー株式会社におけるAWS Control Towerの導入](https://qiita.com/ys-cover/items/1822ba6d505795f74e22)」でした。AWS Control Tower の導入を検討されている方にとって、とても興味深い内容となっておりますので、こちらの記事もぜひご覧ください。

# はじめに

先日弊チームメンバーが作成した記事が公開されました。記事に対する反応で多かったのが API クライアントの自動生成の箇所でした。

https://note.cover-corp.com/n/ndf90bb1cc07b?magazine_key=mb173ca3b0d76

API クライアントの自動生成には [openapi-generator](https://github.com/OpenAPITools/openapi-generator) の [dart](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/dart.md) モジュールを利用しています。早期に API クライアント自動生成の仕組みを整えることで、クライアントチームは本質的な機能実装に注力可能になりました。

また API クライアントは Dart 標準の [http](https://github.com/dart-lang/http/tree/master/pkgs/http) ライブラリを用いて作成されるため、メンテナンスコストを低く抑えられ、拡張性も高く様々な要件に柔軟に対応可能でした。

本記事では [openapi-generator](https://github.com/OpenAPITools/openapi-generator) の導入方法の詳細は省きつつ、生成された API クライアントの拡張方法や便利な使い方について具体例を交えて紹介します。

# 便利な使い方

[openapi-generator](https://github.com/OpenAPITools/openapi-generator) 経由で自動生成された API クライアントは下記で初期化することで利用可能です。

```dart
import 'package:openapi/api.dart' as openapi;

final baseUrl = "https://api.example.com";
final apiClient = openapi.ApiClient(
  basePath: baseUrl,
);
```

言及がない限り、後続のコードブロック内に登場する `apiClient` は上記のものを指します。

## 共通リクエストヘッダを定義したい

下記の `addDefaultHeader` 関数で共通リクエストヘッダの定義が可能です。

```dart
// 共通リクエストヘッダの定義
apiClient
        ..addDefaultHeader('App-Version', '1.0.0')
        ..addDefaultHeader('Build-Number', '45')
        ..addDefaultHeader('Accept-Language', 'en-US,en;q=0.5');
```

## 動的に Authorization リクエストヘッダを定義したい

API クライアント初期化時、下記の `authentication` 引数の指定で可能です。

```dart
import 'package:openapi/api.dart' as openapi;

final baseUrl = "https://api.example.com";
final apiClient = openapi.ApiClient(
  basePath: baseUrl,
  authentication: ApiClientAuth(
    () async => authUser?.getIdToken(),
  ),
);
```

`authentication` 引数に指定する `ApiClientAuth` の内容は下記です。指定する値は、自動生成された抽象クラス `Authentication` を継承している必要があります。

```dart:openapi/client/lib/auth/authentication.dart
abstract class Authentication {
  Future<void> applyToParams(
    List<QueryParam> queryParams,
    Map<String, String> headerParams,
  );
}
```

```dart:api_client_auth.dart
import 'package:openapi/api.dart';

class ApiClientAuth implements Authentication {
  ApiClientAuth(this.tokenCaller);

  final Future<String?> Function() tokenCaller;

  @override
  Future<void> applyToParams(
    List<QueryParam> queryParams,
    Map<String, String> headerParams,
  ) async {
    final accessToken = await tokenCaller();

    if (accessToken != null) {
      headerParams['Authorization'] = 'Bearer $accessToken';
    }
  }
}

```


## 利用する API の Client を差し替えたい

例えば、リトライポリシーや通信時に割り込み処理を追加したい場合、標準 http ライブラリだけでは対応できません。対応の一つとして http ライブラリの Client を差し替える方法があります。

自動生成された API クライアント内部では、標準 http ライブラリの Client を利用しています。そのため、[BaseClient](https://pub.dev/documentation/http/latest/http/BaseClient-class.html) を継承したクラスであれば差し替え可能です。

[http_interceptor](https://github.com/CodingAleCR/http_interceptor) のような標準 http ライブラリとの連携が意識されているライブラリであれば Client を差し替え可能です。`flutter pub add http_interceptor` 実行後に下記で差し替え時の挙動確認が可能です。

```dart
import 'package:openapi/api.dart' as openapi;

final apiClient = openapi.ApiClient(
  basePath: baseUrl,
)..client = InterceptedClient.build(
  interceptors: [LoggingInterceptor()],
  retryPolicy: ApiClientRetryPolicy(),
);
```

`interceptors` 引数に指定する `ApiClientInterceptor` の内容は下記です。指定する値は [`InterceptorContract`](https://pub.dev/documentation/http_interceptor/latest/http_interceptor_contract/InterceptorContract-class.html) を継承している必要があります。

```dart:logging_interceptor.dart
import 'package:http_interceptor/http/interceptor_contract.dart';
import 'package:http_interceptor/models/request_data.dart';
import 'package:http_interceptor/models/response_data.dart';

class LoggingInterceptor implements InterceptorContract {
  @override
  Future<RequestData> interceptRequest({required RequestData data}) async {
    print(data.toString());
    return data;
  }

  @override
  Future<ResponseData> interceptResponse({required ResponseData data}) async {
    print(data.toString());
    return data;
  }
}

```

`retryPolicy` 引数に指定する `ApiClientRetryPolicy` の内容は下記です。指定する値は [`RetryPolicy`](https://pub.dev/documentation/http_interceptor/latest/models_retry_policy/RetryPolicy-class.html) を継承している必要があります。

```dart:logging_interceptor.dart
import 'package:http_interceptor/models/retry_policy.dart';

class ApiClientRetryPolicy extends RetryPolicy {
  @override
  int get maxRetryAttempts => 3;

  @override
  Future<bool> shouldAttemptRetryOnResponse(ResponseData response) async {
    if (500 <= response.statusCode) {
      return true;
    }

    return false;
  }
}

```

# おわりに

弊チームでは本記事内で紹介した内容で API クライアントを利用しております。OpenAPI 定義ファイルから Flutter で利用可能な API クライアントの自動生成を検討されている方の参考になれば幸いです。

また、本記事では紹介しませんでしたが [openapi-generator](https://github.com/OpenAPITools/openapi-generator) の [dart](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/dart.md) モジュールの導入を検討したくなった方のために、参考記事を後述の参考リンクに載せておきます。

次回は @mk-cover による「[「笑い声」を分析・検証してみる](https://qiita.com/mk-cover/items/e67c4f13b23edce603ea)」です。こちらも是非ご覧ください。

# 参考リンク

- [OpenAPI x Flutterでプロダクト開発をより早く回そう](https://zenn.dev/team_soda/articles/20d984a8c5d07e#%E5%B0%8E%E5%85%A5)