magcho's blog

AxiosにJWTつけてレスポンスに型もつけたい

Created at: Last updated at:

JWTを用いたAPIリクエストをするSPAをReactで作っていた中で、全てのAxiosのリクエストのヘッダーにJWTを付与してさらにレスポンスに型をつけたかった。

SPAからバックエンドサーバーに通信する際のリクエストヘッダーにBearer: {JWT}を付与したく、さらにはJWTの有効期限が切れている場合は勝手にJWTを更新した上でリクエストをする仕組みが欲しかった。

axiosのレスポンスに型をつける

Axiosの型定義を見るとAxiosResponce<T = any>といった形でレスポンスの型情報1に型パラメータが使える。

export interface AxiosResponse<T = any>  {
  data: T;  // 👈
  status: number;
  statusText: string;
  headers: any;
  config: AxiosRequestConfig;
  request?: any;
}

しかし、Axiosは非同期的に使うと思うのでawait axios.get()の返り値はAxiosPromiseですが、以下のような型定義2なので同じとみなせます。

export interface AxiosPromise<T = any> extends Promise<AxiosResponse<T>> {}

ということは、以下のようなコードがかけます。

https://example.com/api/のレスポンスが以下の型である時

interface ApiResponceType {
  name: string
  id: string
}

リクエストするAxiosのコードは

import axios, { AxiosRequestConfig, AxiosResponce } from 'axios'

const responce = await axios.get<ApiResponceType>('https://example.com/api/')

responce.data // ここの型がApiResponceTypeになる

Bearerヘッダーを付与・JWTの有効期限も考慮する

axiosのインスタンスを作り、そこにBearerヘッダーを付与しておきます。さらにaxiosのrequest/responceにミドルウェア的に処理を挟み込める仕組みとしてinterrceptors4があるので、これを用いてJWTの有効期限が切れていた時はJWTの更新・再リクエストをおこないます。

import axios, { AxiosResponse, AxiosError, AxiosRequestConfig } from 'axios'

export const apiClient = async <T, R = AxiosResponse<T>>(
  url: string,
  method: 'get' | 'post',
  data?: any,
  headers?: Map<string, string>,
  ...requestConfig: any
): Promise<R> => {
  if (!user) {
    throw new Error('undefined currentuser')
  }
  const jwtToken = getJwtToken() // 👈 任意の方法で保持しているJWT

  const config: AxiosRequestConfig = {
    ...requestConfig,
    url: url,
    method: method,
    data,
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${jwtToken}`,
      ...headers,
    },
  }

  const customAxios = axios.create()

  let isRetry = false
  customAxios.interceptors.response.use(
    (res: AxiosResponse<T>) => res,
    async (error: AxiosError) => {
      if (error.response?.status === 401 && isRetry) {
        // 👈 JWT有効期限切れかつ一回目のリクエストである時
        isRetry = true
        await refreshJwtToken()

        const originalRequestConfig = error.config
        return customAxios.request(originalRequestConfig) // 👈 再度リクエスト
      } else {
        return Promise.reject(error)
      }
    }
  )
  return customAxios.request(config)
}

const responce = apiClient<ApiResponceType>('https://example.com/api/', { id: 1 })
responce.data // 👈 ApiResponceTypeの型がつく

google analyticsを導入しています