## はじめに 外部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を追加する