モダンなフロントエンド開発において、AxiosなどのHTTPクライアントをそのまま各コンポーネントで使用することは、メンテナンス性の低下を招くリスクがあります。本記事では、堅牢なアプリケーションを構築するために、Axiosをラップ(二次封入)して共通化する方法について解説します。
Axiosをラップすべき主な理由
- ライブラリへの依存度低減: ライブラリに重大なバグが発生したり、メンテナンスが停止したりした場合でも、ラップしたファイルのみを修正すれば済むため、プロジェクト全体への影響を最小限に抑えられます。
- 共通処理の集約: リクエストヘッダーの設定、ローディング表示、認証トークンの付与、共通のエラーハンドリングなどを一箇所で管理できます。
- ビジネスロジックの分離: ネットワークエラーや特定のステータスコード(401未認証など)に対する挙動を隠蔽し、呼び出し側は純粋なデータのやり取りに集中できます。
- データ整合性の確保: サーバーからのレスポンスが正常(200系)であっても、業務上のエラーコードが含まれる場合に例外として処理するロジックを共通化できます。
エラーハンドリングとステータスメッセージの定義
まず、HTTPステータスコードに応じたユーザー向けのメッセージを定義します。これにより、エラー発生時のフィードバックを統一できます。
const statusCodeMap = {
200: 'リクエストが成功しました。',
201: 'データの作成/更新に成功しました。',
202: 'リクエストは受理されましたが、処理は完了していません。',
204: 'データの削除に成功しました。',
400: 'リクエストが不正です。サーバーは処理を行いませんでした。',
401: '認証に失敗しました。再ログインが必要です。',
403: 'アクセス権限がありません。',
404: 'リクエストされたリソースが見つかりません。',
422: 'バリデーションエラーが発生しました。',
500: 'サーバー内部でエラーが発生しました。',
502: 'ゲートウェイエラーが発生しました。',
503: 'サービスが一時的に利用できません。',
504: 'ゲートウェイタイムアウト。'
};
const handleApiError = (error) => {
if (error.response) {
const { status, config } = error.response;
const message = statusCodeMap[status] || error.response.statusText;
console.error(`[API Error] ${status}: ${config.url} - ${message}`);
} else {
// ネットワーク切断やサーバーダウンなど
console.error('ネットワーク異常が発生したか、サーバーに接続できません。');
}
};
Axiosインスタンスの構築とインターセプターの実装
axios.createを使用してインスタンスを生成し、リクエストとレスポンスのインターセプターを設定します。
import axios from 'axios';
const apiService = axios.create({
timeout: 15000,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
});
// リクエストインターセプター
apiService.interceptors.request.use(
(config) => {
// 送信前にトークンをヘッダーに注入するなどの処理
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// レスポンスインターセプター
apiService.interceptors.response.use(
(response) => {
// 正常系のレスポンス処理
return response.data;
},
(error) => {
// エラーハンドリングの実行
handleApiError(error);
return Promise.reject(error);
}
);
/**
* GETリクエスト
*/
export const httpRequestGet = async (url, params = {}) => {
try {
return await apiService.get(url, { params });
} catch (error) {
throw error;
}
};
/**
* POSTリクエスト
*/
export const httpRequestPost = async (url, data = {}) => {
try {
return await apiService.post(url, data);
} catch (error) {
throw error;
}
};
レスポンスデータの安全性とオプショナルチェイニング
APIから返されるデータには、特定のフィールドが null や undefined になるケースが多々あります。これによるフロントエンドでのクラッシュを防ぐために、デフォルト値の設定やオプショナルチェイニング(Optional Chaining)の活用が推奨されます。
// データのデフォルト値設定の例
const userList = response.data || [];
// オプショナルチェイニングによる安全なアクセス
const userName = response.user?.profile?.name ?? '名無し';
フィールドマッピングによるデカップリングの検討
バックエンドのAPI設計(スネークケースなど)とフロントエンドの命名規則(キャメルケースなど)が異なる場合や、バックエンドのキー名が頻繁に変更される可能性がある場合、レスポンスを受け取った直後に「フィールドマッピング」を行うレイヤーを挟むことが有効です。
メリット:
- UIコンポーネントがバックエンドの物理名に依存しなくなる。
- APIのキー名が変更されても、マッピング定義を修正するだけでUI側は変更不要。
- 不要なデータをフィルタリングし、必要な型に変換できる。
// マッピング関数の例
const mapUserResponse = (data) => ({
id: data.user_id,
name: data.user_display_name || 'ゲスト',
email: data.contact_email
});
// 使用例
httpRequestGet('/api/v1/user/1').then(res => {
const user = mapUserResponse(res);
// UI側では user.name を使用する
});
このようにAxiosを適切にラップすることで、変更に強く、エラーに堅牢なアプリケーション基盤を構築することができます。