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
標準機能をコアに、開発者の「欲しい」を薄く実装。
Compatibility
Quick Start
最小限のセットアップで、型安全かつ堅牢な通信環境を構築します。
Install
標準として pnpm を推奨します。
$ pnpm add @liha-labs/apizelMinimal Usage
クライアントを作成し、メソッドを呼ぶだけです。
import { apizel } from '@liha-labs/apizel'
const api = apizel({
baseURL: 'https://api.example.com'
})
// 即座にGET。レスポンスは自動でJSONパースされます
const data = await api.get('/status')With Typing
ジェネリクスを使用して、DTOの型を適用します。
interface User {
id: string
name: string
}
// 戻り値に型を指定して型安全な開発を
const user = await api.get<User>('/me')
console.log(user.name)Error Handling
中断(Abort)とサーバーエラーを明確に分離してハンドリングします。
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 をスローします。 「通信そのものが成立しなかった」のか「サーバーが拒否したのか」を型レベルで安全に区別するための設計です。
Usage
実務の「困った」を解決する、逆引きリファレンス。
Creating a client
apizel() で共通設定を持つインスタンスを作成します。
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 などを差し替えた派生クライアントを作成できます。
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
await api.get('/users', {
headers: { 'X-Project-ID': 'apizel' },
params: { role: 'admin' },
timeoutMs: 5000,
signal: controller.signal // 外部からの手動中断
})Body Handling
値の種類を判別し、適切な Content-Type を自動で設定します。
Object / ArrayFormDataBlob / string ...Query Params
// 配列はデフォルトで '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エラー時に一度だけリフレッシュを試行する、実用的なフローを内蔵しています。
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)
リクエストの前後にログや計測を差し込めます。
const api = apizel.create({
observe: {
onRequest: (req) => console.log(`Sending ${req.method} to ${req.url}`),
onResponse: (res) => trackMetric(res.url, res.status)
}
})Errors
ステータスコード 4xx, 5xx の場合に投げられます。レスポンスの中身を保持します。
タイムアウト、または手動の中断時に投げられます。これはサーバーエラーではありません。
API Reference
apizel の全 API インターフェースと詳細な振る舞い。
apizel(config)
ApiClient インスタンスを生成します。
apizel(config?: ApizelConfig): ApiClientApizelConfig
設定オブジェクト(フラットな構造です)。auth や observe は論理的なグルーピングであり、ネストではありません。
stringリクエストの起点となるURL。未指定時は endpoint をそのまま使います。
Record<string, string>全リクエスト共通ヘッダー。リクエスト単位で上書き可能です(後勝ち)。
() => string | null | Promise<string | null>現在のアクセストークンを取得する関数。同期・非同期のどちらでも可能です。
(req: RequestMeta) => boolean特定のリクエストにトークンを付与するかどうかの判定。
未定義の場合は全リクエストに付与されます。
() => Promise<string>401エラー時に実行されるトークン再取得関数。新しいトークンを返してください。
Single-flight: 並行リクエストがあっても1回だけ実行され、他は待機します。
Safety: リフレッシュ処理中の再帰的な401エラーは無視され、無限ループを防ぎます。
() => void | Promise<void>リフレッシュ処理が失敗(例外スロー)した場合のフック。ログアウト処理などに。
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)FormData,URLSearchParams,Blobはそのまま送信- その他は
JSON.stringifyされ、Content-Type: application/json が自動付与
RequestOptions & Params
Query String として展開されます。
配列は repeat 形式(tag: ['a','b'] → ?tag=a&tag=b)
※ 軽量化のため Object のネストは非対応(意図的にエラーをスロー)。
number指定時間経過後にリクエストを Abort します。HttpError ではなく AbortError が投げられます。
Errors
res.ok === false の場合にスロー。status, data, url 等を保持します。
タイムアウトや signal.abort() による中断時にスロー。 apizel はこれを HttpError に変換しません。
Examples
apizel は “薄さ” がコンセプトです。ここでは TanStack Query や認証まわりの定番パターンを最小の形で示します。
With TanStack Query (GET)
TanStack Query が渡す signal をそのまま apizel に流し、キャンセル可能な GET を書く最小形です。
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)
POST/PUT の基本。JSON送信と FormData 送信の “分岐” だけを押さえます。
// 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
アクセストークンの付与を制御します。Authorization: Bearer ヘッダーは apizel が自動付与します。
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
401時に1度だけ refresh を試行。複数リクエストは single-flight で1つに合流します。
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.