Help us understand the problem. What is going on with this article?

HTTPクライアントにおけるaspidaという選択

概要

  • 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アプリを実装します。

技術選定

とりあえず1人organizationにして管理した、ここは静かで快適です : https://github.com/Qiita-Viewer

ざっくりアーキテクチャ

aspida-qiita.png

フロントの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

srcapi ディレクトリを作成
デフォルトでのエントリーは apis になるが、これはアプリのルートに aspida.config.js を作成し、中で変更できる

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 にはこんな感じで書いた

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 が生成、ちょっと覗いてみる

$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関数を記述する部分に型を読み込んでやると補完が利いている、うれしい

スクリーンショット 2020-03-16 17.03.34.png

これでとりあえずタグで記事を引っ張ってくるを叩く関数が完成したので、次はフロントエンド(ざっくり話してるので詳細な実装はリポジトリを見て下さい)

余計な話

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/apiaspida.config.jsinputにセットして、型定義作成
Lambdaで、 ${LAMBDA_ENDPOINT}/api-v2/get-articlesで待機しているので get-articles内に index.tsをおいて以下を記述

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);
  });

resにしっかり型が利いている、ありがとう
スクリーンショット 2020-03-16 17.43.33.png

出来上がりはこちら
後で非同期のLoadingアニメーションとか入れておきたい
q-v-pre.gif

例によってクライアント側にエンドポイント晒されちゃうのでどうにかします、ローカルホストだと安心して動かせるので使いたい場合クローンしてみて見てください

↑対応しました

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書いた時みたいな感じになりそう
スクリーンショット 2020-03-16 18.55.24.png

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 までお願いします
did0es
街のWeb屋さん
https://did0es.me
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away