Now in Preview

Thin Layer.
Big Clarity.

Web標準の fetch をそのままに、型安全で快適な通信体験へ。apizel は “必要最低限” に絞った TypeScript 向け API クライアントです。

$ pnpm add @liha-labs/apizel
GitHub
main.ts
import { apizel, type ApizelConfig } from '@liha-labs/apizel'

const config: ApizelConfig = {
  baseURL: 'https://api.example.com',
  getAccessToken: () => localStorage.getItem('token'),
  refresh: async () => {
    // refresh endpoint を叩いて新しい token を返す
    return 'new-token'
  },
}

const api = apizel(config)

const me = await api.get<{ id: string; name: string }>('/v1/me')
/ 01

Introduction

Web標準のfetchを最大限に尊重しつつ、実務で不可欠な機能を補完する軽量HTTPクライアント。

What is apizel?

apizel(アピゼル)は、Web標準の fetch APIを最優先に考え、 Axiosのような巨大な抽象化を避けつつ、型安全で予測可能な開発体験を提供します。

Design goals

  • fetch-firstラッパーではなく、fetchを拡張する考え方
  • minimal依存ゼロ。バンドルサイズへの影響を最小限に
  • standardsURLSearchParams等の標準に準拠
  • TS-friendly徹底した型定義による開発体験の向上

Non-goals

  • 自動リトライ401 refresh以外は対象外
  • 型検証zod等のバリデーションは利用側で実装
  • ネストしたparams{ a: { b: 1 } } のような形式は非対応

Features

標準機能をコアに、開発者の「欲しい」を薄く実装。

JSON / FormDataContent-Typeを賢く自動判別。
timeoutAbortSignalをベースとした秒数指定。
repeat arrayクエリ配列を key=a&key=b へ展開。
auth refresh401時のトークン再取得フローを内蔵。
observe hooksonRequest / onResponse の観測。
single-flight重複する更新リクエストを一本化。

Compatibility

Environment
Browser (Modern)Node.js v18+React Native
Integration
TanStack QuerySWRSignal-based libs
/ 02

Quick Start

最小限のセットアップで、型安全かつ堅牢な通信環境を構築します。

STEP 01

Install

標準として pnpm を推奨します。

$ pnpm add @liha-labs/apizel
STEP 02

Minimal Usage

クライアントを作成し、メソッドを呼ぶだけです。

client.ts
import { apizel } from '@liha-labs/apizel'

const api = apizel({
  baseURL: 'https://api.example.com'
})

// 即座にGET。レスポンスは自動でJSONパースされます
const data = await api.get('/status')
STEP 03

With Typing

ジェネリクスを使用して、DTOの型を適用します。

api.ts
interface User {
  id: string
  name: string
}

// 戻り値に型を指定して型安全な開発を
const user = await api.get<User>('/me')
console.log(user.name)
STEP 04

Error Handling

中断(Abort)とサーバーエラーを明確に分離してハンドリングします。

error.ts
import { HttpError } from '@liha-labs/apizel'

try {
  await api.get('/data', { timeoutMs: 3000 })
} catch (err) {
  if (err.name === 'AbortError') {
    // タイムアウトまたは手動の中断
    console.error('Request timed out')
  } else if (err instanceof HttpError) {
    // 4xx, 5xx ステータスエラー
    console.error('Server error:', err.status)
  }
}
💡 落とし穴:Timeout は例外的な挙動です

timeoutMs による中断は、サーバーが返したエラー(HttpError)ではなく、ブラウザ標準の AbortError をスローします。 「通信そのものが成立しなかった」のか「サーバーが拒否したのか」を型レベルで安全に区別するための設計です。

/ 03

Usage

実務の「困った」を解決する、逆引きリファレンス。

Creating a client

apizel() で共通設定を持つインスタンスを作成します。

client.ts
import { apizel } from '@liha-labs/apizel'

const api = apizel({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: {
    'Content-Type': 'application/json',
  },
  timeoutMs: 10000,
})

Extend client

共通設定を維持しつつ、サービスごとに baseURL などを差し替えた派生クライアントを作成できます。

extend.ts
const api = apizel(common)

const usersApi = api.extend({ baseURL: USERS_URL })
const billingApi = api.extend({ baseURL: BILLING_URL })

await usersApi.get('/me')
await billingApi.post('/invoices', body)

Request options

options.ts
await api.get('/users', {
  headers: { 'X-Project-ID': 'apizel' },
  params: { role: 'admin' },
  timeoutMs: 5000,
  signal: controller.signal // 外部からの手動中断
})

Body Handling

値の種類を判別し、適切な Content-Type を自動で設定します。

JSON
Object / Array
application/jsonJSON.stringify されます。
FormData
FormData
(Multipart boundary)ブラウザの自動設定に任せます。
Others
Blob / string ...
No transform値をそのまま body に渡します。

Query Params

params.ts
// 配列はデフォルトで 'key=a&key=b' 形式に展開されます
await api.get('/search', {
  params: {
    tags: ['typescript', 'fetch'], // -> ?tags=typescript&tags=fetch
    active: true,                  // -> ?active=true
    page: 1                        // -> ?page=1
  }
})

// ※ ネストしたObject( { a: { b: 1 } } )は非対応です。

Auth & Refresh

401エラー時に一度だけリフレッシュを試行する、実用的なフローを内蔵しています。

