Web APIに型を付与する"Aspida"が結構便利な話

# HTTPクライアントに型がなくてつらい

VueやReactもかなりTypescriptへのサポートが厚くなって来ているが、どうしてもバックエンドとクライアントの間だけは型の恩恵を得ることが難しい。
実際、僕が少し前にVue.jsでWebサービスを作ろうとした際にも、以下のようなaxiosをラップした関数を大量に含むファイルが含まれていた。

```ts
// プレイヤーを追加する
// 返値: PlayerID
export async function addPlayer(
  roomID: RoomID,
  playerName: string
): Promise<PlayerID> {
  const result = await axios.post(`${urlPrefix}/room/${roomID}/player`, {
    name: playerName
  });
  return result.data.id;
}
```

しかし、この例で言えば`result.data.id`は`any`であるから、ここを書き間違えることはよくある。
また、サーバサイドに渡す引数名を変えた際などにも、型として繋がっていないためにエディタの力を借りて一気に修正することは難しい。

# Web APIを付与するAspidaを使ってみる

axiosやfetchのようなHTTPクライアントに型を付与する`aspida`がかなりいい感じだったので紹介する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41402/684a8808-c777-a92e-d9ee-c7a3a71df04d.png)
https://github.com/aspidajs/aspida

npmを用いてaspidaをインストールする。aspidaではRESTクライアントとして`axios`、`ky`、`fetch`が使えるがここではfetchを使ってみる。

```shell
npm install aspida @aspida/fetch --save
```

## APIの型定義を記述する

ここでは例えば以下のWeb APIをサーバ側が提供していたとして、それに対応する型定義を記述してみる。

### **GET /user/:userId**

指定したユーザの情報を返す。

**Request**

* header
    * なし
* body
    * なし

**Response**

* header
    * なし
* body
    * UserData型を示すJSON

```typescript
interface UserData{
   userId:string;
   name:string;
}
```

### **POST /user**

**Request**

* header
    * なし
* body
    * UserCreationData型を示すJSON

```typescript
interface UserCreationData{
   name:string;
}
```
**Response**

* header
    * なし
* body
    * UserCreationResult型を示すJSON

```typescript
interface UserCreationResult{
   userId:string;
}
```


### **PUT /user/:userId**

**Request**

* header
    * Authorization:string
* body
    * UserCreationData型を示すJSON

```typescript
interface UserCreationData{
   name:string;
}
```

**Response**

* header
    * なし
* body
    * なし

### **DELETE /user/:userId**

**Request**

* header
    * Authorization:string
* body
    * なし

**Response**

* header
    * なし
* body
    * なし

### Aspida用型定義の記述

以上のAPIに対する型を生成するためにaspida用のapi定義フォルダとして`apis`フォルダを作成し、その中に`user`フォルダを作り以下の2つのファイルを配置する。

```typescript:apis/user/index.ts
export interface UserCreationData {
  name: string;
}

export interface UserCreationResult {
  userId: string;
}

export interface Methods {
  // Methodsという名前のinterfaceをexportしている必要がある
  post: {
    // 他にもget,delete,putなどをこの形式で記述する
    reqBody: UserCreationData; // reqBodyはリクエストの型を指定する。

    resBody: UserCreationResult; // resBodyはレスポンスの型を指定する。
  };
}
```

```typescript:apis/user/_userId@string.ts
// パス自身が引数を指している場合には`_<引数名>@{string|number}.tsのように記述する。
// GET /group/:groupId/user/:userId のように中間のパスが引数となる場合には
// /apis/group/_groupId@string/user/_userId@string.tsのようなファイルを作成すれば良い　
import { UserCreationData } from "./index";

export interface UserData {
  userId: string;
  name: string;
}

export interface AuthHeaders{
   Authorization: string;
}

