TypeScript APIの接続先とURLの組み立てを一か所に集める
開発環境では別ポートのAPI、本番環境では公開APIへ接続する場合、各画面へ接続先の文字列を書くと、環境を変えるたびに修正が必要になります。
今回は、接続先を設定として受け取り、APIのパスを解決する処理を1つにまとめる方法を紹介します。
URL全般を扱う万能な関数ではなく、アプリ内のAPI通信に使うURLへ用途を限定します。
文字列の結合だけではルールが曖昧になる
接続先の末尾とパスの先頭に両方/があると、単純な結合ではスラッシュが重複します。どちらにもない場合は、ホスト名とパスがつながってしまいます。
URLを使えば解決できますが、今度は基準URLにパスを含めるか、絶対URLを受け取るかも決める必要があります。
この記事では、次のルールにします。
| 項目 | ルール |
|---|---|
| 設定する接続先 | https://api.example.comのようなオリジンだけ |
| APIのパス | /api/から始まるもの |
| 同じ接続先の絶対URL | 受け付ける |
| 別の接続先の絶対URL | エラーにする |
| URL末尾のフラグメント | APIへ送るURLから取り除く |
オリジンは、プロトコル、ホスト名、ポートの組み合わせです。#sectionなどのフラグメントはHTTPリクエストへ送られない情報なので、ここでは戻り値からも取り除きます。
接続先を検証してから関数を作る
createApiUrlResolver.tsexport const createApiUrlResolver = (baseUrl: string) => {
const base = new URL(baseUrl.trim());
if (
!['http:', 'https:'].includes(base.protocol) ||
base.username !== '' || base.password !== '' ||
base.pathname !== '/' || base.search !== '' || base.hash !== ''
) {
throw new Error('API接続先にはHTTPまたはHTTPSのオリジンを指定してください。');
}
return (path: string): string => {
const url = new URL(path, base);
if (url.origin !== base.origin) {
throw new Error('設定されたAPI接続先とは異なるURLです。');
}
if (!url.pathname.startsWith('/api/')) {
throw new Error('APIのパスは/api/から始めてください。');
}
url.hash = '';
return url.toString();
};
};形式として不正な接続先は、new URLの時点でも例外になります。画面からAPIを呼ぶたびに設定を読み直すのではなく、アプリの初期化時にこの関数を作ります。
利用例import { createApiUrlResolver } from './createApiUrlResolver';
const resolveApiUrl = createApiUrlResolver('https://api.example.com');
console.log(resolveApiUrl('/api/products?page=2'));
// https://api.example.com/api/products?page=2実際のアプリでは、接続先を環境設定から渡します。ブラウザーへ渡る設定値は利用者から確認できるため、接続先の設定へ秘密鍵やパスワードを含めません。
基準URLのパスを含めない理由
https://example.com/service/を基準にした場合、itemsと/itemsでは解決先が異なります。先頭に/があるパスは、ホストのルートを基準にします。
この例では、その違いで迷わないよう、設定をオリジンだけに限定しています。
APIを/service/api/配下へ配置する必要があるなら、このルールはそのまま使えません。基準パスを含む設計へ変更し、先頭スラッシュの扱いもテストします。
別の接続先をどう扱うかを決める
new URL('https://other.example/api/products', base)は、baseと関係なく指定された絶対URLを返します。
そのため、設定した接続先だけを使う方針なら、解決後のoriginも確認します。//other.example/api/productsのようにプロトコルを省いたURLも、解決後の比較で対象外にできます。
別の方針として、受け取ったURLのパスだけを使い、接続先を設定値へ差し替える方法もあります。ただし、呼び出し側の誤りを隠すこともあるため、この記事では黙って書き換えずエラーにする方針を選んでいます。
API用と外部リンク用を分ける
商品画像のCDNや外部サイトへのリンクまで、すべてこの関数へ渡す必要はありません。
API用の関数は、決めた接続先とパスのルールを守るために使います。外部リンクや別サービスへの通信は、それぞれの用途で扱います。
共通処理にallowExternalなどの例外が増え続けるなら、用途を広げすぎていないかを確認します。
この例はURLの組み立て方を限定するものです。サーバーの認証・認可、CORS、通信先からのリダイレクトなどは別の仕組みです。
URLがこの関数を通ったことだけを理由に、操作が許可されていると判断しないようにします。
接続先を変えるだけで同じ確認を使えるようにする
| 入力 | 期待する結果 |
|---|---|
/api/products | 設定した接続先になる |
api/products | 基準をルートにした同じパスになる |
| 同じオリジンの絶対URL | 受け付ける |
| 別オリジンの絶対URL | エラーになる |
/apiary/products | /api/配下ではないためエラーになる |
/api/../admin | 解決後のパスが範囲外なのでエラーになる |
/api/products?page=2#top | クエリを保ち、フラグメントを除く |
検索条件そのものの変換は、別の小さな処理として扱えます。
接続先を一か所へ集めると、環境変更時の修正範囲を減らせます。文字列の結合を共通化するだけでなく、何を受け付けるかを決めておくと、接続先の誤りも早く見つけられます。