Tools for consuming OpenAPI schemas in TypeScript.
Openapi-fetch is an ultra-fast fetch client for TypeScript using your OpenAPI schema. Weighs in at 1 kb and has virtually zero runtime. Works with React, Vue, Svelte, or vanilla JS.
Library | Size (min) |
---|---|
openapi-fetch | 1 kB |
openapi-typescript-fetch | 4 kB |
openapi-typescript-codegen | 345 kB |
The syntax is inspired by popular libraries like react-query or Apollo client, but without all the bells and whistles and in a 1 kb package.
import createClient from "openapi-fetch";
import { paths } from "./v1"; // (generated from openapi-typescript)
const { get, post } = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
// Type-checked request
await post("/create-post", {
body: {
title: "My New Post",
// ❌ Property 'publish_date' is missing in type …
},
});
// Type-checked response
const { data, error } = await get("/blogposts/my-blog-post");
console.log(data.title); // ❌ 'data' is possibly 'undefined'
console.log(error.message); // ❌ 'error' is possibly 'undefined'
console.log(data?.foo); // ❌ Property 'foo' does not exist on type …
Notice there are no generics, and no manual typing. Your endpoint’s exact request & response was inferred automatically off the URL. This makes a big difference in the type safety of your endpoints! This eliminates all of the following:
any
types that hide bugsas
type overrides that can also hide bugsInstall this library along with openapi-typescript:
npm i openapi-fetch
npm i -D openapi-typescript
Next, generate TypeScript types from your OpenAPI schema using openapi-typescript:
npx openapi-typescript ./path/to/api/v1.yaml -o ./src/lib/api/v1.d.ts
⚠️ Be sure to validate your schemas! openapi-typescript will err on invalid schemas.
Lastly, be sure to run typechecking in your project. This can be done by adding tsc --noEmit
to your npm scripts like so:
{
"scripts": {
"test:ts": "tsc --noEmit"
}
}
And run npm run test:ts
in your CI to catch type errors.
✨ Tip
Always use
tsc --noEmit
to check for type errors! Your build tools (Vite, esbuild, webpack, etc.) won’t typecheck as accurately as the TypeScript compiler itself.
Using openapi-fetch is as easy as reading your schema! For example, given the following schema:
Here’s how you’d fetch GET /blogpost/{post_id}
and POST /blogposts
:
import createClient from "openapi-fetch";
import { paths } from "./v1";
const { get, post } = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
const { data, error } = await get("/blogposts/{post_id}", {
params: {
path: { post_id: "my-post" },
query: { version: 2 },
},
});
const { data, error } = await post("/blogposts", {
body: {
title: "New Post",
body: "<p>New post body</p>",
publish_date: new Date("2023-03-01T12:00:00Z").getTime(),
},
});
The pathname of get()
, put()
, post()
, etc. must match your schema literally. Note in the example, the URL is /blogposts/{post_id}
. This library will replace all path
params for you (so they can be typechecked)
✨ Tip
openapi-fetch infers types from the URL. Prefer static string values over dynamic runtime values, e.g.:
- ✅
"/blogposts/{post_id}"
- ❌
[...pathParts].join("/") + "{post_id}"
The get()
request shown needed the params
object that groups parameters by type (path
or query
). If a required param is missing, or the wrong type, a type error will be thrown.
The post()
request required a body
object that provided all necessary requestBody data.
All methods return an object with data, error, and response.
2xx
response if the server returned 2xx
; otherwise it will be undefined
4xx
/5xx
response if the server returned either; otherwise it will be undefined
default
will also be interpreted as error
, since its intent is handling unexpected HTTP codesstatus
, headers
, etc. It is not typechecked.createClient accepts the following options, which set the default settings for all subsequent fetch calls.
createClient<paths>(options);
Name | Type | Description |
---|---|---|
baseUrl | string | Prefix all fetch URLs with this option (e.g. "https://myapi.dev/v1/" ). |
fetch | fetch | Fetch function used for requests (defaults to globalThis.fetch ) |
(Fetch options) | Any valid fetch option (headers , mode , cache , signal …) (docs) |
import { paths } from "./v1";
const { get, put, post, del, options, head, patch, trace } = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
const { data, error, response } = await get("/my-url", options);
Name | Type | Description |
---|---|---|
params | ParamsObject | Provide path and query params from the OpenAPI schema |
params.path | { [name]: value } | Provide all path params (params that are part of the URL) |
params.query | { [name]: value } | Provide all `query params (params that are part of the searchParams |
body | { [name]:value } | The requestBody data, if needed (PUT/POST/PATCH/DEL only) |
querySerializer | QuerySerializer | (optional) Override default param serialization (see Parameter Serialization) |
(Fetch options) | Any valid fetch option (headers , mode , cache , signal …) (docs) |
In the spirit of being lightweight, this library only uses URLSearchParams to serialize parameters. So for complex query param types (e.g. arrays) you’ll need to provide your own querySerializer()
method that transforms query params into a URL-safe string:
import createClient from "openapi-fetch";
import { paths } from "./v1"; // generated from openapi-typescript
const { get, post } = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
const { data, error } = await get("/post/{post_id}", {
params: {
path: { post_id: "my-post" },
query: { version: 2 },
},
querySerializer: (q) => `v=${q.version}`, // ✅ Still typechecked based on the URL!
});
Note that this happens at the request level so that you still get correct type inference for that URL’s specific query params.
Thanks, @ezpuzz!
Authentication often requires some reactivity dependent on a token. Since this library is so low-level, there are myriad ways to handle it:
Here’s how it can be handled using nanostores, a tiny (334 b), universal signals store:
// src/lib/api/index.ts
import { atom, computed } from "nanostores";
import createClient from "openapi-fetch";
import { paths } from "./v1";
export const authToken = atom<string | undefined>();
someAuthMethod().then((newToken) => authToken.set(newToken));
export const client = computed(authToken, (currentToken) =>
createClient<paths>({
headers: currentToken ? { Authorization: `Bearer ${currentToken}` } : {},
baseUrl: "https://myapi.dev/v1/",
})
);
// src/some-other-file.ts
import { client } from "./lib/api";
const { get, post } = client.get();
get("/some-authenticated-url", {
/* … */
});
You can also use proxies which are now supported in all modern browsers:
// src/lib/api/index.ts
import createClient from "openapi-fetch";
import { paths } from "./v1";
let authToken: string | undefined = undefined;
someAuthMethod().then((newToken) => (authToken = newToken));
const baseClient = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
export default new Proxy(baseClient, {
get(_, key: keyof typeof baseClient) {
const newClient = createClient<paths>({
headers: authToken ? { Authorization: `Bearer ${authToken}` } : {},
baseUrl: "https://myapi.dev/v1/",
});
return newClient[key];
},
});
// src/some-other-file.ts
import client from "./lib/api";
client.get("/some-authenticated-url", {
/* … */
});
fetch()
API while reducing boilerplate (such as await res.json()
)
Generate TypeScript types from static OpenAPI schemas
Ultra-fast fetching for TypeScript generated automatically from your OpenAPI schema.
Author: kecrily
Source Code: https://github.com/kecrily/openapi-typescript
License: MIT