はじめに
こんにちは! GameWith の kuromoka です!
今回のブログでは、GameWith のダッシュボードの開発に、aspida という API のリクエスト/レスポンスに型を付与するライブラリを導入した話を紹介したいと思います。
ダッシュボードのフロントエンド環境については、Vue.js + TypeScript を導入しています。以下の記事も合わせてご参考ください。
課題
前述の通り、GameWith のダッシュボードは Vue.js + TypeScript を導入しています。これまで API にリクエストするときは axios を使って以下のように書いていました。
async getPostData(): Promise<any> { const response = await axios.get('/api/post'); return response.data.post; } async postPostData(title: string, text: string): Promise<any> { const response = await axios.post('/api/post', { title: title, text: text }); return response.data.post; },
この書き方には、以下のような課題があります。
- URL の文字列部分を typo しても TS のコンパイルは通ってしまうため、実行するまで間違いに気づくことができない
- 関数名と API のリクエスト先が紐付いていないため、関数名からどこにリクエストしているか分かりづらく、また API の種類が増えていくと命名に悩む
- レスポンスの型を any にしているため、型安全なコードでなくなっている
これらの解決方法として、今年の3月に行われた Roppongi.ts #1 のセッションの1つに aspida を利用している話があり、そこで初めて aspida を知りました。
ちょうど今回の課題を解決するのに、aspida が利用できそうだったので導入することにしました。
aspida とは
aspida を使うと、以下のようなことができるようになります。
- API のリクエスト/レスポンスに型を付与できる
- API のリクエストを文字列ではなく、プロパティ経由で行えるようになる
aspida を作成した方の記事もあるので、こちらもご覧ください。
使い方
プロジェクトのルートに aspida の設定ファイル aspida.config.js
を作成して、以下のように設定します。今回は dashboard/apis
以下を aspida の定義ファイルを置くディレクトリに指定しています。
module.exports = { input: 'dashboard/apis' };
aspida ではディレクトリ構造と定義ファイルの名前が、そのままAPIのリクエスト先になるイメージになります。
たとえば dashboard/apis/api/post/index.ts
に以下のような定義ファイルを書いた場合、 api/post
で GET リクエストを送信するための定義になります、
dashboard/apis/api/post/index.ts
export interface Methods { get: { query?: { page: number; }; resBody: { posts: { id: number; title: string; text: string; }[]; }; }; }
aspida では API 定義をした後に、以下のコマンドでビルドする必要があります。
$ aspida --build
ビルドすると$api.ts
というファイルが生成され、今回の場合 dashboard/apis/$api.ts
にファイルが生成されます。
あとは以下のようなコードで axios で aspida を使って、API リクエストを送ることができます。
import axios from 'axios'; import aspida from '@aspida/axios'; import api from './apis/$api'; const client = api(aspida(axios)); (async() => { const response = await client.api.post.$get({ query: { page: 1 } }); response.posts.forEach(post => { console.log(post.id); console.log(post.title); console.log(post.text); }); })();
VSCode で確認すると、リクエスト/レスポンスの部分に、先ほど定義した型が付与されていることが確認できます。
そのためプロパティ名が間違っていたりすると、エラーになるため間違いに気づくことができます。
また URL にリクエストパラメーターが含まれる API の場合は、ファイル名に_
から始まる変数名と@
の後に型を指定します。型は number
と string
の2つが指定できるようです。
以下は ID を指定する API で、api/post/1
のような URL に DELETE でリクエストをするAPIになります。
dashboard/apis/api/post/_id@number.ts
export interface Methods { delete: { resBody: { status: 'failed' | 'success'; }; }; }
これを使う場合は以下のようになります(リクエスト部分のみ)。
(async() => { const response = await client.api.post._id(1).$delete(); if (response.status === 'success') { console.log('success!'); } })();
IDは number
のため、たとえば _id()
の部分で string
を送ろうとすると、エラーになります。
テストコードの書き方
テストコードは Jest で書いています。aspida の部分のテストは、以下のように aspida のモックを作ってテストコードを書いています。
モックのデータ生成時に型補完を効かせるために、aspida から型情報を取得して利用しています(type PostType
の部分)。
type PostType = ReturnType<typeof client.api.post.$get> extends Promise<infer T> ? T : never; const mockPost = jest.spyOn(client.api.post, '$get'); const mockValue: PostType = { posts: [ { id: 1, title: 'title 1', text: 'text 1', }, { id: 2, title: 'title 2', text: 'text 2', }, ] }; // コンパイルを通すために any にしております・・・・・・ mockPost.mockReturnValue(mockValue as any); // fetchPost 内で client.api.post.$get を実行している const data = await fetchPost(1); // ID = 1のデータをテスト expect(data[0]).toEqual(mockValue.posts[0]);
今は aspida-mock というライブラリがあるらしく、これを利用することで今より楽にテストコードを書けそうなので、利用してみたいと思っています。
メリット/デメリット
apisda を使うことで、以下のようなメリットがあると感じました。
- ディレクトリ構造がそのままリクエスト先になるため、分かりやすい
- APIへのリクエストがプロパティ経由で書けるためコード補完が効き、またリクエスト/レスポンスともに型情報が補完されるので書き心地が良い
- aspida は日本人の方が作成しているので、日本語のドキュメントがある
デメリットは GitHub のドキュメントは充実していますが、まだまだ利用例が少ないため、例えばテストコードを書くときなどに少し苦労しました。
終わりに
API 側とフロント側での型定義は連携が取れていないので、現在はそれぞれに型定義ファイルが存在しています。今後 Open API を使うなどして、二重管理になっている型定義を共通化していきたいと考えています。
またデメリットの部分でも書きましたが、aspida はまだ利用例が少ないと思っていますが、便利なのでもっと普及していってほしいと思ってます。
他のサービスでは今回のような場合にどのように管理しているか気になっているので、よければ Twitter でリプライなどいただけるとうれしいです!
GameWithのDeveloper向けTwitterアカウントも開設しました。
ブログの更新情報などを発信するので良かったらフォロー宜しくお願いします!