AxiosでAPIレスポンスをcamelCaseに変換するお話

# はじめに

この記事は[呉高専 Advent Calendar 2019 9日目](https://qiita.com/advent-calendar/2019/kurekosen)の記事になります。

こんにちは、呉高専在学中のフロントエンドエンジニアの@kobakazu0429です。

12月も始まって1週間が経とうとしているのに、このアドベンドカレンダーはまだ1つも投稿されていなくて寂しいですね。。。
（そもそも、今のところ4記事分しか予約されていない！）

普段はReactを書いているのですが、バックエンドや、外部サービスからデータを取ってくる際には100%と言っていいほど、 [GitHub - axios/axios](https://github.com/axios/axios) を利用しています。
axios便利でいいですよね！
ですが、jsonのkeyは基本snake_caseだと思うので、camelCaseのjsではいまいち、と感じることもしばしば。

「各レスポンスを手作業で変換なんかしたくない！！」そんなあなたに、今回の記事がおすすめです。

Demo: https://codesandbox.io/s/funny-payne-4q345

# axios#interceptorsとは

axiosには[interceptors](https://github.com/axios/axios#interceptors)という、リクエスト/レスポンスを **intercept(傍受する)** メソッドが生えています。
簡単にいうと、リクエスト/レスポンスの前後に処理を挟むことができます。
こちらに関しては、[axiosを乗りこなす機能についての知見集#3-1-interceptorsで処理差し込み
](https://qiita.com/terrierscript/items/ccb56b6fc05aa7821c42#3-1-interceptors%E3%81%A7%E5%87%A6%E7%90%86%E5%B7%AE%E3%81%97%E8%BE%BC%E3%81%BF)という記事が、非常にわかりやすくまとまっているので、より詳しく知りたい方は1度目を通しておくと良いでしょう。

# snake_case → camelCase変換

こちらはライブラリ選定中に、lodashのissueを見つけてしまったのでそのまま流用させていただこうと思います。

> 一部わかりやすように変数名を変更しています。

> ```js
const mapKeysDeep = (data, callback) => {
  if (isArray(data)) {
    return data.map(innerData => mapKeysDeep(innerData, callback));
  } else if (isObject(data)) {
    return mapValues(mapKeys(data, callback), val => mapKeysDeep(val, callback))
  } else {
    return data;
  }
}
```
```js
const mapKeysCamelCase = data => mapKeysDeep(data, (_value, key) => camelCase(key));
```

>[GitHub - lodash/lodash issue: Feature request - deep object walk through #1244](https://github.com/lodash/lodash/issues/1244#issuecomment-338139221)

# 実装

今回はデモとしてQiitaのAPIをお借りして、私(@kobakazu0429)のuserデータを取得してみます。
Demo: https://codesandbox.io/s/funny-payne-4q345

## ライブラリのインストール

```bash
$ yarn add axios
$ yarn add lodash.camelcase lodash.isarray lodash.isobject lodash.mapkeys lodash.mapvalues lodash.snakecase

# TypeScriptを使っている場合はこちらの型もご一緒に
$ yarn add -D @types/lodash.camelcase @types/lodash.isarray @types/lodash.isobject @types/lodash.mapkeys @types/lodash.mapvalues @types/lodash.snakecase
```

## コード

```js
const axios = require("axios");
const isArray = require("lodash.isarray");
const isObject = require("lodash.isobject");
const camelCase = require("lodash.camelcase");
const mapValues = require("lodash.mapvalues");
const mapKeys = require("lodash.mapkeys");

const mapKeysDeep = (data, callback) => {
  if (isArray(data)) {
    return data.map(innerData => mapKeysDeep(innerData, callback));
  } else if (isObject(data)) {
    return mapValues(mapKeys(data, callback), val =>
      mapKeysDeep(val, callback)
    );
  } else {
    return data;
  }
};

const mapKeysCamelCase = data =>
  mapKeysDeep(data, (_value, key) => camelCase(key));

const myAxios = axios.create({ baseURL: "https://qiita.com/api/v2/" });

// 一番重要なところ！
myAxios.interceptors.response.use(
  response => {
    const { data } = response;
    const convertedData = mapKeysCamelCase(data);
    return { ...response, data: convertedData };
  },
  error => {
    console.log(error);
    return Promise.reject(error);
  }
);

// 使い方
const res = await myAxios.get("users/kobakazu0429");
```

## 実際のレスポンス

```json
{
    "description": null,
    "facebook_id": null,
    "followees_count": 2,
    "followers_count": 0,
    "github_login_name": "kobakazu0429",
    "id": "kobakazu0429",
    "items_count": 4,
    "linkedin_id": null,
    "location": null,
    "name": "",
    "organization": null,
    "permanent_id": 197122,
    "profile_image_url": "https://avatars1.githubusercontent.com/u/31001505?v=4",
    "team_only": false,
    "twitter_screen_name": "kobakazu0429",
    "website_url": null
}
```

## 変換後のレスポンス
```json
{
    "description": null,
    "facebookId": null,
    "followeesCount": 2,
    "followersCount": 0,
    "githubLoginName": "kobakazu0429",
    "id": "kobakazu0429",
    "itemsCount": 4,
    "linkedinId": null,
    "location": null,
    "name": "",
    "organization": null,
    "permanentId": 197122,
    "profileImageUrl": "https://avatars1.githubusercontent.com/u/31001505?v=4",
    "teamOnly": false,
    "twitterScreenName": "kobakazu0429",
    "websiteUrl": null
}
```
このようにきちんとスネークケースがキャメルケースに変換されていますね！
また、今回はJSON中に配列や、オブジェクトなどが入れ子状に格納されていませんでしたが、`mapKeysCamelCase `は再帰的に動作するため、全keyに対して変換をかけてくれるので安心です。

# 最後に
今回の記事が誰かの助けになると嬉しいです。
ちなみに、再帰はAST(抽象構文木: Abstract Syntax Tree)の世界では **walk** と呼ばれているらしいです。