旧ブログ(ISSEN)から移行しました

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

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

鳥井京祐
鳥井京祐12分で読めます
はてなブックマーク

はじめに

こんにちは、株式会社TOKOSの鳥井です!
普段の開発でHTTPリクエストを書くとき、Fetch APIに不満を感じたことはありませんか?
JSON.stringifyContent-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との違いを表で整理しました。

項目upfetchaxiosky
サイズ(gzip)1.6kB13kB3.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-fetch

fetchクライアントの設定

up関数にfetchを渡してfetchクライアントを作成します。
第2引数のコールバックでデフォルトのオプションを設定できます。

src/libs/api/upfetch.ts
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-Typeapplication/jsonならJSONとして、text/*ならテキストとして自動判定されるため、.json()を呼ぶ必要がありません。
各HTTPメソッドにおける使い方を見ていきましょう!

GET

Fetch APIの場合
const response = await fetch("https://jsonplaceholder.typicode.com/users")
const users = await response.json()
upfetchの場合
import { upfetch } from "@/libs/api/upfetch"
 
const users = await upfetch("/users")

response.json()の呼び出しが不要になり、たった1行でデータを取得できます!
さらに、クエリパラメータはparamsオプションにオブジェクトを渡すだけで自動的にシリアライズされます!

Fetch APIの場合
fetch("https://jsonplaceholder.typicode.com/users?_page=1&_limit=5")
upfetchの場合
import { upfetch } from "@/libs/api/upfetch"
 
upfetch("/users", {
  params: { _page: 1, _limit: 5 },
})

URLの組み立てを意識する必要がなくなり、コードの見通しがよくなりますね✨

POST

Fetch APIの場合
await fetch("https://jsonplaceholder.typicode.com/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "田中太郎", email: "tanaka@example.com" }),
})
upfetchの場合
import { upfetch } from "@/libs/api/upfetch"
 
await upfetch("/users", {
  method: "POST",
  body: { name: "田中太郎", email: "tanaka@example.com" },
})

オブジェクトをbodyに渡すだけで、自動的にJSON.stringifyされます。
Content-Typeヘッダーの指定が不要なのも嬉しいです!

PUT / PATCH

Fetch APIの場合(PUT)
await fetch("https://jsonplaceholder.typicode.com/users/1", {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "田中次郎" }),
})
upfetchの場合(PUT)
import { upfetch } from "@/libs/api/upfetch"
 
await upfetch("/users/1", {
  method: "PUT",
  body: { name: "田中次郎" },
})

PATCHも同様の書き方でOKです👌

upfetchの場合(PATCH)
import { upfetch } from "@/libs/api/upfetch"
 
await upfetch("/users/1", {
  method: "PATCH",
  body: { email: "jiro@example.com" },
})

DELETE

Fetch APIの場合
await fetch("https://jsonplaceholder.typicode.com/users/1", {
  method: "DELETE",
})
upfetchの場合
import { upfetch } from "@/libs/api/upfetch"
 
await upfetch("/users/1", {
  method: "DELETE",
})

DELETEの場合はbodyの差が出ませんが、baseUrlを共通化できる分、URLの記述が簡潔になります。

鳥井
鳥井

Fetch APIと比べて、ボイラープレートがかなり減りますね。特にPOSTリクエストの簡潔さは感動ものです✨

スキーマバリデーション

upfetchではschemaオプションにスキーマを渡すことでレスポンスの型をバリデーションできます!
ここではZodを使った例を紹介します。

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には複数のライフサイクルフックがあります。ここでは代表的なonRequestonSuccessonErrorを紹介します。
リクエストの前後に共通処理を挟みたい場合に便利です!

フックの使用方法
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を使って自前で実装する必要がありました。

Fetch APIの場合
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)
}

AbortControllersetTimeoutを組み合わせ、指定時間後にリクエストを中断する必要があります。
upfetchではtimeoutオプションを指定するだけです!

upfetchの場合
import { upfetch } from "@/libs/api/upfetch"
 
await upfetch("/users", {
  timeout: 3000,
})

fetchクライアント作成時にデフォルト値として設定も可能です。

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.okfalseの場合に自動でResponseErrorがスローされるため、このチェックが不要になります!

isResponseErrorisResponseValidationErrorを使って、エラーの種類に応じた処理を書けます。

エラーの種類に応じた分岐
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"をデフォルトオプションに設定します。

Cookie認証の設定
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を使う場合は、headersAuthorizationにトークンを設定します。
upの第2引数は関数のため、リクエストのたびに呼び出され、最新のトークンを取得できます。

Bearer認証の設定
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」の導入方法を紹介しました!
今回は主要な機能と基本的な使い方を取り上げましたが、その他にもストリーミングやカスタムシリアライザーなどの高度な機能もあります。
ぜひプロジェクトへの導入を検討してみてください✨

この記事を書いた人

鳥井京祐
鳥井京祐

TOKOSのバックエンドエンジニアです。最近はフロントエンド、特にReactに興味があります!趣味は麻雀です!