HTTPリクエストを型安全にする手法とNuxt TSでの実装例

## エンドポイントに文字列を使い続ける限りHTTPリクエストは型安全に出来ない

axios, ky, fetch, request, superagent...
どれを使っても「エンドポイントの指定が文字列」なせいで型安全にならない・・・
という課題にTypeScriptユーザーなら誰もが共感してくれるのではないかなーと思ってます

例えば、axiosで以下のようなリクエストをするとして、

```typescript
const userId = 1
const res = await axios.get<History>(`/user/${userId}/histories`)
console.log(res.data)
```
VSCodeで`res.data`の返り値の型を見ると
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63528/2d9fb2aa-0ffa-d52b-2b48-6098927f026c.png)
`History`なんだなあということがわかります
型が表示されてるので一見問題なさそうですが、

＿人人人人人人人人人＿
＞　**突然の仕様変更**　＜
￣Y^Y^Y^Y^Y^Y^Y^￣

- エンドポイントの`user`が`users`に
- 返り値がオブジェクトから配列に

なんてことはまあよくあるわけです
API叩いてるファイルで`/user/`とかで文字列検索して一つずつ書き換えていくことになります

```typescript
const userId = 1
const res = await axios.get<History[]>(`/users/${userId}/histories`)
console.log(res.data)
```
返り値も配列に変わりました
(これがやりたくて無理やりhistoriesを登場させました・・・)
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63528/32e7ebd8-668b-41c9-57da-d07ddd8b870a.png)
しかし

```typescript
const response = await axios.get<History>(`user/${userId}/histories`)
```
のように`/user/`にマッチしない書き方をしている行があって修正漏れ・・・
なんてことにならないように血眼になってファイルを確認する羽目になるわけです

これはTypeScriptだけでなく、静的型付け言語全てにおいて未解決の問題ではないでしょうか
（TypeScript以外の型付け言語を触ったことがないので想像です・・・）
エンドポイントを**文字列**で指定し続ける限り、HTTPリクエストは型安全にならないのです

## エンドポイントをプロパティで指定出来れば型安全になる
`'/users/1/histories'`という文字列から型を判別させることは出来ないけど、
もし`api().users._userId(1).histories`のようにプロパティを繋いでリクエストできれば型安全に出来そうじゃないですか？
イメージはまさにこんな感じ
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63528/282831ff-fdc0-cb0f-ab96-9a6a0c1604b1.gif" width="720" alt="aspida.gif" title="aspida.gif" />

VSCodeの補完が効いてタイポすることも無くなります
仕様変更があっても静的に型エラーを検出できるので安心ですね！
残る問題は型定義をどうやってラクに書くか・・・

## エンドポイントの定義はNuxtのオートルーティングっぽく書けるとラク

個人的に[Nuxtのオートルーティング機能](https://ja.nuxtjs.org/guide/routing/)が気に入っています
これを応用して`api().users._userId(1).histories`の型定義を
`apis/users/_userId/histories.ts`ファイルに書けたら直感的で楽しそう！

```apis/users/_userId/histories.ts
type History = {
  id: number
  date: string
  action: 'walking' | 'sleeping'
}

export type Methods = {
  get: {
    resBody: History[] // GETの返り値はHistoryの配列
  }
}
```
こういうファイルを作ったらエンドポイント型定義がいい感じに自動で作成されて

```typescript
const userId = 1
const res = await api().users._userId(userId).histories.get()
console.log(res.data)
```
こう書けるようになり、
さらにVSCodeで返り値の型を確認すると
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63528/32e7ebd8-668b-41c9-57da-d07ddd8b870a.png)
こうなってくれれば最高ですね！

つまりは`apis/users/_userId/histories.ts`ファイルからエンドポイント型定義を生成するバッチを作ればいい・・・
けどこれが超めんどい！

## 一番めんどいところをやってくれるCLIをOSSとして公開しました

