概要
- aspidaでaxiosやfetchをラップすると型のサポートが厚くなる
- aspidaを使ったユースケース
- tips等
背景
近年のWebアプリケーション・フロントエンド開発において、型の付いた言語(主にTypeScript、場合によってはFlow。今回は前者の話)によってJavaScriptを比較的安全に扱えるような開発を試みるケースが増えていることは皆さんもよくご存知の通りだと思います。
型による安全性向上の他にもVSCode等のエディタ補完機能による開発体験の良さや、仕様のコードへの内包化により、保守性と開発速度の両方を上げることに一役買っており、可能な限りフロントエンドはTypeScriptで書いていきたいという気持ちがあります。
また、フロントエンドにおける役割の1つとしてHTTPメソッドにより、ブラウザでリクエストを生成 -> サーバーからレスポンスを取得 を通してデータのやり取りをすることが多々あります。
この際によく用いられる、PromiseベースのHTTPクライアントのaxiosではどうしても型による恩恵を受けづらいです。
具体的にはせっかくTypeScriptを導入しているのに、取得したAPIのパラメーターの型定義がany型になってしまい、全部手動で拡張するも、パラメーター変更の可能性まで視野に入れた型を書けるような人間があまりいない(筆者もそう) || 属人性を上げたくないため複雑な型は書かない。など、どのみちanyになるようなケースが見られます。
axios自体に型定義あるんですが、やはりanyが使われていて(dataとかは仕方ないと思います)窓が割れちゃう心配があります。
それを防ぐための拡張の方法も1つではないので、なかなか開発の足並みが揃わないなどが起きがちです。
とにかく非常にaxiosがつらい。
でもやめない
しかし、代表的なフロントエンドのプログレッシブフレームワーク・ライブラリではHTTP通信となると、やはりaxiosの採用が多いと思われます。
Vue.js/Nuxt.js だと、Vue.js公式が推している感 があったり、Nuxt.jsのモジュール にまでなっていてかなり便利、というかこだわりが無い限り今までに見たプロジェクトだとaxiosが組み込まれ、それを人間が割れ窓を作らないように頑張るみたいなケースがありました
Reactだとそこらへん自由に選べますが、なんだかんだaxios もしくは window.fetch とかに落ち着く印象です : https://ja.reactjs.org/docs/faq-ajax.html#how-can-i-make-an-ajax-call
でも結局型は自分たちでどうにかすることになります。しっかりパラメータをカバーできるような型を付け、変更に対応するコスト と 型による恩恵を天秤にかけるのもつらい。
なのでそのつらさをなんとかしたいという気持ちを他方にぶつける話です
aspidaとは
aspida : https://github.com/aspidajs/aspida
ギリシャ語で盾という意味らしいです。守備を固めながら闘う感じがあって良いです
ライブラリについてですが、TypeScriptフレンドリーなHTTPクライアントのラッパーです。
主にaxiosやfetchの他にOpenAPIなどにも対応しています
OSSとして公開されているのでGitHubからソースコードが読めます。このライブラリ自体TypeScriptベースで開発されています。
star数150超えでちゃんとメンテもされていて盛り上がっている印象です。ちゃんと日本語ドキュメントも用意されています。読みやすいです
仕様等は後述のユースケースやTipsで説明していきます
ユースケース
- Qiitaの検索ができるWebクライアントの開発
- コマンド検索がいつまで経っても覚えられないので、クライアントからPOSTしたvalueでAPI叩いて絞り込みたい
- Qiita API叩くロジックはバックエンドに持つ
以上の想定でQiitaのビュワーWebアプリを実装します。
技術選定
- フロントエンド - https://github.com/Qiita-Viewer/qiita-viewer-frontend
- Next.js + Zeit Now
- CDはNowのGitHubとの連携
- バックエンド - https://github.com/Qiita-Viewer/qiita-viewer-backend
- Node.js + Serverless Frameworks + AWS Lambda + AWS API Gateway
- GitHub ActionsでCI/CD、
sls deploy
でAWS Lambdaにデプロイ - データベースは無し
とりあえず1人organizationにして管理した、ここは静かで快適です : https://github.com/Qiita-Viewer
ざっくりアーキテクチャ
フロントのNext.jsはただの好みです、Reactが使いたかった && ルーティングで消耗したくなかった だけです。もれなくTS化します。
バックエンドはServerless Framework + AWS Lambda + API Gatewayを用いて小規模 ~ 中規模の業務レベルを想定したような形です、DB無しで外部のAPI叩いて整形するだけなので若干オーバーな気がしますが。
こちらはServerless公式が提供している TypeScriptテンプレート を用いてTS化します。
この構成の課題
早速課題が出てるんですが、これだとクライアントからアクセスするとき思いっきりLambdaのエンドポイントを晒すことになってしまうので、何かしらの手段でプライベートにする必要があります(VPCとかAPI Gatewayいじれば良さそう? Serverlessでなんとか出来てほしい)
今回はaspidaに絞りたいのでまた別で検証していきます。
[追記:2020/3/21]
ServerlessでCORSオリジン設定したので公開する感じになりました : https://qiita-viewer.now.sh
ゆくゆくはRSSリーダーにでもしたい...
バックエンド
先にバックエンドでのaspida使用部分の説明から入っていきます
とりあえず sls create
で生成したプロジェクトを以下の状態まで持っていき、Actionsでデプロイ設定終了、Qiita APIを叩くところから進めます。ここからですます調が消える
# 生成したプロジェクトに src/ を追加し handler.ts を移動、tsconfig.json と serverless.yml を更新
├── package.json
├── .github/workflows
│ └── deploy-lambda.yml
├── serverless.yml
├── src
│ └── handler.ts
├── tsconfig.json
├── webpack.config.js
└── yarn.lock
aspidaのaxiosラッパーを使用するので公式のインストール方法に従って以下を実行
yarn add @aspida/axios axios
src
に api
ディレクトリを作成
デフォルトでのエントリーは apis
になるが、これはアプリのルートに aspida.config.js
を作成し、中で変更できる
module.exports = {
input: "src/api"
}
次にQiita API叩く部分の実装
とりあえずタグ検索で記事の情報を持ってくるように実装する。Qiita API v2のドキュメントのtags を見ると GET
でクエリ渡せば叩けるので、aspidaでそれ用の型を定義する。
成功するとページ数を指定しないままなので最新から20件記事が取得できる。
aspidaの書き方は、APIのURLのうち、axiosやfetchのdefault configで指定したbaseURL以下の構造を apis/
ディレクトリ以下(今回は src/api
)に作成し、 index.ts
内に型定義を記述する。 --build
で型定義が src/api/$api.ts
に吐き出される.
https://qiita.com/api/v2/tags/${tagName}/items
を叩きたいのでこんな感じになった.
├── src
│ ├── api
│ │ ├── $api.ts # ビルドコマンドで生成
│ │ └── v2
│ │ └── tags
│ │ └── _tagName@string # URLに変数を含む場合、_0@number や _hoge@string のようにディレクトリ名を設定
│ │ └── items
│ │ └── index.ts
tagName
はフロントエンド側からのPOSTで取得する。後で実装
型定義を格納する index.ts
にはこんな感じで書いた
import { APIGatewayProxyEvent } from "aws-lambda";
export interface GetQiitaArticlesResult {
data: string,
input: APIGatewayProxyEvent
};
// Methods という名前のインターフェイスが必要
export interface Methods {
get: {
resBody: GetQiitaArticlesResult
}
};
AWSのイベントの型だけ aws-lambda
から引っ張ってくる。
そして yarn aspida --build
で $api.ts
が生成、ちょっと覗いてみる
/* eslint-disable */
import { AspidaClient } from 'aspida'
import { Methods as Methods0 } from './v2/tags/_tagName@string/items/index'
const api = <T>(client: AspidaClient<T>) => {
const prefix = (client.baseURL === undefined ? '' : client.baseURL).replace(/\/$/, '')
return {
v2: {
tags: {
_tagName: (val0: string) => ({
items: {
get: (option?: { config?: T }) =>
client.fetch<Methods0['get']['resBody']>(prefix, `/v2/tags/${val0}/items`, 'GET', option).json(),
$get: async (option?: { config?: T }) =>
(await client.fetch<Methods0['get']['resBody']>(prefix, `/v2/tags/${val0}/items`, 'GET', option).json()).data
}
})
}
}
}
}
export type ApiInstance = ReturnType<typeof api>
export default api
単純に GET
するだけでもこれだけ型が必要だと思うとaspidaに頼る価値があると思う。
ちゃんとlambda関数を記述する部分に型を読み込んでやると補完が利いている、うれしい
これでとりあえずタグで記事を引っ張ってくるを叩く関数が完成したので、次はフロントエンド(ざっくり話してるので詳細な実装はリポジトリを見て下さい)
余計な話
sls offline
でLambdaをオフライン実行出来る serverless-offline が便利で挙動確認が楽になります
フロントエンド
-
yarn create next-app
でガッと生成、TS化、Nowにデプロイ - 特殊なことはしていないが、
Next.js v9~
なのでBuiltin-CSSでCSSリセットしたりしてた - Zeitさんへ GUIでNowの環境変数管理できるようにしてほしいです
aspidaで先程のバックエンドのエンドポイントに POST
する。LambdaのエンドポイントはNowに環境変数として登録する
まずは types/api
を aspida.config.js
の input
にセットして、型定義作成
Lambdaで、 ${LAMBDA_ENDPOINT}/api-v2/get-articles
で待機しているので get-articles
内に index.ts
をおいて以下を記述
export interface GetArticlesParams {
tagName: string
}
export interface GetArticlesResponse {
statusCode: number
articles: Array<{
title: string
tags: Array<string>
url: string
user: {
id: string
profile_image_url: string
}
}>
}
export interface Methods {
post: {
reqBody: GetArticlesParams
resBody: GetArticlesResponse
}
}
POST
する部分はこんな感じになっている
const submitQiitaTag = async () => {
const client = api(aspida(axios, axiosConfig));
const res = await client.get_articles.post({
data: {
tagName: inputText
}
})
res.data.articles.map((article) => {
const f = {
title: article.title,
tag: article.tags,
url: article.url,
userID: article.user.id,
userImage: article.user.profile_image_url
};
info.push(f);
});
return info;
};
submitQiitaTag()
.then((data) => {
setArticlesInfo(JSON.stringify(data));
})
.catch((e) => {
console.error(e);
})
.finally(() => {
setIsClicked(false);
});
出来上がりはこちら
後で非同期のLoadingアニメーションとか入れておきたい
例によってクライアント側にエンドポイント晒されちゃうのでどうにかします、ローカルホストだと安心して動かせるので使いたい場合クローンしてみて見てください
↑対応しました
Tips等
watchモードについて
型をビルドするとき、yarn aspida --build
で行っていましたが、yarn aspida --watch
にするとwatchモードとして起動出来ます。
ちゃんと型更新して保存するとホットリロードで$api.ts
が更新されるのでDXが良いgaxiosとの比較
Google製のaxiosライクなHTTPクライアントです : https://github.com/googleapis/gaxios
ちゃんと触っていないのでなんとも言えませんが、コード見た感じnode-fetchをラップしているのでそもそもの作りがaxiosと違いそう。
型定義でやっぱり比較したいのでちょっと潜ってみたんですが、responseのデータを型変数で書く感じになっていますね。拡張はやっぱりaxios書いた時みたいな感じになりそう
aspidaと比較して日本語の文献/docsの少なさも目立ちます、axiosからの移行先なのでみんな見なくてもわかる前提っぽい
まとめたい
aspidaについて
-
--watch
が欲しいと言っていたらあった、良かったです。aspida開発者の @m_mitsuhide さん、教えてくださってありがとうございます - responseのdataにちゃんと型を付けられるのが安心、方法もaspidaに則るだけで良いので楽
- axiosにがっつり依存していてるプロジェクトの型拡張をするとき、aspida自体ラッパーなので導入&型付けまでコケることが少ない
- aspida も gaxios もどっちも頑張ってほしい、どうかフロントから any を滅ぼして下さい
ユースケースで作ったものの残滓 : https://qiita-viewer.now.sh
- UIもちゃんと作ったのでLambdaの対策だけしてアプリ公開したい、張り切ってロゴまで作っちゃった...
- そこそこの規模になってきたら、DB(Dynamoとか?)くっつけて
PUT
DELETE
までやりたい- RSSリーダーとかにすれば色々機能増やしたくなりそう
- 欲しい機能ある or 開発したい 等要望あれば随時機能や人間がスケーリングするスタイルなので、@did0es までお願いします