APIサーバ、フロントエンドともにTypeScriptで、とあるベンチャーのCTO*として1人でWebアプリを作りました。忘れないうちに感想をまとめておきたいと思います。(*記事を書いている現在はフリーランスとして活動しています。)
要約すると
全体について
完璧とはいえないまでも、基本的にはとても良かったです。
フロント
ある程度良かったです。今回はNuxt(Vue2)を使ったのですが、Vue(正確にはtemplate部分にjsxを利用しないフレームワーク?)とTypeScriptとの相性があまりよくないのかなと思いました。その意味で次はReactを試したいです。また、Vuexやスキーマから自動生成(OpenAPI Generator)されたクライアントコードとTypeScriptは相性が良かったと思います。
バックエンド
全体的にとても良かったです。(あえて不満を述べるなら、後述するように今回はスキーマ定義としてOpenAPIを利用しましたが、これはバックエンドのAPIのinterface相当のものを出力できなくてその部分は不満でした。結果的にはTypeScriptの型関数を駆使して 7割くらいのエンドポイントに関しては(typescript-axiosテンプレートから出力されたTSコードを利用して)ある程度型を当てることができました。)
スキーマ
OpenAPIおよびOpenAPI Generator(typescript-axiosテンプレート)を利用しました。基本的には良かったですが出力されるコードで肝心の型情報が失われていることがたまにあったので、次はgRPCWebを試したいです。
補足
タイトルから特にAPIスキーマについて単一のTypeScriptの型情報をフロント、バックエンドどちらかにおき、両方のソースコードから参照する方法を想定された方もいらっしゃるかも知れませんが、それは行いませんでした。(行ってもいいと思いますが、今回はバックエンドとフロントエンドをより疎結合にする(それにより、たとえばバックエンドのみ別言語にリプレイスしやすくなる)ため、スキーマに関してはなるべくプログラミング言語非依存にしようと考えました。)
技術スタック
全体的なソースコードは公開できませんが、主な技術スタックは次の通りです。
APIサーバ
- TypeScript(3.9.7)
- Node.js(14.4)
- server: Express
- ORM: sequelize
- API設計: REST
- ソースコードアーキテクチャ: オニオンアーキテクチャ(なるべく)
- テスト: jest
フロントエンド
- framework: Nuxt.js(2.11)
- Node.js for SSR(12.13.0)
- TypeScript(3.7.5)
- Vue(2.6.11)(with vue-property-decorator)
- Vuex(with vuex-module-decorators)
- nuxt-i18n(4か国語)
スキーマ定義
- OpenAPI
- 編集ツール: Stoplight Studio
- OpenAPI Generator(公式Dockerイメージ)
インフラ
- Heroku
- DB: MySQL(JawsDB)
CI/CD
- Heroku Pipelines
- GitHub Actions + GitHub Packages(openapi-generatorで出力したファイルをGitHub Actionsでプライベートにホスティング)
その他
僕の開発環境
- code-server
- spot instanceのEC2でホスティング(terraformで管理)
読んで良かった本
- TypeScriptの良い書き方: Effective TypeScript
- DDDっぽい書き方: ドメイン駆動設計 モデリング/実装ガイド
TypeScriptの好きな機能
Gitレポジトリ構成
- xxx-api
- xxx-front
- xxx-openapi-schema
各分野に関して感想
バックエンド
Express
前提として僕はRuby on Rails出身で、Expressで本格的なアプリは初めて作った形になります。
Expressは非常にミニマムで、
・nodeのhttpサーバモジュールの薄いラッパー
・HTTPリクエストを解析してルーティングを行ってくれる
・Railsと同じように抜き差し可能なミドルウェアの連なりを持つ(簡単に外部の機能をミドルウェアとして挟める)
ような特徴をもったものだと考えています。
余談ですがExpressの本体はTypeScriptでは書かれておらず、TypeScriptの型はDefinitelyTypedから持ってくる必要があります。個人的にはソースコードが追いやすいのでExpress自体がTypeScriptで書き直されて欲しいです。
とはいえ通常利用する分には全く問題なく、DefinitelyTypedから型定義を持ってきて利用すると例えばExpressのRequstやReponseクラスに型定義が付くおかげで、これらに対してどんなメソッドが呼べるのかすぐ分かるなどのメリットはちゃんとありました。
TypeScriptに移行してExpressに関して大きく変化した部分は、RequestHandlerというジェネリクス型です。このRequestHandlerジェネリクスはExpressのミドルウェア型を表すもので、(型引数を渡さなくても利用できますが)そのエンドポイントのリクエストやレスポンスの型を型引数として渡せばそのミドルウェアをより詳細に型付けできます。具体的にはreqやresに型がつき、req.bodyから存在しないキーで取得したり、res.jsonに誤った引数を渡す(型レベルで誤ったAPIレスポンスを返す)ことがなくなります。
今回はOpenAPIという規格で(個人的には初めての試みでしたが)、API仕様を定義しました。できればこのスキーマ情報から型付きのRequestHandlerを自動生成できたら良かったのですが、OpenAPIからのサーバサイドの型生成はできませんでした(おそらく基本的にserver stubが限界だと思います)。なので、苦肉の策ですがクライアントのコード生成をtypescript-axiosテンプレートで行い、そこから型関数を利用してなんとかRequestHandlerへAPIの型を渡していました。とはいえエンドポイントのmethodやpathまではOpenAPIからは生成することができず、中途半端になってしまいました。最終的にこれは無理やりすぎてあまり良かった方法でもないかなと思うので、宿題です。
(参考)typescript-axiosテンプレートで生成されたクライアントコードからAPI定義だけ引き抜くコード
// openapi-schemaレポジトリ// tsディレクトリ: generate済みのtypescript-axiosコードimport{DefaultApi}from'./ts'import{AxiosResponse}from'axios'typeThenArg<T>=TextendsPromiseLike<inferU>?U:TtypeAxiosArg<T>=TextendsAxiosResponse<inferU>?U:TexporttypeExtractResponseType<TextendskeyofDefaultApi>=AxiosArg<ThenArg<ReturnType<DefaultApi[T]>>>exporttypeExtractRequestType<TextendskeyofDefaultApi>=Parameters<DefaultApi[T]>[0]
ロジックなど
Webアプリケーションはいろいろ複雑な処理やオブジェクトを書く必要があり、そういったコードはしばしば(書くときはまだしも)読み解くのが難しくなると思います。こういったコードを読み解く際、やはり型があると型だけ見ればなんとなくやりたいことが分かるのでソースコードの理解が早くなるかな、という印象はありました。
アーキテクチャ
やはり型があると、(静的型付け言語をある程度前提とするような)アーキテクチャ論を勉強した際にその恩恵を実感しやすかったです。たとえばDIを行う場合、rubyやJavaScriptだとその依存注入が妥当かどうか型レベルで検査できませんが、TypeScriptだとできます。
まとめ
良かったこと
- TypeScriptの型機能を存分に利用できる
- フロントと同じ言語なので頭の切り替えが楽
- 場合によっては、フロントからバックへ、あるいはバックからフロントへコピペ+多少の手直しで済ませられることがある(本当は共通化しても良いのかも知れない)
- npmライブラリを眺める時、
universal
という言葉があるとテンションが上がる
悪かったこと
- 型を付けないといけない
- 型にこだわりすぎると進まない
- (TypeScript全般と思われるが)トランスパイルする際に変数名が変わる場合があるので、debuggerで止めて変数の中身を確認するとき、debugコンソールに存在するはずの変数名を入力しても取得できない場合がある(この辺(VSCode)とかこの辺(TypeScript)とか)
- HerokuでSentry(エラー監視ツール)にソースマップを送るのが大変だった
- (ORMライブラリのsequelizeが苦手)
まとめ
- 個人的な体験としてはNuxtのフロントよりTypeScriptの恩恵は大きかった。ただ、debug時に変数が変わってしまっていてそれがdebugコンソールにて補正されていないのが気になった。
- 個人的な興味として選べるなら次はgRPCWebとGoでやってみたい
フロントエンド
Vue(Nuxt)
前提として僕はVue経験あり、React経験なしでした。このアプリは最初はJavaScriptで書き始めて途中からTypeScriptに置き換えたのですが、他でも言われている通りTypeScriptの型システムを隅々まで活用したい場合、VueよりReactの方が相性がいいのかなと思いました。
Vue+TypeScriptで個人的に特に気になったのは、
- (VeturというVSCodeのextentionで一応対応はしていましたが)templateの中で利用する変数が存在してなくてもコンパイルエラーにならない。また、誤った型のpropsを渡してもコンパイルエラーにならない。
- SFC(single file component)を少なくともVSCode上でauto importできない(Reactのコンポーネントや普通のTypeScriptのクラス、関数はできた)
あたりでした。
Vuex(Nuxt)
TypeScriptを導入したことにより、Vuexを型付けできること自体は良かったです。例えばアクションを呼びたい場合、通常はstore.dispatch('user/fetch', { id: 3 })
というようにアクション名をstringでdispatchを呼ばないといけないですが、これをuserModule.fetch({ id: 3 })
というようにメソッドで呼べるので良かったです。
特にAPIを呼ぶ部分がOpenAPI Generatorで生成したクライアントコードで書けたので、レスポンスにも型がついていて良かったです。
ただ、vuex-module-decoratorsが特にNuxtのSSRと組み合わさった場合に個人的に扱いづらく、安定的にSSRできるまで難儀しました。
まとめ
良かったこと
- 型を使える
- 特にAPI部分にもスキーマから型をつけられると良い
悪かったこと
- Vue(Nuxt)よりReactの方がTypeScriptとの相性が良いと思われる
- NuxtのVuex(SSR)+vuex-module-decoratorsが大変だった
まとめ
- 次はReact+gRPCWebでやってみたい。
スキーマ定義
Spotlight Studio
こちらを、OpenAPIの定義ファイル(OAS)編集ツールとして利用しました。
ずっと無料で使えて個人的にはとても使いやすかったです。
OpenAPI Generator
期待値が高すぎましたが、使って良かったと思います。ただ、oneOfやnullがしばしば使えなかったり、ステータスコードごと(200,404,500など)にレスポンスのスキーマを定義しても出力されたTypeScriptコードでは1種類にまとめられていたりなど、OpenAPIの理想にコード生成機能が追いついていない印象は受けました。
まとめ
- 大体良かったが、次はgRPC Webを試してみたい
終わりに
TypeScriptでアプリを作る際のなにかしら参考になれば幸いです。誤りや修正点を見つけた際は指摘いただければ幸いです。ここまでお読みいただきありがとうございました。
ここまでお読みいただきありがとうございました。