【JavaScript】axiosインスタンスとAPI連携クラスを作って非同期処理をいい感じで書く

JavaScript

axiosはフロントエンドでAPI連携を実装する際の定番HTTPクライアントです。
API連携が必要な箇所でaxios.get(...)という感じで愚直にコードを書いてもいいのですが、API連携に関する専用クラスを用意してあげると実装がすっきりします。

今回はaxiosを利用したAPI連携クラスの作成方法について紹介します。

axiosを利用したAPI連携を素直に実装する場合

今回はTodoアプリで理解するRedux Thunkによる非同期処理の実装方法で紹介したReact Reduxのアプリケーションを具体例として利用します。
以下のソースコードではRedux Thunkという非同期処理用のミドルウェアと、axiosを利用してAPI経由でデータをやりとりしています。

src/redux/action.ts

import { ActionTypes } from "./actionTypes";
import { TodoItem, RootState } from "./types";
import { Dispatch } from "redux";
import axios from "axios";
import { ThunkAction } from "redux-thunk";

type SetTodosAction = {
  type: ActionTypes.SET_TODOS;
  payload: { todos: TodoItem[] };
};

export const setTodos = (todos: TodoItem[]): SetTodosAction => ({
  type: ActionTypes.SET_TODOS,
  payload: { todos },
});

export const fetchTodos = (): ThunkAction<
  void,
  RootState,
  unknown,
  TodoActions
> => {
  return async (dispatch: Dispatch<TodoActions>) => {
    // axiosを利用してAPI連携をしている部分
    // http://localhost:4000 で起動しているAPIに接続
    // 『/todos』というエンドポイントにアクセスし、データを取得する
    const response = await axios
      .get(`http://localhost:4000/todos`)
      .catch((error) => {
        throw new Error(error.message);
      });
    dispatch(setTodos(response.data));
  };
};

export type TodoActions = SetTodosAction;

上記のままでも問題ないのですが、以下のような改善点があります。

上記のコードの改善点
  • APIにアクセスするたびにホスト名を記述する必要があるので冗長
  • ActionのソースコードにAPI連携のロジックもあるのでコードが長くなる

以下では、順を追ってリファクタリングの方法について紹介をします。

APIの基本設定はaxiosのインスタンスに保存する

axios.createでインスタンスを作成できます。インスタンスにはAPIの基本設定が保存できます。

設定できる情報の詳細はaxios#Request Configを参照してください。

たとえば、『http://localhost:4000に対してAPIリクエストを行う』という設定をインスタンスに保存する場合は以下のようにします。

src/api/index

import axios from "axios";

const baseURL = `http://localhost:4000/`;
const axiosInstance = axios.create({
  baseURL: baseURL,
});

export default axiosInstance;

axiosインスタンスの設定を利用してAPIアクセスをする場合はaxiosのインスタンスメソッドを利用します。
axiosインスタンスを利用すると以下のようにリファクタリングできます。

src/redux/action.ts

import { ActionTypes } from "./actionTypes";
import { TodoItem, RootState } from "./types";
import { Dispatch } from "redux";
import axios from "axios";
import { ThunkAction } from "redux-thunk";
+ import axiosInstance from "../api/todos";


type SetTodosAction = {
  type: ActionTypes.SET_TODOS;
  payload: { todos: TodoItem[] };
};

export const setTodos = (todos: TodoItem[]): SetTodosAction => ({
  type: ActionTypes.SET_TODOS,
  payload: { todos },
});

export const fetchTodos = (): ThunkAction<
  void,
  RootState,
  unknown,
  TodoActions
> => {
  return async (dispatch: Dispatch<TodoActions>) => {
    const response = await axios
      // `http://localhost:4000/todos` から `todos`だけですむようになった
+     .get(`todos`)
-     .get(`http://localhost:4000/todos`)
      .catch((error) => {
        throw new Error(error.message);
      });
    dispatch(setTodos(response.data));
  };
};

export type TodoActions = SetTodosAction;

API連携に関する専用クラスを作成する

API連携用のクラスを用意することで、API呼び出しをする際に毎回axios.get(...)のように記述する必要がなくなります。
たとえば、以下のようにするとAPI連携に関する専用クラスが実装できます。

src/api/todos.ts

class Todos {
  static async getAll() {
    const response = await axiosInstance.get(`http://localhost:4000/todos`).catch((error) => {
      throw new Error(error.message);
    });
    return response.data;
  }
}

export default Todos;

先ほど作成したaxiosインスタンスを利用するとさらにシンプルになります。

src/api/todos.ts

+ import axiosInstance from "./index";

class Todos {
  static async getAll() {
    // `http://localhost:4000/todos` から `todos`だけですむようになった
+   const response = await axiosInstance.get(`todos`).catch((error) => {
-   const response = await axiosInstance.get(`http://localhost:4000/todos`).catch((error) => {
      throw new Error(error.message);
    });
    return response.data;
  }
}

export default Todos;

API連携用の専用クラスを利用した場合、APIの呼び出し元(Actionのソースコード)は以下のようにリファクタリングできます。

import { ActionTypes } from "./actionTypes";
import { TodoItem, RootState } from "./types";
import { Dispatch } from "redux";
import axios from "axios";
import { ThunkAction } from "redux-thunk";
+ import axiosInstance from "../api/todos";


type SetTodosAction = {
  type: ActionTypes.SET_TODOS;
  payload: { todos: TodoItem[] };
};

export const setTodos = (todos: TodoItem[]): SetTodosAction => ({
  type: ActionTypes.SET_TODOS,
  payload: { todos },
});

export const fetchTodos = (): ThunkAction<
  void,
  RootState,
  unknown,
  TodoActions
> => {
  return async (dispatch: Dispatch<TodoActions>) => {
+    const todos = await TodosApiService.getAll();
+    dispatch(setTodos(todos));
-    const response = await axios
-     .get(`http://localhost:4000/todos`)
-     .catch((error) => {
-       throw new Error(error.message);
-     });
-    dispatch(setTodos(response.data));
  };
};

export type TodoActions = SetTodosAction;

さいごに

Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!