はじめに
HTTPリクエストを送るだけなのに、なんでこんなにコードが長くなるんだろう。fetchを使っていて、そう思ったことはないですか。
GitHubで108,000スター、毎月3億回以上のnpmダウンロード。Axiosは11年間ずっと第一線で使われ続けているHTTPクライアントライブラリです。
正直なところ、最初は「fetchで十分じゃない?」と思っていたんですよ。ES6以降、標準APIも充実してきたし。でも実際にAxiosを使い始めたら、エラーハンドリングやレスポンスの変換で書いていた定型コードがごっそり消えた。30代になって思うのは、「標準でできるから」という理由で効率の悪い方法を続けるのは、時間の無駄だということ。
Axiosとは
Axiosはブラウザとnode.js両方で動作するPromiseベースのHTTPクライアントです。
ブラウザではXMLHttpRequest、Node.jsではHTTPモジュールを使い分けて、同じインターフェースで統一的にHTTPリクエストを扱えます。
現在の最新バージョンはv1.13.2で、MITライセンスのオープンソースとして公開されています。523人以上のコントリビューターが開発に参加しており、最終コミットは2日前。活発にメンテナンスされていますね。
特徴・メリット
1. fetchより直感的なAPI
これ、意外と大事なんですけど、Axiosはレスポンスを自動的にJSONにパースしてくれます。
// fetchの場合
const response = await fetch('/api/users')
const data = await response.json()
// Axiosの場合
const { data } = await axios.get('/api/users')
たった1行の違いだけど、何十回も書くと差が出てきます。時短になる。
2. エラーハンドリングが楽
fetchだとHTTPエラー(404とか500とか)でもPromiseがrejectされないんですよね。毎回response.okをチェックする必要がある。
// fetchの場合
const response = await fetch('/api/users')
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
// Axiosの場合
// 自動的にエラーをthrowしてくれる
Axiosは2xx以外のステータスコードを自動的にエラーとして扱ってくれます。これだけでコード量がかなり減る。
3. インターセプターが便利
リクエストやレスポンスに共通の処理を挟めます。認証トークンの付与とか、エラーログの記録とか。
axios.interceptors.request.use(config => {
config.headers.Authorization = `Bearer ${getToken()}`
return config
})
個人的には、この機能が一番のメリットだと思います。
4. リクエストのキャンセルができる
ページ遷移時に不要なリクエストをキャンセルしたいケース、結構あるんですよ。
const controller = new AbortController()
axios.get('/api/users', { signal: controller.signal })
controller.abort() // リクエストをキャンセル
5. ブラウザとNode.js両対応
同じコードがブラウザでもサーバーサイドでも動く。SSRやテストコードを書くときに地味に助かる。
6. XSRF対策が組み込み
クライアントサイドでのクロスサイトリクエストフォージェリ対策が標準で入っています。セキュリティ面でも安心。
インストール方法
パッケージマネージャー
# npm
npm install axios
# yarn
yarn add axios
# pnpm
pnpm add axios
# bun
bun add axios
CDN
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
バンドルサイズは約26.2kB(minified + gzipped)。fetchがゼロコストなのと比べると多少大きいですが、得られる開発体験を考えれば十分許容範囲ですね。
TypeScriptサポート
型定義は内蔵されているので、@types/axiosみたいな追加パッケージは不要です。
基本的な使い方
GETリクエスト
import axios from 'axios'
// シンプルなGET
const response = await axios.get('/api/users')
console.log(response.data)
// クエリパラメータ付き
const response = await axios.get('/api/users', {
params: {
page: 1,
limit: 10
}
})
URLの組み立てを自分でやらなくていい。paramsオブジェクトを渡せば自動的にクエリ文字列にしてくれます。
POSTリクエスト
const response = await axios.post('/api/users', {
name: '山田太郎',
email: 'yamada@example.com'
})
オブジェクトを渡せば自動的にJSON.stringifyしてくれます。Content-Typeヘッダーも自動設定。
複数のリクエストを同時に
const [users, posts] = await Promise.all([
axios.get('/api/users'),
axios.get('/api/posts')
])
エラーハンドリング
try {
const response = await axios.get('/api/users')
} catch (error) {
if (axios.isAxiosError(error)) {
console.error('Status:', error.response?.status)
console.error('Data:', error.response?.data)
} else {
console.error('Unexpected error:', error)
}
}
axios.isAxiosError()でAxios由来のエラーか判定できる。TypeScriptでの型の絞り込みにも使えます。
実践的なユースケース
APIクライアントの作成
import axios, { AxiosInstance, AxiosError } from 'axios'
interface User {
id: number
name: string
email: string
}
interface ApiError {
message: string
code: string
}
const apiClient: AxiosInstance = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// リクエストインターセプター
apiClient.interceptors.request.use(config => {
const token = localStorage.getItem('accessToken')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// レスポンスインターセプター
apiClient.interceptors.response.use(
response => response,
async (error: AxiosError<ApiError>) => {
if (error.response?.status === 401) {
// トークンリフレッシュの処理
localStorage.removeItem('accessToken')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
// APIメソッド
export const userApi = {
getAll: () => apiClient.get<User[]>('/users'),
getById: (id: number) => apiClient.get<User>(`/users/${id}`),
create: (data: Omit<User, 'id'>) => apiClient.post<User>('/users', data),
update: (id: number, data: Partial<User>) => apiClient.put<User>(`/users/${id}`, data),
delete: (id: number) => apiClient.delete(`/users/${id}`)
}
axios.create()でインスタンスを作れば、baseURLやヘッダーを共通化できます。これ、APIクライアントを作るときの定番パターンですね。
リトライ処理
async function fetchWithRetry<T>(
url: string,
maxRetries: number = 3
): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
const { data } = await axios.get<T>(url)
return data
} catch (error) {
if (i === maxRetries - 1) throw error
// 指数バックオフ
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)))
}
}
throw new Error('Max retries exceeded')
}
フォームデータの送信
const formData = new FormData()
formData.append('file', file)
formData.append('description', 'プロフィール画像')
const response = await axios.post('/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total ?? 1)
)
console.log(`Upload: ${percentCompleted}%`)
}
})
onUploadProgressでアップロードの進捗も取れます。ユーザーにプログレスバーを表示したいときに便利。
タイムアウトとキャンセル
const controller = new AbortController()
// 5秒後に自動キャンセル
const timeoutId = setTimeout(() => controller.abort(), 5000)
try {
const response = await axios.get('/api/slow-endpoint', {
signal: controller.signal
})
clearTimeout(timeoutId)
return response.data
} catch (error) {
if (axios.isCancel(error)) {
console.log('Request was cancelled')
}
throw error
}
fetchとの使い分け
正直なところ、どちらを使うかはプロジェクトの規模と要件次第です。
fetchが向いているケース:
- バンドルサイズを極限まで小さくしたい
- シンプルなリクエストだけ
- 依存関係を増やしたくない
Axiosが向いているケース:
- インターセプターで共通処理を入れたい
- エラーハンドリングを簡潔にしたい
- Node.jsとブラウザで同じコードを使いたい
- チームで統一的なAPIクライアントを作りたい
個人的には、ある程度の規模のプロジェクトならAxios一択ですね。
まとめ
Axiosを導入して感じた変化:
- コード量: fetchで書いていた定型処理がごっそり消えた
- エラーハンドリング: response.okのチェックから解放された
- 拡張性: インターセプターで認証やログを一元管理できるように
- 型安全性: TypeScriptとの相性が良く、レスポンスの型付けが楽
- 保守性: チームで統一されたAPIクライアントを作れた
11年間、毎月3億回ダウンロードされ続けているのには理由があります。「枯れた技術」と言えばそれまでですが、安定して動くというのは実務では何より大事。
まだfetchで頑張っている人は、一度試してみてください。HTTP通信周りのストレスがかなり減りますよ。