72
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-02

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

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

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

const userId = 1
const res = await axios.get<History>(`/user/${userId}/histories`)
console.log(res.data)

VSCodeでres.dataの返り値の型を見ると
無題.png
Historyなんだなあということがわかります
型が表示されてるので一見問題なさそうですが、

_人人人人人人人人人_
> 突然の仕様変更 <
 ̄Y^Y^Y^Y^Y^Y^Y^ ̄

  • エンドポイントのuserusers
  • 返り値がオブジェクトから配列に

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

const userId = 1
const res = await axios.get<History[]>(`/users/${userId}/histories`)
console.log(res.data)

返り値も配列に変わりました
(これがやりたくて無理やりhistoriesを登場させました・・・)
無題.png
しかし

const response = await axios.get<History>(`user/${userId}/histories`)

のように/user/にマッチしない書き方をしている行があって修正漏れ・・・
なんてことにならないように血眼になってファイルを確認する羽目になるわけです

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

エンドポイントをプロパティで指定出来れば型安全になる

'/users/1/histories'という文字列から型を判別させることは出来ないけど、
もしapi().users._userId(1).historiesのようにプロパティを繋いでリクエストできれば型安全に出来そうじゃないですか?
イメージはまさにこんな感じ
aspida.gif

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

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

個人的にNuxtのオートルーティング機能が気に入っています
これを応用して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の配列
  }
}

こういうファイルを作ったらエンドポイント型定義がいい感じに自動で作成されて

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

こう書けるようになり、
さらにVSCodeで返り値の型を確認すると
無題.png
こうなってくれれば最高ですね!

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

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

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

インストール

$ npm install @aspida/axios axios

axiosに依存してます

オートルーティング対象のディレクトリを作る

$ 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"
  }
}
$ npm run api:build

> apis/$api.ts was built successfully.

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

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日本語ドキュメント

Nuxt TypeSciptで実装してみよう

サンプルとしてNuxtのaxios moduleとaspidaを組み合わせる方法を紹介します
無題.png

インストール

Nuxt TypeScriptのセットアップは完了している前提です

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

apiの型定義を行うディレクトリを作る

$ mkdir apis

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

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"
  }
}

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

$ npm run dev

世界中のWebAPIの型定義をnpmに登録出来たらもっとフロントが楽しくなる

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

aspidaの意味

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

logo.png

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

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

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

Vue入門者向けのVuex解説記事も公開しています
Vue.js + Vuexでデータが循環する全体像を図解してみた

72
40
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
72
40

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?