本文へ移動

レシピ ​

どのレシピも、生成されるクライアントがすでに提供している機能だけを使います。

想定外のステータスで例外を投げる ​

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 オプションなど)
拒否される nullablenullable が存在しない OpenAPI 3.1 または 3.2 のドキュメントtype: [..., "null"] を使用