Skip to content

Recipes ​

Each recipe uses only what the generated client already provides.

Throwing on unexpected statuses ​

Calls return non-2xx responses instead of throwing. When an exception works better, for example in a data-fetching library, wrap the call and pass the status that you expect:

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;
}; /** * Resolves to the data of a response with `status`, * and throws an HttpError for any other status. */ 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` is typed from the 200 response. const
pet
= await
expectStatus
(
call
, 200);

TanStack Query ​

You do not need generated hooks. Make the call in queryFn with the signal that TanStack Query provides, so that canceled queries abort the request.

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);
}, }); }

Setting the base URL for each environment ​

To call a different server in each environment, for example when the document's servers point to a development host, set baseUrl from your build's environment variables. With Vite:

ts
const 
api
=
createClient
({
baseUrl
: import.meta.
env
.
VITE_API_BASE_URL
});

In the browser, a relative baseUrl such as "/api" also works, for example with the server.proxy option of Vite's dev server.

Refreshing a token on 401 ​

A middleware can call next more than once. To retry a request with a new token after a 401 response, keep an unsent copy of the request:

ts
declare function 
signIn
():
Promise
<string>;
let
token
= await
signIn
();
const
bearer
:
Middleware
= async (
request
,
next
) => {
// A body can only be sent once, so keep a copy for the retry. 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
] });

Mocking responses in tests ​

The fetch option replaces the network for the whole client:

ts
const 
pet
= {
id
: 1,
name
: "Tama",
kind
: "cat" };
const
api
=
createClient
({
baseUrl
: "https://api.test",
fetch
: async () =>
Response
.
json
(
pet
),
});

To mock some operations and send the other requests over the network, use a middleware that returns a Response without calling next:

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
);

Reading streamed responses ​

For a response with a sequential media type, such as JSON Lines or server-sent events, data is an async iterable. Narrow on status first, and then iterate:

ts
const 
res
= await
api
.
GET
("/stream/jsonl");
if (
res
.
status
=== 200) {
for await (const
pet
of
res
.
data
) {
console
.
log
(
pet
.
name
);
} }

Breaking out of the loop stops the download. If an item does not parse, the loop throws a DecodeError.

Per-request clients on a server ​

On a server, one client can serve requests from many users. Create the client once, and use with to derive a client for each incoming request, so that one user's token never reaches another user's request:

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
: []);
}

Managing the generated file ​

If you commit the generated file, the project can type-check without running oasis-gen. If you generate it in a build step instead, it stays in sync with the document. In either case, exclude the file from linters and formatters, because it does not follow your project's style rules. For example, with ESLint's flat config:

js
import { globalIgnores } from "eslint/config";

export default [
  globalIgnores(["src/api.ts"]),
  // ...your config
];

Troubleshooting ​

ProblemCauseSolution
TypeError: Failed to parse URL outside a browserThe request URL is relative, because the document has no servers or baseUrl is relativeSet an absolute baseUrl
TS5097: An import path can only end with a '.ts' extension ...The project does not allow .ts extensions in importsEnable allowImportingTsExtensions, or import ./api.js or ./api
TS2554: Expected 0-1 arguments, but got 2. in DecodeErrorlib is older than ES2022Set lib to ES2022 or later
TS2304: Cannot find name 'RequestInit', 'Blob', or 'FormData'Neither the dom lib nor @types/node is availableAdd one of them
DecodeError for an error responseThe server returned a body that does not match its Content-Type, such as an HTML error pageCatch DecodeError and check error.response.status; see Errors
Cookie parameters or cookie API keys are not sent in a browserBrowsers ignore a Cookie header set in fetchLet the browser send its own cookies, for example with the credentials fetch option
nullable is rejectedThe document is OpenAPI 3.1 or 3.2, where nullable does not existUse type: [..., "null"]