auth.ts
const api = apizel.create({
  auth: {
    getAccessToken: () => localStorage.getItem('token'),
    shouldAttachToken: (req) => !req.url.includes('/login'),

    // 複数のリクエストが同時に401になっても、実行は1回に絞られます(Single-flight)
    refresh: async () => {
      const { token } = await api.post('/refresh-token')
      localStorage.setItem('token', token)
    },
    onRefreshFailed: () => {
      window.location.href = '/login'
    }
  }
})

Hooks (Observe only)

リクエストの前後にログや計測を差し込めます。

hooks.ts
const api = apizel.create({
  observe: {
    onRequest: (req) => console.log(`Sending ${req.method} to ${req.url}`),
    onResponse: (res) => trackMetric(res.url, res.status)
  }
})

Errors

HttpError

ステータスコード 4xx, 5xx の場合に投げられます。レスポンスの中身を保持します。

AbortError

タイムアウト、または手動の中断時に投げられます。これはサーバーエラーではありません。

/ 04

API Reference

apizel の全 API インターフェースと詳細な振る舞い。

apizel(config)

ApiClient インスタンスを生成します。

apizel(config?: ApizelConfig): ApiClient

ApizelConfig

設定オブジェクト(フラットな構造です)。
authobserve は論理的なグルーピングであり、ネストではありません。

baseURL
string

リクエストの起点となるURL。未指定時は endpoint をそのまま使います。

headers
Record<string, string>

全リクエスト共通ヘッダー。リクエスト単位で上書き可能です(後勝ち)。

Auth
getAccessToken
() => string | null | Promise<string | null>

現在のアクセストークンを取得する関数。同期・非同期のどちらでも可能です。

shouldAttachToken
(req: RequestMeta) => boolean

特定のリクエストにトークンを付与するかどうかの判定。
未定義の場合は全リクエストに付与されます。

refresh
() => Promise<string>

401エラー時に実行されるトークン再取得関数。新しいトークンを返してください。
Single-flight: 並行リクエストがあっても1回だけ実行され、他は待機します。
Safety: リフレッシュ処理中の再帰的な401エラーは無視され、無限ループを防ぎます。

onRefreshFailed
() => void | Promise<void>

リフレッシュ処理が失敗(例外スロー)した場合のフック。ログアウト処理などに。

Observe
onRequest
onResponse
(ctx) => void | Promise<void>

観測用フック。ログ、計測、デバッグ出力などに使用(挙動の変更は不可)。

ApiClient

Promise<DTO> を返す薄い5つのメソッドを提供します。

.get<T>(endpoint, options?).delete<T>(endpoint, options?).post<T>(endpoint, body?, options?).put<T>(endpoint, body?, options?).patch<T>(endpoint, body?, options?).extend(overrides)
Body Handling:
  • FormData, URLSearchParams, Blob はそのまま送信
  • その他は JSON.stringify され、Content-Type: application/json が自動付与
extend(overrides): 現在の実効設定をベースに新しいクライアントを返します。 元のクライアントや設定は変更されません。

RequestOptions & Params

params

Query String として展開されます。
配列は repeat 形式(tag: ['a','b']?tag=a&tag=b
※ 軽量化のため Object のネストは非対応(意図的にエラーをスロー)。

timeoutMs
number

指定時間経過後にリクエストを Abort します。HttpError ではなく AbortError が投げられます。

Errors

HttpError

res.ok === false の場合にスロー。status, data, url 等を保持します。

AbortError

タイムアウトや signal.abort() による中断時にスロー。 apizel はこれを HttpError に変換しません

/ 05

Examples

apizel は “薄さ” がコンセプトです。ここでは TanStack Query や認証まわりの定番パターンを最小の形で示します。

With TanStack Query (GET)

signalparams

TanStack Query が渡す signal をそのまま apizel に流し、キャンセル可能な GET を書く最小形です。

useUsers.ts
const useUsers = (role: string) => {
  return useQuery({
    queryKey: ['users', role],
    queryFn: ({ signal }) =>
      api.get<User[]>('/users', {
        params: { role },
        signal
      })
  })
}

No adapters, no wrappers. Just pass signal.

With TanStack Query (Mutation)

jsonformdata

POST/PUT の基本。JSON送信と FormData 送信の “分岐” だけを押さえます。

useCreateUser.ts
// JSON: Content-Type は自動付与
const mutation = useMutation({
  mutationFn: (newUser: UserDTO) => api.post('/users', newUser)
})

// FormData: Content-Type はブラウザに任せる
const upload = useMutation({
  mutationFn: (formData: FormData) => api.post('/upload', formData)
})

Keep mutations explicit: api.post(path, body).

Auth Token

getAccessTokenshouldAttachToken

アクセストークンの付与を制御します。Authorization: Bearer ヘッダーは apizel が自動付与します。

auth.ts
const api = apizel({
  getAccessToken: async () => await storage.getToken(),
  shouldAttachToken: (req) => !req.endpoint.includes('/auth/login')
})

Token rules belong to the app, not the client.

Refresh on 401

refreshsingle-flightretry-once

401時に1度だけ refresh を試行。複数リクエストは single-flight で1つに合流します。

refresh.ts
const api = apizel({
  refresh: async () => {
    const { token } = await api.post('/auth/refresh')
    return token // 自動的に retry に使用される
  },
  onRefreshFailed: () => {
    logout() // リフレッシュ失敗時のハンドリング
  }
})

Retry happens once. If it still fails, HttpError is thrown.

apizel

apizel

TypeScript のための薄い fetch ラッパー。

標準の fetch に、ちょうどいい薄皮。

Produced byLiha LabsLiha Labs
© 2026 Liha Labs. Released under the MIT License.