Open Api Generator（typescript-fetch）で自動生成したApiClientを使ったリクエストの前後に処理を追加する

# Open Api Generator（typescript-fetch）で自動生成したApiClientを使ったリクエストの前後に処理を追加する

## Open Api Generatorとは


> OpenAPI Generator allows generation of API client libraries (SDK > generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (both 2.0 and 3.0 are supported)

https://github.com/OpenAPITools/openapi-generator#overview より引用

とのこと。つまり、特定の言語に依存しないAPI記述仕様であるOpenAPI Specから各言語で利用するためのクライアントライブラリや、スタブサーバー、ドキュメントや設定を自動生成してくれるツールです。

## 何が嬉しいの？

OpenAPI Spec（以下OAS）はAPI記述の標準仕様です。
そのため、OASに基づいて作成された成果物を用いることで、APIの仕様について他者とコミュニケーションを取る際のコストを軽減することが期待できます。そして、作成された成果物からクライアントライブラリ等を自動生成することで、実装ミスの軽減したり、仕様と実装の乖離を防ぐ効果があります。

## typescript-fetch Generator

typescript-fetch GeneratorはfetchAPIを用いたTypeScriptのクライアントライブラリのGeneratorです。類似のGeneratorとしてtypescript-axiosなどがあります。同じTypeScript用のGeneratorであっても、ものによってconfig可能な値や生成されるコードに違いがあるため、自身のプロジェクトが採用している技術に応じてGeneratorを選択すると良いでしょう。

[Generatorの一覧はこちら](https://openapi-generator.tech/docs/generators)

今回はTypeScript×fetchAPIを使っているプロジェクトへ導入するという前提で、`typescript-fetch Generator`の簡単な使い方について解説していきます。

## コードの生成

実際にツールを使ってコードを生成する方法はいくつかあるので[公式の手順](https://github.com/OpenAPITools/openapi-generator#1---installation)を参照してみてください。
今回はDockerを使った方法で生成してみます。

```sh
docker run --rm -v "${PWD}:/local" \
  openapitools/openapi-generator-cli:v6.0.0 generate \
  -i /local/openapi.yaml \
  -g typescript-fetch \
  -o /local/dist/target
```

上記のコマンドを実行すると、targetディレクトリ以下にファイルが生成されます。
なお、最新の安定版はv6.2.1ですが、生成されたコードが型エラーになってしまったため、v6.0.0を使用しています。

![target.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933553/0f6a88c0-ed57-10e4-78cd-ccae3b6cba52.png)


生成に使用したOpenAPIドキュメントはこちら

```yml
openapi: 3.0.0
info:
  version: 1.0.0
  title: Example
paths:
  /user:
    get:
      operationId: getUser
      tags:
        - User
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/User"
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
```

### 使い方

生成されたAPIクライアントのメソッドを実行すれば型付けされたレスポンスを得ることができます。便利ですね。

![usage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/933553/b3f457c6-975b-9b45-d1fd-7dc462750bcc.png)


## Middlewareの実装

APIリクエストといえば、処理の前後に認証や後処理などの共通処理を挟み込むことがよくあります。
typescript-fetchには、それを叶える`Middleware`という仕組みが用意されています。

詳しくは生成された`runtime.ts`というソースを読んでいただきたいのですが、UserApiの継承元としてBaseApiというクラスが定義されており、コンストラクタや`withMiddleware`メソッドでMiddlewareという型を受け取れます。

型定義はこんな感じ

```typescript
export interface Middleware {
    pre?(context: RequestContext): Promise<FetchParams | void>;
    post?(context: ResponseContext): Promise<Response | void>;
}
```

このMiddleware型を満たすようなインスタンスをUserApiに設定してあげると、`pre`関数でリクエストの前に、`post`関数でリクエストの後に実施したい処理を挟み込むことができます。

```typescript
const middleware: Middleware = {
    pre: async (context: RequestContext) => {
        console.log("pre function")
        return {
            init: context.init,
            url: context.url
        }
    },
    post: async (context: ResponseContext) => {
        console.log("post function")
        return Promise.resolve(context.response)
    }
}
// 初期化時に渡す
const user = await (new UserApi(new Configuration({ middleware: [middleware] })).getUser())
// 後から設定もできる
const user = await (new UserApi().withMiddleware(middleware).getUser())

```

簡単ですね。（このあたりの話がドキュメントに書かれていると嬉しいのですが、私には見つけられませんでした。誰か見つけたら教えて下さい。）

## さらに便利に

こんな感じすれば、各APIで共通処理を使いつつ必要なときにoverrideできて良さそう？（未検証）

```typescript
const initClient = <T extends BaseAPI>(api: T, middleware?: Middleware): ApiClient<T> => {
    const defaultMiddleware = {} as Middleware // デフォルト動作を規定
    return middleware ? api.withMiddleware(middleware) : api.withMiddleware(defaultMiddleware)
}

type ApiClient<T extends BaseAPI> = {
    [P in keyof T]: T[P]
}
```

## 余談

生成されるコードのセミコロンの有無に一貫性がなく、夜も眠れなかったので、PR出したら無事マージされた。祝コントリビュート。

https://github.com/OpenAPITools/openapi-generator/pull/13856