zukucode
主にWEB関連の情報を技術メモとして発信しています。

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.ts
export 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クエリを保ち、フラグメントを除く

検索条件そのものの変換は、別の小さな処理として扱えます。

検索条件のURL変換をまとめる方法

接続先を一か所へ集めると、環境変更時の修正範囲を減らせます。文字列の結合を共通化するだけでなく、何を受け付けるかを決めておくと、接続先の誤りも早く見つけられます。


関連記事