型安全に URL を構築できるライブラリ routopia を作った
はじめに
Web アプリなどソフトウェアの開発していると、必ずと言っていいほど URL の構築を行い、リンクを設定したり画面遷移させたりという制御を行うと思います。
ただ他の多くの記事でも触れられている通り、 URL の構築には以下のような課題があります。
- パスやパラメータ名の typo
- パラメータの漏れ・型の不一致
- 変更追従漏れ
- そもそも期待値がわからず実装を読み解く必要がある
- エンコード漏れ
みなさんもこういった問題に遭遇したことはありませんか?
これらの問題は単純にバグの温床になるだけではなく、開発者の生産性や体験を下げる要因にもなっています。
今回はそれを解決するために routopia
というライブラリを開発してみました。
このライブラリは、型安全で開発体験 (DX) に配慮した TypeScript 製 URL ビルダーライブラリで、宣言的に URL 設計を定義しそれから型安全に URL を構築することができます。
基本的な使い方
まずは routopia
の基本的なインターフェースがどういったものかをみていきましょう。
1. インストール
npm
パッケージとして提供しているのでインストールして利用します。
npm i routopia
2. スキーマの定義
routes
関数を使って、URL 生成オブジェクト myRoutes
を作成します。
引数にスキーマ定義としてエンドポイントのパスと、各 HTTP メソッドで必要なパラメータの型を定義します。
import { routes, empty, type } from 'routopia';
export const myRoutes = routes({
/** ユーザー一覧エンドポイント */
"/users": {
get: empty,
post: empty,
},
/** ユーザー詳細エンドポイント */
"/users/[id]": {
get: {
params: {
/** ユーザーID */
id: type as number,
},
queries: {
/** なにかしらのクエリ */
q: type as string | undefined,
},
hash: type as "anchor1" | "anchor2",
},
},
});
3. URL の生成
上で作成したオブジェクトを使います。
スキーマ定義に従って、TypeScript の型チェックやエディタの補完がサポートしてくれます。
スキーマ定義に書いたコメントもエディタで表示されます。
import { myRoutes } from './path/to/myRoutes';
myRoutes["/users"].get();
myRoutes["/users"].post();
// => "/users"
myRoutes["/users/[id]"].get({ params: { id: 123 } });
// => "/users/123"
myRoutes["/users/[id]"].get({ params: { id: 123 }, queries: { q: "abc" }, hash: "anchor1" });
// => "/users/123?q=abc#anchor1"
基本的な使い方は以上です!
より詳細な機能・使い方については README をご参照ください。
routopia
の主な特徴とメリット
routopia
には以下のような特徴があります。
安全性
- 各種パラメータのキー名・型チェック
- (当然ながら)URL 構築時のエンコード処理
- URL 結果は
template literal types
で可能な限り厳密に型推論-
/users/${number}
のような具体的な型 - なお呼び出し時に
.get({ params: { id: 123 }} as const)
とすれば/users/123
まで確定する
-
開発体験(DX)に配慮した設計
- 各種パラメータの可能な限りの補完
- 呼び出し側からの JSDoc 参照、定義元へのコードジャンプ
- オプショナルな引数は省略可能に
宣言的でシンプルなインターフェース
- 設定不要で、プレーンなオブジェクトでスキーマを定義
- フレームワーク・環境に非依存
- BaseURLの設定や可変長パスパラメータなど主要な要件にも対応
実現方法
細やかな DX への配慮を実現するために TypeScript の高機能な型の恩恵にあやかってめっちゃパズルしました。
それぞれの型に具体的な動作例などコメントを書いているので、興味ある方はどうぞ
(どっかミスってるとかもっといい書き方あるとかはあるかもしれないので悪しからず...)
ちなみに作った後に知ったのですが、パズル中で使っているテクニックがわりと type-challenges の問題と同じだったりしていました。
パズル力を鍛えるにはいいのかもしれない?(安易な型パズルはメンテナンス性を犠牲にするため用法用量をよく守って TypeScript しましょう)
開発の動機
最後に、既存のよくできたライブラリなど、エコシステムが多くある中わざわざ開発をするに至ったのかについて書いておきます。
私が関わっていたプロジェクトでは、(routopia の前身となる) API エンドポイントの URL を型安全に構築するための内製ライブラリを使っていましたが、以下のような課題が上がりました。
- エンドポイント定義時に補完が効かず、使い方を忘れがち
- パラメータなどの説明 (JSDoc) が利用側で表示されない
- 利用側から定義元へのコードジャンプができない
既存のライブラリや記事等で紹介されている手法なども調べましたが、同様の課題を抱えていたり、特定のフレームワークに依存していたり、歴史あるプロジェクトなので自動生成系に移行するハードルが高すぎたりなど、様々な要因から要件に完全にマッチするものが見つけられませんでした。
内製ライブラリはかなり開発体験がよく、ベースはそのままに課題を解決したいという思いがチーム内で強かったので、「宣言的な記述でシンプルに使え、かつ TypeScript の型システムの力を最大限に活かして、補完や型チェック、コードジャンプや JSDoc 参照といった開発体験を得られる、URL 構築に特化したビルダー」をコンセプトにして開発しました。
まあ実は初めは内製ライブラリの改善だったのでそこまで深く考えていませんでしたが、改善していく中で API の URL 構築だけでなく、内部リンクの URL 生成にも流用したり、ちょっとした開発で大袈裟なライブラリを使うのが面倒な場合にも使えると気づき、汎用的な URL 構築ライブラリとして作成した感じです。
まとめ
- 型安全で DX に配慮した TypeScript 製 URL ビルダーライブラリ
routopia
を開発した - 宣言的なスキーマ定義から、型安全に URL を構築することができる
- パスパラメータやクエリパラメータに加えて可変長なパスパラメータや BaseURL といった一般的な要件に対応済み
- template literal types を利用した詳細な URL 結果の推論で Next.js typedRoutes のような機能にも対応可能
- 自動生成など既存のエコシステムではなく、あえて手打ち・もしくはミニマムな URL ビルダーを求めているなら使えるかも
Discussion
変更ログ