## はじめに
外部APIを使う際に作成するクライアントについて、毎回同じようなものを作っているので知見集約する。
### 前提
[[axios]]と[[🦉Owlelia]]を使う。
```console
npm i axios owlelia
```
> [!note]
> この2つのライブラリを使わなくても実現は可能(コードは変わる)。
### 作成するAPI
- ユーザー取得API
- `/users/{:userId}`
## パッケージ構成
- external
- yourapi
- api
- [[#users ts]]
- [[#YourApiClient.ts]]
- [[#YourApiClientImpl.ts]]
- utils
- [[#string ts]]
## 実装
### `users.ts`
`/users/{:usersId}`のAPIに関する情報。**APIのエンドポイントが増えるたびに追加する。**
```ts
export interface User {
userId: string;
userName: string;
}
export namespace user {
export interface get {
pathParams: {
userId: string;
};
query: {};
response: User;
}
}
```
| プロパティ | 意味 |
| ---------- | ------------------------ |
| pathParams | [[パスパラメータ]]の型 |
| query | [[クエリパラメータ]]の型 |
| response | 成功レスポンスの型 |
もしAPIが`/users`の場合、`namespace user` の外側に定義する。
```ts
export interface User {
userId: string;
userName: string;
}
export interface get {
pathParams: {};
query: {};
response: User[];
}
```
なお、`namespace get`の`get`は[[HTTPリクエストメソッド]]のこと。
### `YourApiClient.ts`
APIクライアントのインターフェース。**APIのエンドポイントが増えたら`PathMap`を追加する。**
```ts
import { AsyncResult, BaseError } from "owlelia";
import * as users from "~/external/yourapi/api/users";
export interface PathMap {
"/users/${userId}": users.user.get;
}
export interface YourApiClient {
request<K extends keyof PathMap>(
_path: K,
pathParams: PathMap[K]["pathParams"],
query: PathMap[K]["query"]
): AsyncResult<PathMap[K]["response"], BaseError>;
}
```
[[パスパラメータ]]は[[テンプレートリテラル (TypeScript)|テンプレートリテラル]]で表現する。ただし、`pathParams`と同じな名前を使う こと。(間違えてもエラーにはならない)
### `YourApiClientImpl.ts`
[[#YourApiClient.ts]]の実装部分。**APIのエンドポイントが増えても追加==不要==**。ただし、機能面ではまだ成熟していないため、**やりたいことが増えれば改修が必要になる**。
```ts
import { AsyncResult, BaseError, fromPromise } from "owlelia";
import axios from "axios";
import { PathMap, YourApiClient } from "~/external/yourapi/YourApiClient";
import { fmt } from "~/utils/string";
export class YourApiClientImpl implements YourApiClient {
constructor(public baseUrl: string) {}
request<K extends keyof PathMap>(
_path: K,
pathParams: PathMap[K]["pathParams"],
query: PathMap[K]["query"]
): AsyncResult<PathMap[K]["response"], BaseError> {
return fromPromise(
axios
.get<PathMap[K]["response"]>(
`${this.baseUrl}${fmt(String(_path), pathParams)}`,
{
params: query,
}
)
.then((x) => x.data)
.catch((e) => {
throw handleError(e.response);
})
);
}
}
function handleError(e: any | undefined): BaseError {
if (!e) {
return new BaseError("接続エラーです");
}
if (e.data) {
return new BaseError(e.data.message);
}
return new BaseError("予期せぬエラーが発生しました");
}
```
> [!todo]
> `handleError`は[[HTTPレスポンスステータスコード]]ごとに`BaseError`を継承したエラークラスを作成するのが理想。
### `string.ts`
**一度作れば追加は==不要==**。
```ts
// https://kuma-emon.com/it/pc/1025/
export function fmt(
template: string,
values?: { [key: string]: string | number | null | undefined }
): string {
return !values
? template
: new Function(...Object.keys(values), `return \`${template}\`;`)(
...Object.values(values).map((value) => value ?? "")
);
}
```
[[パスパラメータ]]から[[URL]]を作成するとき、以下のような関数が欲しかったため追加した。
```ts
fmt("Hello, ${user1}, ${user2}", { user1: "taro", user2: "jiro" })
```
これは以下サイトの実装をそのまま使わせてもらっている。
<div class="link-card">
<div class="link-card-header">
<img src="https://kuma-emon.com/wp-content/uploads/2021/05/cropped-sk-2-32x32.png" class="link-card-site-icon"/>
<span class="link-card-site-name">いつかの熊右衛門</span>
</div>
<div class="link-card-body">
<div class="link-card-content">
<div>
<p class="link-card-title">typescript や javascript で format っぽいことをするには</p>
</div>
<div class="link-card-description">
javascript には、java や C# などにある format の様に書式化するものがありません。 数値や文字列、日付の書式化に対応するのは辛いですが、文字列に変数を展開するものであれば比較...
</div>
</div>
<img src="https://kuma-emon.com/wp-content/uploads/2021/05/js2.jpg" class="link-card-image" />
</div>
<a href="https://kuma-emon.com/it/pc/1025/"></a>
</div>
## 呼び出し側
こんな感じになる。ほぼほぼ[[curl]]と同じインターフェース。
```ts
const client = new YourApiClientImpl("https://base/url");
const user = await client.request("/users/${userId}", { userId: "001" }, {});
```
だが、型安全の恩恵を得ることができる。特に以下の点がポイント。
- 第1引数の候補が表示され補完できる
- 第1引数が決まると、第2引数(`pathParams`)と第3引数(`query`)を自動推論する
## API追加時の作業
APIにエンドポイントを追加するときの作業は2つだけ。
1. [[#users.ts]]に[[パスパラメータ]]、[[クエリパラメータ]]、レスポンスの型を定義する
2. [[#YourApiClient.ts]]の`PathMap`に、keyとしてパス、valueとして1で追加したnamespaceを追加する