export interface Methods {
  get: {
    resBody: UserData;
  };
  put: {
    reqBody: UserCreationData;
    reqHeaders: AuthHeaders;
  };
  delete: {
    reqHeaders: AuthHeaders;
  };
}
```

### Aspidaによって型定義の生成

もともとのAPIから直感的にaspida用のインターフェースに書き下すことが出来る。このaspida用のインターフェースをaspidaは解釈をして、実際にRESTクライアントを用いる際の型ファイルを生成する。

```shell
npx aspida --build
```

とすると、以下のようなaspidaが吐き出す型定義である`$api.ts`が生成される。

```typescript

/* eslint-disable */
import { AspidaClient } from 'aspida'
import { Methods as Methods0 } from './index'
import { Methods as Methods1 } from './_userId@string'

const api = <T>(client: AspidaClient<T>) => {
  const prefix = (client.baseURL === undefined ? '' : client.baseURL).replace(/\/$/, '')

  return {
    _userId: (val0: string) => ({
      get: (option?: { config?: T }) =>
        client.fetch<Methods1['get']['resBody']>(prefix, `/${val0}`, 'GET', option).json(),
      $get: async (option?: { config?: T }) =>
        (await client.fetch<Methods1['get']['resBody']>(prefix, `/${val0}`, 'GET', option).json()).data,
      put: (option: { data: Methods1['put']['reqBody'], headers: Methods1['put']['reqHeaders'], config?: T }) =>
        client.fetch<void>(prefix, `/${val0}`, 'PUT', option).send(),
      $put: async (option: { data: Methods1['put']['reqBody'], headers: Methods1['put']['reqHeaders'], config?: T }) =>
        (await client.fetch<void>(prefix, `/${val0}`, 'PUT', option).send()).data,
      delete: (option: { headers: Methods1['delete']['reqHeaders'], config?: T }) =>
        client.fetch<void>(prefix, `/${val0}`, 'DELETE', option).send(),
      $delete: async (option: { headers: Methods1['delete']['reqHeaders'], config?: T }) =>
        (await client.fetch<void>(prefix, `/${val0}`, 'DELETE', option).send()).data
    }),
    post: (option: { data: Methods0['post']['reqBody'], config?: T }) =>
      client.fetch<Methods0['post']['resBody']>(prefix, '', 'POST', option).json(),
    $post: async (option: { data: Methods0['post']['reqBody'], config?: T }) =>
      (await client.fetch<Methods0['post']['resBody']>(prefix, '', 'POST', option).json()).data
  }
}

export type ApiInstance = ReturnType<typeof api>
export default api
```

## 生成された型定義の利用

さらに、実際にリクエストを行うために以下のようにaspidaによってfetchを初期化する。

```typescript:client.ts
import api from "../apis/$api";
import aspida from "@aspida/fetch";

const f = api(aspida(fetch, { baseURL: "http://hogeAPI.com" }));
```

こうして初期化されたRESTクライアントを用いることで型の恩恵を受けた状態でプログラムを書くことができる。
`$get`,`$post`,`$put`,`$delete`をこのRESTクライアントの参照から呼び出すことによってリクエストを飛ばす。

VSCodeでAPIを叩くときは補完が以下のように働く。

**パス上に引数がない例**

![](https://i.gyazo.com/e2b1117b1720cb2d2bc2f4bf8aacfdfb.gif)

**パス上に引数がある場合の例**

![](https://i.gyazo.com/463b8908f1b4195a78aacf8529f8372b.gif)

**ヘッダーが必要なAPIの例**

![](https://i.gyazo.com/57c17370979314ec455cdb9809687757.gif)

## まとめ

自分はTypescriptではAPIを扱う際に自前でラップする関数群を作ることが標準的だった。
aspidaを使うことで、やり取りするデータに対してのみ注力すれば、実際のそれらのデータ毎に対応したREST APIの呼び出しを意識せず型の恩恵を受けることが出来る。
また、このaspida用に記述する形式の中でやり取りされる型をインターフェースなどにしてexportすることによって、Node.js用にサーバが記述されているのであればこれらの型をサーバ上でも用いることが出来る。