日本語
レシピ
どのレシピも、生成されるクライアントがすでに提供している機能だけを使います。
想定外のステータスで例外を投げる
API の呼び出しでは、2xx 以外のレスポンスに対しても例外を投げずにレスポンスを返します。データ取得ライブラリーの中で使うときなど、例外を投げるほうが適しているときは、呼び出しをラップして想定するステータスを渡します。
ts
class HttpError extends Error {
response: Response;
data: unknown;
constructor(response: Response, data: unknown) {
super(`${response.status} ${response.statusText}`);
this.name = "HttpError";
this.response = response;
this.data = data;
}
}
type Result = {
status: number;
data: unknown;
response: Response;
};
/**
* ステータスが `status` のレスポンスのデータで解決し、
* それ以外のステータスでは HttpError を投げる。
*/
async function expectStatus<R extends Result, S extends R["status"]>(
call: Promise<R>,
status: S,
): Promise<Extract<R, { status: S }>["data"]> {
const res = await call;
if (res.status !== status) {
throw new HttpError(res.response, res.data);
}
return res.data as Extract<R, { status: S }>["data"];
}
const call = api.GET("/pets/{petId}", { path: { petId: 1 } });
// `pet` の型は 200 のレスポンスから決まる。
const pet = await expectStatus(call, 200);TanStack Query
フックを生成する必要はありません。queryFn の中で、TanStack Query が提供する signal を渡してクライアントを呼び出してください。そうすれば、クエリーがキャンセルされたときにリクエストも中止されます。
ts
import { useQuery } from "@tanstack/react-query";
function usePet(petId: number) {
return useQuery({
queryKey: ["pets", petId],
queryFn: ({ signal }) => {
const call = api.GET("/pets/{petId}", {
path: { petId },
signal,
});
return expectStatus(call, 200);
},
});
}環境ごとにベース URL を設定する
OpenAPI ドキュメントの servers が開発用のホストを指しているときなど、環境ごとに異なるサーバーを呼び出すには、ビルド時の環境変数から baseUrl を設定します。Vite では次のようにします。
ts
const api = createClient({ baseUrl: import.meta.env.VITE_API_BASE_URL });また、ブラウザーでは Vite の開発サーバーの server.proxy オプションと組み合わせるなどして、"/api" のような相対パスの baseUrl も使えます。
401 を受け取ったらトークンを更新する
ミドルウェアは next を複数回呼び出せます。401 のレスポンスを受け取った後に新しいトークンでリクエストを再試行するには、まだ送信していないリクエストのコピーを保持しておきます。
ts
declare function signIn(): Promise<string>;
let token = await signIn();
const bearer: Middleware = async (request, next) => {
// リクエスト本体は 1 回しか送信できないため、再試行用にコピーを保持しておく。
const retry = request.clone();
request.headers.set("authorization", `Bearer ${token}`);
const response = await next(request);
if (response.status !== 401) return response;
token = await signIn();
retry.headers.set("authorization", `Bearer ${token}`);
return next(retry);
};
const api = createClient({ middleware: [bearer] });テストでレスポンスをモックする
fetch オプションを使うと、クライアント全体でネットワーク通信を置き換えられます。
ts
const pet = { id: 1, name: "Tama", kind: "cat" };
const api = createClient({
baseUrl: "https://api.test",
fetch: async () => Response.json(pet),
});一方、一部の操作だけをモックし、それ以外のリクエストはネットワーク経由で送信するには、next を呼び出さずに Response を返すミドルウェアを使います。
ts
const mock: Middleware = async (request, next, { operation }) => {
if (operation.method === "GET" && operation.path === "/pets") {
return Response.json([{ id: 1, name: "Tama", kind: "cat" }]);
}
return next(request);
};
const mocked = api.with(mock);ストリーミングされるレスポンスを読み取る
JSON Lines やサーバー送信イベントのような逐次的なメディアタイプ(sequential media types)のレスポンスでは、data は非同期反復可能オブジェクトになります。先に status で型を絞り込んでから反復処理します。
ts
const res = await api.GET("/stream/jsonl");
if (res.status === 200) {
for await (const pet of res.data) {
console.log(pet.name);
}
}ループを抜けるとダウンロードは停止します。また、項目を解析できないときは、ループが DecodeError を投げます。
サーバーでリクエストごとにクライアントを派生させる
サーバー側では、1 つのクライアントで多くのユーザーからのリクエストを処理できます。クライアントは一度だけ作成し、受け取ったリクエストごとに with でクライアントを派生させてください。そうすれば、あるユーザーのトークンが別のユーザーのリクエストに渡ることはありません。
ts
const base = createClient({ baseUrl: "https://api.example.com" });
async function handle(incoming: Request): Promise<Response> {
const token = incoming.headers.get("authorization");
const api = base.with(async (request, next) => {
if (token) request.headers.set("authorization", token);
return next(request);
});
const res = await api.GET("/pets");
return Response.json(res.status === 200 ? res.data : []);
}生成されるファイルを管理する
生成されるファイルをコミットすると、oasis-gen を実行しなくてもプロジェクトの型チェックができます。一方、ビルド時に生成すると、ファイルは OpenAPI ドキュメントと同期した状態に保たれます。どちらの方法でも、生成されるファイルはプロジェクトのスタイル規則に従っていないため、リンターやフォーマッターの対象から除外してください。たとえば ESLint のフラット設定(flat config)では、次のようにします。
js
import { globalIgnores } from "eslint/config";
export default [
globalIgnores(["src/api.ts"]),
// ...プロジェクトの設定
];トラブルシューティング
| 問題 | 原因 | 解決方法 |
|---|---|---|
ブラウザー以外の環境での TypeError: Failed to parse URL | リクエストの URL が相対 URL(ドキュメントに servers がないか、baseUrl が相対 URL のため) | 絶対 URL の baseUrl を設定 |
TS5097: An import path can only end with a '.ts' extension ... | インポートパスでの .ts 拡張子を許可していないプロジェクト設定 | allowImportingTsExtensions を有効にするか、./api.js または ./api をインポート |
DecodeError での TS2554: Expected 0-1 arguments, but got 2. | ES2022 より古い lib の設定 | lib を ES2022 以降に設定 |
TS2304: Cannot find name 'RequestInit'、'Blob'、または 'FormData' | lib の dom と @types/node のどちらも利用不可 | いずれかを追加 |
エラーレスポンスでの DecodeError | サーバーから返された、Content-Type と一致しない本体(HTML のエラーページなど) | DecodeError を捕捉して error.response.status を確認。エラーを参照 |
| ブラウザーで送信されないクッキーパラメーターやクッキーの API キー | fetch で設定された Cookie ヘッダーをブラウザーが無視 | ブラウザー自身によるクッキー送信を利用(fetch の credentials オプションなど) |
拒否される nullable | nullable が存在しない OpenAPI 3.1 または 3.2 のドキュメント | type: [..., "null"] を使用 |