【JavaScript】Fetch APIのラッパーライブラリ「upfetch」の導入方法

はじめに
こんにちは、株式会社TOKOSの鳥井です!
普段の開発でHTTPリクエストを書くとき、Fetch APIに不満を感じたことはありませんか?
JSON.stringifyやContent-Typeの設定を毎回書くのは面倒ですし、レスポンスの型安全性も自分で担保する必要があります。
本記事では、そんな課題を解決してくれるfetchラッパーライブラリ「upfetch」の導入方法を紹介します💪
対象読者
- Fetch APIやfetchのライブラリでHTTPリクエストを書いている方
- fetchのライブラリを検討している方
- HTTPリクエストのコードに煩雑さを感じている方
upfetchとは
upfetchは、ネイティブのFetch APIをラップして使いやすくするライブラリです!
主な特長は以下の通りです。
- 軽量: 1.6kB(gzip)、他npmパッケージへの依存なし
- 型安全: Zodなどのスキーマライブラリと連携してレスポンスをパースできる
- 自動シリアライズ: クエリパラメータやリクエストボディをオブジェクトで渡せる
- その他の機能: タイムアウト、リトライ、ライフサイクルフックを標準搭載
Fetch APIの使い勝手はそのままに、実務で必要な機能を追加できるのがupfetchの魅力です。
また、使用可能な実行環境はブラウザだけでなく、ブラウザ外でJavaScriptを実行するランタイムや、CDNのエッジサーバー上でコードを実行するエッジ環境でも動作します。
それぞれの環境における具体例を以下に示します。
- ブラウザ: Chrome、Firefox、Safari、Edge
- ランタイム: Node.js 18以上、Bun、Deno
- エッジ環境: Cloudflare Workers、Vercel Edge Runtime
GitHub - L-Blondy/up-fetch: Advanced fetch client builder
Advanced fetch client builder. Contribute to L-Blondy/up-fetch development by creating an account on GitHub.
github.com他ライブラリとの比較
fetchラッパーとしてはaxiosやkyも有名です。
upfetchとの違いを表で整理しました。
| 項目 | upfetch | axios | ky |
|---|---|---|---|
| サイズ(gzip) | 1.6kB | 13kB | 3.5kB |
| 外部への依存 | なし | あり | なし |
| スキーマバリデーション | ◯ | ✕ | ✕ |
| リトライ | ◯ | ✕(プラグインが必要) | ◯ |
| タイムアウト | ◯ | ◯ | ◯ |
upfetchはfetchネイティブでありながら、スキーマバリデーションを内蔵している点が大きな差別化ポイントです!
axiosはデフォルトでXMLHttpRequestを使用しており、サーバーサイドとの互換性やバンドルサイズの面で不利になる場合があります(fetchアダプターも選択可能です)。
kyも優秀なライブラリですが、スキーマバリデーションは自分で組み合わせる必要があります。

