シンプルなaxiosのラッパー

19701 ワード

APIを叩くクライアントまわりは、いつも迷うが、最終的に薄いラッパースクリプトになった。

  • エラーハンドリング
  • axiosに渡すデフォルトの設定

などを意識して作った。

import {
  AxiosError,
  AxiosRequestConfig,
  AxiosResponse,
  AxiosResponseHeaders,
} from 'axios';
import { nanoid } from 'nanoid';
import { getAccessToken, TokenError } from 'your-access-token-service';

export type ResponseAPI<Body = unknown, Header = unknown> =
  | {
      isSuccess: true;
      body: Body; // これbodyがないとき、body: undefinedって指定しないといけないので不便。なにか他に方法ないのか。
      header: Header;
    }
  | {
      isSuccess: false;
      error: string;
    };

const isAxiosError = (error: any): error is AxiosError => {
  return !!error.isAxiosError;
};

/**
 * axiosのエラーハンドリングとデフォルトの設定をやるヘルパー関数。
 * @param request 引数1requestIdは自動生成id。引数2defaultConfigはbaseURLとアクセストークンの設定
 * @param defaultErrorText
 * @returns
 * @example
 * // axios単体で使う
 * requestAxios(
 *   () =>
 *     axios.post<LineTokenResponse>(
 *       'https://example.com',
 *       { hoge: 'hoge' },
 *       {
 *         headers: { 'Content-Type': 'application/json' },
 *       },
 *     ),
 *   'hoge作成に失敗しました!',
 * );
 *
 * // openapi generator の sdkと一緒に使う
 * const hogeApi = new HogeApi();
 *
 * requestAxios(
 *  (requestId, defaultConfig) =>
 *    HogeApi.getById(
 *      hogeId,
 *      defaultConfig,
 *    ),
 *  'hogeの取得に失敗しました!',
 *);
 */
export const requestAxios = async <T = unknown>(
  request: (
    requestId: string,
    defaultConfig: AxiosRequestConfig,
  ) => Promise<AxiosResponse<T>>,
  defaultErrorText: string,
  isSkipToSetToken?: boolean,
): Promise<ResponseAPI<T, Record<string, string>>> => {
  try {
    const accessToken = isSkipToSetToken ? '' : await getAccessToken();
    const defaultRequestId = nanoid();
    const defaultConfig = {
      headers: isSkipToSetToken
        ? undefined
        : {
            Authorization: `Bearer ${accessToken}`,
          },
      // 残念ながらurlを絶対パスで指定されている状態でaxios.request()するとbasePathが無視されるみたい。
      // openapi generator で生成されたSDKはurlを絶対パスを渡すため、この設定は無視される。
      // baseURLの代わりにbasePathをConfigurationから渡すこと。
      baseURL: process.env.API_URL,
    };

    const res = await request(defaultRequestId, defaultConfig);
    return {
      isSuccess: true,
      body: res.data,
      header: res.headers,
    };
  } catch (err) {
    console.error(err);
    if (isAxiosError(err)) {
      if (err.response) {
        if (err.response.status >= 500) {
          return {
            isSuccess: false,
            error: `${defaultErrorText} サーバーエラーです。しばらく経ってからもう一度試してみてください。`,
          };
        }

        if (err.response.status >= 400) {
          return {
            isSuccess: false,
            error:
              err.response.data?.message ?? `${defaultErrorText} リクエストエラーです。`,
          };
        }
      } else if (err.request) {
        return {
          isSuccess: false,
          error: `${defaultErrorText} リクエストがタイムアウトしました。`,
        };
      } else {
        return {
          isSuccess: false,
          error: `${defaultErrorText} 原因不明のエラーが発生しました。`,
        };
      }
    }

    if (err instanceof TokenError) {
      return {
        isSuccess: false,
        error: 'セッションの期限切れです。ログインし直してください。',
      };
    }

    return {
      isSuccess: false,
      error: `${defaultErrorText} ネットワークエラーです。`,
    };
  }
};

使い方

const res = await requestAxios(
   (requestId, defaultConfig) =>
     axios.post(
       'https://example.com',
       { hoge: 'hoge' },
       defaultConfig
     ),
   'hoge作成に失敗しました!',
);

if(!res.isSuccess){
  console.error(res.error);
  return;
}

console.log(res.body);