## 概要
[[Zod]]を規模の大きいコードベースで使うと、開発時のパフォーマンスに大きな影響を及ぼすことが分かった。具体的にはオートコンプリートの速度、ビルド・型チェックの速度など。
そこで、実際に[[GitHub REST API]]の[[OpenAPI Specification]]ファイルを使って、どれくらいパフォーマンスに影響があるかを確かめてみる。具体的には以下のケースを比較してみる。
| ケース | SVL | スキーマ | クライアント |
| ------------------------------------ | --------- | -------- | ------------ |
| [[#OpenAPI fetch (ケース1)]] | | 自動生成 | 自動生成 |
| [[#openapi-zod-client (ケース2)]] | [[Zod]] | 自動生成 | 自動生成 |
| [[#OpenAPI Typescript (ケース3)]] | | 自動生成 | 自動生成 |
| [[#orval (ケース4)]] | | 自動生成 | 自動生成 |
| [[#OpenAPI fetch + Typia (ケース5)]] | [[Typia]] | 自動生成 | 自動生成 |
> [!note]
> `SVL` は [[スキーマバリデーションライブラリ]] の略。
## 個人的な結論
あくまで **個人的なもの** であるが、中規模以上のプロダクトで使うなら、1つ目のケースである『[[OpenAPI fetch]]を使用して、[[スキーマバリデーションライブラリ]]は使わない』 がベストだと思った。
[[スキーマバリデーションライブラリ]]がどうしても必要な場合は、スキーマだけを[[OpenAPI Specification]]から自動生成しつつ、クライアントは要件にあわせて自作した方がいいと思う。既存クライアントはよくできているものの、エッジケースでバグが発生したときに対応が求められる。
[[Zodios]]を利用している[[openapi-zod-client]]は、インターフェースもシンプルで、利用方法もカンタン、堅牢なコーディングをしたい場合にも必要な機能が揃っていて一瞬パーフェクトに見えるが、APIの規模がある程度以上の場合、パフォーマンスに深刻な問題が発生する。[[GitHub]] issuesを見る限り、今のところ解消される見込みはなさそう
## 準備
[[GitHub REST API]]の[[OpenAPI Specification]]ファイルを用意する。以下のリポジトリで管理されている。
<div class="link-card">
<div class="link-card-header">
<img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/>
<span class="link-card-site-name">GitHub</span>
</div>
<div class="link-card-body">
<div class="link-card-content">
<p class="link-card-title">GitHub - github/rest-api-description: An OpenAPI description for GitHub's REST API</p>
<p class="link-card-description">An OpenAPI description for GitHub's REST API. Contribute to github/rest-api-descriptio ... </p>
</div>
<img src="https://opengraph.githubassets.com/a1b1b741c1ff53233a2988df6f31cb7810756f4ed4deff1b5ad9c2444f0cbc55/github/rest-api-description" class="link-card-image" />
</div>
<a href="https://github.com/github/rest-api-description/tree/main"></a>
</div>
今回は[[YAML]]ファイルを使う。7.7MB。21万7000行の大きなファイル。
https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml
## OpenAPI fetch (ケース1)
ケース1は[[Zod]]を使わずに、スキーマとクライアントを自動生成するケース。[[OpenAPI fetch]]を使って実現する。
### インストール
```console
npm i openapi-fetch
npm i -D openapi-typescript typescript
```
バージョン。
```console
├── @fsouza/
[email protected]
├── @tsconfig/
[email protected]
├──
[email protected]
├──
[email protected]
├──
[email protected]
└──
[email protected]
```
### スキーマコードの生成
```console
wget https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml
npx openapi-typescript api.github.com.yaml -o api.ts
```
`api.ts`は4.2MB、10万行とかなり大きなものに。
### コードを書く
`index.ts`
```ts
import createClient from "openapi-fetch";
import type { paths } from "./api.ts";
async function main() {
const client = createClient<paths>({ baseUrl: "https://api.github.com" });
const { data, error } = await client.GET("/search/repositories", {
params: { query: { per_page: 3, q: "obsidian" } },
});
if (error) {
console.error(error.message);
return;
}
const repositoryNames = data?.items.map((x) => x.name);
console.log(repositoryNames);
}
main();
```
### 結果
#### 補完速度
サクサクだったため測定はしていない。
#### 型チェック速度
2秒くらい。0.3秒くらいは`npx tsc`コマンド自体のオーバーヘッドだが、本稿では差分で比較するため気にしない。
```console
❯ time npx tsc --noEmit
npx tsc --noEmit 1.94s user 0.19s system 212% cpu 1.001 total
```
## openapi-zod-client (ケース2)
ケース2は[[Zod]]を利用し、スキーマとクライアントを自動生成するケース。[[openapi-zod-client]]を利用する。
### インストール
```console
npm i -D openapi-zod-client
```
バージョン。
```console
├── @fsouza/
[email protected]
├── @tsconfig/
[email protected]
├──
[email protected]
├──
[email protected]
└──
[email protected]
```
### クライアントコードの生成
```console
wget https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml
npx openapi-zod-client api.github.com.yaml -o client.ts
```
`client.ts`は1.4MBで45620行。[[#ケース1]]よりもサイズは小さく1/4程度だが、[[Zodios]]に入力するためのスキーマが定義されているだけだからと思われる。
### コードを書く
`index.ts`
```ts
import { createApiClient } from "./client";
async function main() {
const client = createApiClient("https://api.github.com");
}
main();
```
諸事情でここまでしか書けなかった。詳細は[[#ケース2#結果#補完速度]]を参照。
### 結果
#### 補完速度
遅すぎて測定不能。
この時点で `get` の候補が表示されず[[LSP]]がタイムアウトしてしまった。。
![[Pasted image 20240511232830.png]]
#### 型チェック速度
20秒弱。実質まだ何もしていないコードにしては非常に遅い。また、大量に型エラーが出てしまった。
```console
❯ time npx tsc --noEmit
npx tsc --noEmit 17.57s user 1.15s system 140% cpu 13.354 total
```
## OpenAPI Typescript (ケース3)
ケース3は[[OpenAPI Typescript]]を使用する。
### インストール
```console
npm i -D @hey-api/openapi-ts
```
バージョン。
```console
├── @fsouza/
[email protected]
├── @hey-api/
[email protected]
├── @tsconfig/
[email protected]
├──
[email protected]
└──
[email protected]
```
### クライアントコードの生成
```console
wget https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml
npx openapi-ts -i api.github.com.yaml -o client
```
生成物はファイルではなくディレクトリになる。ファイルサイズは総計で10MBちょい。
### コードを書く
`index.ts`
```ts
import { SearchService } from "./client/services.gen";
async function main() {
const res = await SearchService.searchRepos({ perPage: 3, q: "Obsidian" });
const repositoryNames = res.items.map((x) => x.name);
console.log(repositoryNames);
}
main();
```
### 結果
#### 補完速度
サクサクだったため測定はしていない。
#### 型チェック速度
4秒くらい。ただ、100近くのエラーが出たので動かなかった。
```console
❯ time npx tsc --noEmit
npx tsc --noEmit 3.75s user 0.26s system 191% cpu 2.098 total
```
## orval (ケース4)
ケース4は[[#ケース2]]と同様のことを[[orval]]で行う。
> [!caution]
> [[orval]]には[clientに`zod`を指定する機能がある](https://orval.dev/guides/zod) はず... なのだが、実際は以下のエラーになってしまう。。
>
> ```error
> 🛑 petstore - TypeError: Cannot read properties of undefined (reading 'map')
> at parseProperty (/home/tadashi-aikawa/tmp/orval-sandbox/node_modules/@orval/zod/dist/index.js:265:37)
> at Array.map
> ```
### インストール
```console
npm i -D orval
npm i axios
```
バージョン。
```console
├── @fsouza/
[email protected]
├── @tsconfig/
[email protected]
├──
[email protected]
├──
[email protected]
└──
[email protected]
```
### クライアントコードの生成
```console
wget https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml
npx orval --input api.github.com.yaml --output client.ts --client zod
```
### コードを書く
`index.ts`
```ts
import axios from "axios";
import { searchRepos } from "./client";
axios.defaults.baseURL = "https://api.github.com";
async function main() {
const { data } = await searchRepos({ per_page: 3, q: "Obsidian" });
const repositoryNames = data.items.map((x) => x.name);
console.log(repositoryNames);
}
main();
```
### 結果
#### 補完速度
サクサクだったため測定はしていない。
#### 型チェック速度
速度は5秒弱。
```console
❯ time npx tsc --noEmit
npx tsc --noEmit 3.50s user 0.31s system 191% cpu 1.986 total
```
[[TS2411]]のエラーが発生するが、無視すれば一応動く。
## OpenAPI fetch + Typia (ケース5)
[[#OpenAPI fetch (ケース1)]]に対して、[[Typia]]で[[スキーマバリデーション]]の機能を追加したもの。
### インストール
```console
npm i openapi-fetch typia
npm i -D openapi-typescript typescript
```
バージョン。
```console
├── @fsouza/
[email protected]
├── @tsconfig/
[email protected]
├──
[email protected]
├──
[email protected]
├──
[email protected]
├──
[email protected]
├──
[email protected]
└──
[email protected]
```
### セットアップ
> [!caution]
>
> `tsconfig.json` に `compilerOptions` プロパティが存在しない場合は、空で設定しておく。そうしないとsetupでエラーになる。
```console
npx typia setup
```
### スキーマコードの生成
```console
wget https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml
npx openapi-typescript api.github.com.yaml -o api.ts
```
### コードを書く
[[#OpenAPI fetch (ケース1)]]のコードをベースに、[[Typia]]のバリデーションを追加する。
`index.ts`
```ts
import createClient from "openapi-fetch";
import typia from "typia";
import type { paths } from "./api.ts";
async function searchRepositories(
client: ReturnType<typeof createClient<paths>>,
query: { per_page: number; q: string },
) {
return await client.GET("/search/repositories", {
params: { query },
});
}
async function main() {
const client = createClient<paths>({ baseUrl: "https://api.github.com" });
const { data, error } = await searchRepositories(client, {
per_page: 3,
q: "obsidian",
});
if (error) {
console.error(error.message);
return;
}
type R = Awaited<ReturnType<typeof searchRepositories>>;
const r = typia.validate<R>(data);
if (!r.success) {
console.error(r.errors);
return;
}
const repositoryNames = r.data.data?.items.map((x) => x.name);
console.log(repositoryNames);
}
main();
```
この時点でかなり嫌な予感はしている...。
```console
$ npx tsc && node .
[
{
path: '$input',
expected: '(__type.o8 | __type)',
value: { total_count: 21120, incomplete_results: false, items: [Array] }
}
]
```
動いているっぽいけど、実質意味をなしてなさそうなvalidate結果になってしまった。使い方が悪いのか、用途がオーバーキルなのかは不明。いずれにせよ、**気軽に使えそうではないので、ハマりたくないのであれば今は使わない方がいい** 気がした。
なお、補完速度などは期待通り問題なかった。validationの実行速度も、`index.js`のコードを見る限り、必要不可欠な処理が実行されているだけのように見える。