2026年4月にはaxiosでCritical(CVSS10.0)の脆弱性(GHSA-fvcv-3m26-pcqx)が公表されたこともあり、この機会にaxiosからupfetchへの移行を考えてみるのも良いかもしれませんね!
セットアップ
インストール
# npm
npm i up-fetch
# yarn
yarn add up-fetch
# pnpm
pnpm add up-fetch
# bun
bun add up-fetch
# deno
deno add up-fetchfetchクライアントの設定
up関数にfetchを渡してfetchクライアントを作成します。
第2引数のコールバックでデフォルトのオプションを設定できます。
import { up } from "up-fetch"
export const upfetch = up(fetch, () => ({
baseUrl: "https://jsonplaceholder.typicode.com",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
}))すべてのリクエストに共通の設定が適用されるfetchクライアントの完成です。
基本的なリクエスト
upfetchはレスポンスを自動でパース(JavaScriptのデータに変換)してくれます。
Content-Typeがapplication/jsonならJSONとして、text/*ならテキストとして自動判定されるため、.json()を呼ぶ必要がありません。
各HTTPメソッドにおける使い方を見ていきましょう!
GET
const response = await fetch("https://jsonplaceholder.typicode.com/users")
const users = await response.json()import { upfetch } from "@/libs/api/upfetch"
const users = await upfetch("/users")response.json()の呼び出しが不要になり、たった1行でデータを取得できます!
さらに、クエリパラメータはparamsオプションにオブジェクトを渡すだけで自動的にシリアライズされます!
fetch("https://jsonplaceholder.typicode.com/users?_page=1&_limit=5")import { upfetch } from "@/libs/api/upfetch"
upfetch("/users", {
params: { _page: 1, _limit: 5 },
})URLの組み立てを意識する必要がなくなり、コードの見通しがよくなりますね✨
POST
await fetch("https://jsonplaceholder.typicode.com/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "田中太郎", email: "tanaka@example.com" }),
})import { upfetch } from "@/libs/api/upfetch"
await upfetch("/users", {
method: "POST",
body: { name: "田中太郎", email: "tanaka@example.com" },
})オブジェクトをbodyに渡すだけで、自動的にJSON.stringifyされます。
Content-Typeヘッダーの指定が不要なのも嬉しいです!
PUT / PATCH
await fetch("https://jsonplaceholder.typicode.com/users/1", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "田中次郎" }),
})import { upfetch } from "@/libs/api/upfetch"
await upfetch("/users/1", {
method: "PUT",
body: { name: "田中次郎" },
})PATCHも同様の書き方でOKです👌
import { upfetch } from "@/libs/api/upfetch"
await upfetch("/users/1", {
method: "PATCH",
body: { email: "jiro@example.com" },
})DELETE
await fetch("https://jsonplaceholder.typicode.com/users/1", {
method: "DELETE",
})import { upfetch } from "@/libs/api/upfetch"
await upfetch("/users/1", {
method: "DELETE",
})DELETEの場合はbodyの差が出ませんが、baseUrlを共通化できる分、URLの記述が簡潔になります。

Fetch APIと比べて、ボイラープレートがかなり減りますね。特にPOSTリクエストの簡潔さは感動ものです✨
スキーマバリデーション
upfetchではschemaオプションにスキーマを渡すことでレスポンスの型をバリデーションできます!
ここではZodを使った例を紹介します。
import { upfetch } from "@/libs/api/upfetch"
import { z } from "zod"
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.email(),
})
const user = await upfetch("/users/1", {
schema: userSchema,
})
// user は { id: number; name: string; email: string } 型として推論されるレスポンスのJSONがスキーマの定義を満たしていない場合はResponseValidationErrorがスローされます。
import { upfetch } from "@/libs/api/upfetch"
import { isResponseValidationError } from "up-fetch"
try {
const user = await upfetch("/users/1", { schema: userSchema })
} catch (error) {
if (isResponseValidationError(error)) {
console.error(error.issues)
}
}APIの型安全性をランタイムで保証できるのは、upfetchならではの強みです。
Intro | Zod
Introduction to Zod - TypeScript-first schema validation library with static type inference
zod.devライフサイクルフック
upfetchには複数のライフサイクルフックがあります。ここでは代表的なonRequest、onSuccess、onErrorを紹介します。
リクエストの前後に共通処理を挟みたい場合に便利です!
import { up, isResponseError } from "up-fetch"
const upfetch = up(fetch, () => ({
baseUrl: "https://jsonplaceholder.typicode.com",
onRequest: (options) => {
console.log("リクエスト開始:", options.method, options.url)
},
onSuccess: (data, options) => {
console.log("リクエスト成功:", options.url)
},
onError: (error) => {
if (isResponseError(error) && [400, 401, 422].includes(error.status)) {
const data = error.data as { message?: string }
console.error("APIエラー:", data.message)
return
}
console.error("予期しないエラーが発生しました")
},
}))上記の例では、onErrorでステータスコードに応じてエラーメッセージを出し分けています。
400系のエラーはAPIからのメッセージを表示し、それ以外は汎用的なエラーメッセージを表示する、というパターンは実務でもよく使います。
タイムアウト
Fetch APIにはタイムアウト機能がなく、AbortControllerを使って自前で実装する必要がありました。
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 3000)
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users", {
signal: controller.signal,
})
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`)
}
const users = await response.json()
} catch (error) {
if (error.name === "AbortError") {
console.error("リクエストがタイムアウトしました")
} else {
throw error
}
} finally {
clearTimeout(timeoutId)
}AbortControllerとsetTimeoutを組み合わせ、指定時間後にリクエストを中断する必要があります。
upfetchではtimeoutオプションを指定するだけです!
import { upfetch } from "@/libs/api/upfetch"
await upfetch("/users", {
timeout: 3000,
})fetchクライアント作成時にデフォルト値として設定も可能です。
import { up } from "up-fetch"
const upfetch = up(fetch, () => ({
baseUrl: "https://jsonplaceholder.typicode.com",
timeout: 5000,
}))タイムアウトすると、リクエストは中断され、例外が発生します!
AbortControllerで手動中断した場合の"AbortError"とは異なり、upfetchのタイムアウトでは"TimeoutError"がスローされます!
リトライ
ネットワークの一時的な障害に備えて、自動リトライを設定できます。
import { up } from "up-fetch"
const upfetch = up(fetch, () => ({
baseUrl: "https://jsonplaceholder.typicode.com",
retry: {
attempts: 3, // 最大リトライ回数
delay: 1000, // リトライ間隔(ミリ秒)
},
}))リトライのたびに待機時間を増やす設定も可能です!
以下の例ではリトライ回数の2乗で待機時間を増やす実装となっています!
import { up } from "up-fetch"
const upfetch = up(fetch, () => ({
baseUrl: "https://jsonplaceholder.typicode.com",
retry: {
attempts: 3,
// 1回目: 1秒後、2回目: 4秒後、3回目: 9秒後にリトライ
delay: (ctx) => ctx.attempt ** 2 * 1000,
},
}))
設定1つでリトライが済むのは嬉しいポイントです。AbortControllerを書かなくていいのも最高ですね😎
エラーハンドリング
Fetch APIはHTTPエラーが返ってきても例外をスローしません。
そのため、response.okを毎回チェックするコードを書く必要があります。
upfetchでは、response.okがfalseの場合に自動でResponseErrorがスローされるため、このチェックが不要になります!
isResponseErrorとisResponseValidationErrorを使って、エラーの種類に応じた処理を書けます。
import { upfetch } from "@/libs/api/upfetch"
import { isResponseError, isResponseValidationError } from "up-fetch"
try {
const user = await upfetch("/users/1", { schema: userSchema })
} catch (error) {
if (isResponseError(error)) {
// APIがエラーレスポンスを返した場合(4xx, 5xx)
console.error("ステータス:", error.status)
console.error("レスポンス:", error.data)
}
if (isResponseValidationError(error)) {
// スキーマバリデーションに失敗した場合
console.error("バリデーションエラー:", error.issues)
}
}実務では、ステータスコードに応じてUIの通知を出し分けるパターンが多いです。
import { up, isResponseError } from "up-fetch"
// showNotificationはトースト通知を表示するユーティリティ関数を想定
const handleError = (error: unknown) => {
if (!(isResponseError(error) && [400, 401, 422].includes(error.status))) {
showNotification("エラーが発生しました。担当者にお問い合わせください。", "error")
return
}
const data = error.data as { message?: string }
showNotification(data.message ?? "エラー", "error")
}
const upfetch = up(fetch, () => ({
baseUrl: "https://jsonplaceholder.typicode.com",
onError: handleError,
}))認証
HTTPリクエストで認証する方法として、代表的なCookie認証とBearer認証の例を紹介します。
Cookie認証
Cookie認証を使う場合は、credentials: "include"をデフォルトオプションに設定します。
import { up } from "up-fetch"
export const upfetch = up(fetch, () => ({
baseUrl: "https://api.example.com",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
credentials: "include",
}))credentials: "include"を設定することで、Cookieが自動的に送信されます。
headersと同様に、fetchクライアント作成時に1度設定すれば、個別のリクエストごとに指定する必要はありません。
Bearer認証
Bearer tokenを使う場合は、headersのAuthorizationにトークンを設定します。
upの第2引数は関数のため、リクエストのたびに呼び出され、最新のトークンを取得できます。
import { up } from "up-fetch"
export const upfetch = up(fetch, () => ({
baseUrl: "https://api.example.com",
headers: {
// localStorageはブラウザ専用のAPI
Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
},
}))なお、upfetch自体はサーバー環境でも動作しますが、localStorageはブラウザ専用のAPIです!
サーバーサイドでは別のトークン管理方法を用意する必要があることに注意してください!
トークンを非同期で取得する場合(SDK経由など)は、関数をasyncにすることで対応できます。
import { up } from "up-fetch"
const upfetch = up(fetch, async () => ({
baseUrl: "https://api.example.com",
headers: {
Authorization: `Bearer ${await getAccessToken()}`,
},
}))おわりに
本記事では、fetchラッパーライブラリ「upfetch」の導入方法を紹介しました!
今回は主要な機能と基本的な使い方を取り上げましたが、その他にもストリーミングやカスタムシリアライザーなどの高度な機能もあります。
ぜひプロジェクトへの導入を検討してみてください✨