`apis/users/_userId/histories.ts`ファイルを作ればオートルーティング的にエンドポイント解決してくれていい感じの型定義ファイルを生成するTypeScript製CLI「aspida(アスピーダ)」を作りました
上記のアプローチで型安全なHTTPリクエストが出来るようになります
[GitHub: aspida日本語ドキュメント](https://github.com/aspidajs/aspida/blob/master/packages/aspida/docs/ja/README.md)

### インストール
```bash
$ npm install @aspida/axios axios
```

axiosに依存してます

### オートルーティング対象のディレクトリを作る
```bash
$ mkdir apis
```

Nuxtでいうところの`pages`みたいなイメージです
apisディレクトリの直下からオートルーティング対象になります

### 型定義
上記で紹介した内容そのままです

```apis/users/_userId/histories.ts
type History = {
  id: number
  date: string
  action: 'walking' | 'sleeping'
}

export type Methods = {
  get: {
    resBody: History[] // GETの返り値はHistoryの配列
  }
}
```

### ビルドしてルーティング解決されたTSファイルを生成

```package.json
{
  "scripts": {
    "api:build": "aspida --build"
  }
}
```
```bash
$ npm run api:build

> apis/$api.ts was built successfully.
```

### アプリケーションからHTTPリクエストする

```typescript:index.ts
import aspida from "@aspida/axios"
import api from "./apis/$api"
;(async () => {
  const client = api(aspida())
  const userId = 1

  const res = await api().users._userId(userId).histories.get()
  console.log(res.data)

  // Nuxtのaxios moduleっぽくも書ける
  const histories = await api().users._userId(userId).histories.$get()
  console.log(histories)
})()
```

Vue/Nuxtに限らずaxiosを使える環境ならどこでも動作します
一度型定義するだけでわざわざラッパーを作らずとも安全にHTTPリクエストが出来ます
baseURLやヘッダーにtokenを設定する方法はドキュメントを読んでください
「これはスゴい！」と思ったらGitHubにスターを押してもらえると非常に嬉しいです！
[GitHub: aspida日本語ドキュメント](https://github.com/aspidajs/aspida/blob/master/packages/aspida/docs/ja/README.md)

## Nuxt TypeSciptで実装してみよう

サンプルとしてNuxtの[axios module](https://github.com/nuxt-community/axios-module)とaspidaを組み合わせる方法を紹介します
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63528/6c39bb29-a9d2-295b-6710-76bd8d01ea29.png)

### インストール
[Nuxt TypeScriptのセットアップ](https://typescript.nuxtjs.org/ja/guide/setup.html)は完了している前提です

```bash
npm install @nuxtjs/axios @aspida/axios
npm install npm-run-all --save-dev
```

### apiの型定義を行うディレクトリを作る
```bash
$ mkdir apis
```

### apiの型定義ファイルを作る

```typescript:apis/users/_userId/histories.ts
type History = {
  id: number
  date: string
  action: 'walking' | 'sleeping'
}

export type Methods = {
  get: {
    resBody: History[] // GETの返り値はHistoryの配列
  }
}
```

### プラグインに$apiを登録する
aspidaにaxios moduleを引数として渡した結果を`$api`プラグインとしてinjectします
(この時点では`'~/apis/$api'`ファイルが存在しないのでVSCode上でエラーが表示されます)

```plugins/api.ts
import { Plugin } from '@nuxt/types'
import axiosClient from '@aspida/axios'
import api, { ApiInstance } from '~/apis/$api'

declare module 'vue/types/vue' {
  interface Vue {
    $api: ApiInstance
  }
}

const plugin: Plugin = ({ $axios }, inject) => inject('api', api(axiosClient($axios)))

export default plugin
```
`nuxt.congif.ts`への追記も忘れずに

```nuxt.config.ts
import { Configuration } from '@nuxt/types'

const config: Configuration = {
  plugins: [
    '~/plugins/api',
  ],
  modules: [
    '@nuxtjs/axios',
  ],
  axios: {
    baseURL: 'http://example.com/api/v1' // aspidaにもbaseURLが反映されます
  }
}

export default config
```

### グローバルの$nuxtから$apiを呼ぶ

globalに$nuxtの型定義を上書きします

```globals.d.ts
import '@nuxtjs/axios'
import { NuxtApp } from '@nuxt/types/app'

declare global {
  const $nuxt: NuxtApp
}
```

あらゆるファイルで以下のように$apiを呼び出せるようになります

```store/index.ts
const histories = await $nuxt.$api.users._userId(userId).histories.$get()
```

### npm scriptに登録して開発・ビルドする

`--watch`オプションでapisディレクトリの変更を検知して`apis/$api.ts`を自動更新できます
開発時(dev)は`npm-run-all`で並列にaspidaとnuxt-tsを呼び、ビルド時(generate)はaspidaのあとにnuxt-tsを呼ぶようにすると良いです

```package.json
{
  "scripts": {
    "dev": "npm-run-all --parallel dev:*",
    "dev:nuxt": "nuxt-ts",
    "dev:api": "aspida --watch"
    "generate": "aspida --build && nuxt-ts generate"
  }
}
```

以下のコマンドでブラウザに表示されたら成功です

```bash
$ npm run dev
```

## 世界中のWebAPIの型定義をnpmに登録出来たらもっとフロントが楽しくなる
内製のAPIは上記のように一人が型定義をしておけば他のメンバーが安心してHTTPリクエストを行えるようになります
一方でTwitter, GoogleMap, YouTubeなど一般公開されててよく使うAPIは、型定義を誰かが書いてnpmに登録しておいてくれたら世界中のみんながラクにAPIを使えるようになりそうじゃないですか！？
ということで`@types/node`のノリで`@aspida/twitter`でTwitter APIの型定義をインストールできるようになるDefinitelyTypedみたいな仕組みも絶賛開発中です

## aspidaの意味
ギリシャ語で「盾」です
型安全の世界観と合っていてカッコいいなと思って名付けました！
(ちなみにaxiosはギリシャ語で「価値が有る」らしい)

![logo.png](https://aspidajs.github.io/aspida/logos/svg/black.svg)

## GitHubのスターを押してもらえると嬉しいです

aspidaは2か月以上前から実戦投入しており、axiosの利用が可能な案件であればほとんどのユースケースに対応できるレベルに達しています
ユーザーから報告があったバグも数時間以内に修正してリリース対応してきました
日本語英語両方でドキュメント整備も頑張っているのでGitHubのスターを押してもらえると非常に嬉しいです！
[GitHub: aspida日本語ドキュメント](https://github.com/aspidajs/aspida/blob/master/packages/aspida/docs/ja/README.md)

最後まで読んでいただきありがとうございました
明日のアドベントカレンダー担当は@Yoshihiro-Hiroseさんです！

Vue入門者向けのVuex解説記事も公開しています
[Vue.js + Vuexでデータが循環する全体像を図解してみた](https://qiita.com/m_mitsuhide/items/f16d988ec491b7800ace)