diff --git a/docs/examples.md b/docs/examples.md
index 51895b291..d53129939 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -11,8 +11,135 @@ The types generated by openapi-typescript are universal, and can be used in a va
Fetching data can be done simply and safely using an **automatically-typed fetch wrapper**:
-- [openapi-fetch](/openapi-fetch/) (recommended)
-- [openapi-typescript-fetch](https://www.npmjs.com/package/openapi-typescript-fetch) by [@ajaishankar](https://github.com/ajaishankar)
+
+openapi-fetch (recommended)
+
+::: code-group
+
+```ts [test/my-project.ts]
+import createClient from "openapi-fetch";
+import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript
+
+const client = createClient({ baseUrl: "https://myapi.dev/v1/" });
+
+const {
+ data, // only present if 2XX response
+ error, // only present if 4XX or 5XX response
+} = await client.GET("/blogposts/{post_id}", {
+ params: {
+ path: { post_id: "123" },
+ },
+});
+
+await client.PUT("/blogposts", {
+ body: {
+ title: "My New Post",
+ },
+});
+```
+
+:::
+
+
+
+
+openapi-typescript-fetch by @ajaishankar
+
+::: code-group
+
+```ts [test/my-project.ts]
+import { Fetcher } from 'openapi-typescript-fetch';
+import type { paths } from './my-openapi-3-schema'; // generated by openapi-typescript
+
+const fetcher = Fetcher.for();
+
+// GET request
+const getBlogPost = fetcher.path('/blogposts/{post_id}').method('get').create();
+
+try {
+ const { status, data } = await getBlogPost({ pathParams: { post_id: '123' } });
+ console.log(data);
+} catch (error) {
+ console.error('Error:', error);
+}
+
+// PUT request
+const updateBlogPost = fetcher.path('/blogposts').method('put').create();
+
+try {
+ await updateBlogPost({ body: { title: 'My New Post' } });
+} catch (error) {
+ console.error('Error:', error);
+}
+```
+
+:::
+
+
+
+
+feature-fetch by builder.group
+
+::: code-group
+
+```ts [test/my-project.ts]
+import { createOpenApiFetchClient } from 'feature-fetch';
+import type { paths } from './my-openapi-3-schema'; // generated by openapi-typescript
+
+// Create the OpenAPI fetch client
+const fetchClient = createOpenApiFetchClient({
+ prefixUrl: 'https://myapi.dev/v1'
+});
+
+// Send a GET request
+const response = await fetchClient.get('/blogposts/{post_id}', {
+ pathParams: {
+ post_id: '123',
+ },
+});
+
+// Handle the response (Approach 1: Standard if-else)
+if (response.isOk()) {
+ const data = response.value.data;
+ console.log(data); // Handle successful response
+} else {
+ const error = response.error;
+ if (error instanceof NetworkError) {
+ console.error('Network error:', error.message);
+ } else if (error instanceof RequestError) {
+ console.error('Request error:', error.message, 'Status:', error.status);
+ } else {
+ console.error('Service error:', error.message);
+ }
+}
+
+// Send a PUT request
+const putResponse = await fetchClient.put('/blogposts', {
+ body: {
+ title: 'My New Post',
+ },
+});
+
+// Handle the response (Approach 2: Try-catch)
+try {
+ const putData = putResponse.unwrap().data;
+ console.log(putData); // Handle the successful response
+} catch (error) {
+ // Handle the error
+ if (error instanceof NetworkError) {
+ console.error('Network error:', error.message);
+ } else if (error instanceof RequestError) {
+ console.error('Request error:', error.message, 'Status:', error.status);
+ } else {
+ console.error('Service error:', error.message);
+ }
+}
+```
+
+:::
+
+
+
::: tip
@@ -60,6 +187,108 @@ TypeChecking in server environments can be tricky, as you’re often querying da
:::
+## Hono with [`@blgc/openapi-router`](https://github.com/builder-group/community/tree/develop/packages/openapi-router)
+
+Instead of manually typing each route with generics as in the [Hono example](#hono), [`@blgc/openapi-router`](https://github.com/builder-group/community/tree/develop/packages/openapi-router) wraps around the [Hono router](https://hono.dev/docs/api/routing) to deliver full typesafety and enforce your OpenAPI-Schema with validators.
+
+::: tip Good To Know
+
+While TypeScript types ensure compile-time safety, they don't enforce runtime schema validation. For runtime compliance, you need to integrate with validation libraries like Zod or Valibot. Although you must define the validation rules manually, they are type-safe to ensure these rules are correctly defined.
+
+:::
+
+::: code-group
+
+```ts [src/router.ts]
+import { createHonoOpenApiRouter } from '@blgc/openapi-router';
+import { Hono } from 'hono';
+import { zValidator } from 'validation-adapters/zod';
+import * as z from 'zod';
+
+import { paths } from './gen/v1'; // OpenAPI-generated types
+import { PetSchema } from './schemas'; // Custom reusable Zod schema for validation
+
+export const router = new Hono();
+export const openApiRouter = createHonoOpenApiRouter(router);
+
+// GET /pet/{petId}
+openApiRouter.get('/pet/{petId}', {
+ pathValidator: zValidator(
+ z.object({
+ petId: z.number() // Validate that petId is a number
+ })
+ ),
+ handler: (c) => {
+ const { petId } = c.req.valid('param'); // Access validated params
+ return c.json({ name: 'Falko', photoUrls: [] });
+ }
+});
+
+// POST /pet
+openApiRouter.post('/pet', {
+ bodyValidator: zValidator(PetSchema), // Validate request body using PetSchema
+ handler: (c) => {
+ const { name, photoUrls } = c.req.valid('json'); // Access validated body data
+ return c.json({ name, photoUrls });
+ }
+});
+```
+
+:::
+
+[Full example](https://github.com/builder-group/community/tree/develop/examples/openapi-router/hono/petstore)
+
+## Express with [`@blgc/openapi-router`](https://github.com/builder-group/community/tree/develop/packages/openapi-router)
+
+[`@blgc/openapi-router`](https://github.com/builder-group/community/tree/develop/packages/openapi-router) wraps around the [Express router](https://expressjs.com/en/5x/api.html#router) to deliver full typesafety and enforce your OpenAPI-Schema with validators.
+
+::: tip Good To Know
+
+While TypeScript types ensure compile-time safety, they don't enforce runtime schema validation. For runtime compliance, you need to integrate with validation libraries like Zod or Valibot. Although you must define the validation rules manually, they are type-safe to ensure these rules are correctly defined.
+
+:::
+
+::: code-group
+
+```ts [src/router.ts]
+import { createExpressOpenApiRouter } from '@blgc/openapi-router';
+import { Router } from 'express';
+import * as v from 'valibot';
+import { vValidator } from 'validation-adapters/valibot';
+
+import { paths } from './gen/v1'; // OpenAPI-generated types
+import { PetSchema } from './schemas'; // Custom reusable Zod schema for validation
+
+export const router: Router = Router();
+export const openApiRouter = createExpressOpenApiRouter(router);
+
+// GET /pet/{petId}
+openApiRouter.get('/pet/{petId}', {
+ pathValidator: vValidator(
+ v.object({
+ petId: v.number() // Validate that petId is a number
+ })
+ ),
+ handler: (req, res) => {
+ const { petId } = req.params; // Access validated params
+ res.send({ name: 'Falko', photoUrls: [] });
+ }
+});
+
+// POST /pet
+openApiRouter.post('/pet', {
+ bodyValidator: vValidator(PetSchema), // Validate request body using PetSchema
+ handler: (req, res) => {
+ const { name, photoUrls } = req.body; // Access validated body data
+ res.send({ name, photoUrls });
+ }
+});
+```
+
+:::
+
+[Full example](https://github.com/builder-group/community/tree/develop/examples/openapi-router/express/petstore)
+
## Mock-Service-Worker (MSW)
If you are using [Mock Service Worker (MSW)](https://mswjs.io) to define your API mocks, you can use a **small, automatically-typed wrapper** around MSW, which enables you to address conflicts in your API mocks easily when your OpenAPI specification changes. Ultimately, you can have the same level of confidence in your application's API client **and** API mocks.
diff --git a/docs/openapi-fetch/index.md b/docs/openapi-fetch/index.md
index 1f57879fa..eedb3d3aa 100644
--- a/docs/openapi-fetch/index.md
+++ b/docs/openapi-fetch/index.md
@@ -10,6 +10,7 @@ openapi-fetch is a type-safe fetch client that pulls in your OpenAPI schema. Wei
| :------------------------- | ---------: | :------------------------- |
| openapi-fetch | `5 kB` | `300k` ops/s (fastest) |
| openapi-typescript-fetch | `4 kB` | `150k` ops/s (2× slower) |
+| feature-fetch | `15 kB` | `300k` ops/s (not slower) |
| axios | `32 kB` | `225k` ops/s (1.3× slower) |
| superagent | `55 kB` | `50k` ops/s (6× slower) |
| openapi-typescript-codegen | `367 kB` | `100k` ops/s (3× slower) |
diff --git a/packages/openapi-fetch/package.json b/packages/openapi-fetch/package.json
index d2cfcc368..b8589d808 100644
--- a/packages/openapi-fetch/package.json
+++ b/packages/openapi-fetch/package.json
@@ -61,6 +61,7 @@
"test:ts": "tsc --noEmit",
"test:ts-no-strict": "tsc --noEmit -p test/no-strict-null-checks/tsconfig.json",
"test-e2e": "playwright test",
+ "bench:js": "vitest bench",
"e2e-vite-build": "vite build test/fixtures/e2e",
"e2e-vite-start": "vite preview test/fixtures/e2e",
"version": "pnpm run prepare && pnpm run build"
@@ -69,15 +70,16 @@
"openapi-typescript-helpers": "workspace:^"
},
"devDependencies": {
- "axios": "^1.7.2",
+ "axios": "^1.7.4",
"del-cli": "^5.1.0",
"esbuild": "^0.20.2",
"execa": "^8.0.1",
+ "feature-fetch": "^0.0.15",
"msw": "^2.3.1",
"openapi-typescript": "workspace:^",
"openapi-typescript-codegen": "^0.25.0",
"openapi-typescript-fetch": "^2.0.0",
- "superagent": "^9.0.2",
+ "superagent": "^10.0.1",
"typescript": "^5.4.5",
"vite": "^5.3.5"
}
diff --git a/packages/openapi-fetch/test/index.bench.js b/packages/openapi-fetch/test/index.bench.js
index fdf780870..e8c3e3308 100644
--- a/packages/openapi-fetch/test/index.bench.js
+++ b/packages/openapi-fetch/test/index.bench.js
@@ -6,6 +6,7 @@ import superagent from "superagent";
import { afterAll, bench, describe } from "vitest";
import createClient, { createPathBasedClient } from "../dist/index.js";
import * as openapiTSCodegen from "./fixtures/openapi-typescript-codegen.min.js";
+import { createApiFetchClient } from "feature-fetch";
const BASE_URL = "https://api.test.local";
@@ -58,6 +59,10 @@ describe("setup", () => {
});
});
+ bench("feature-fetch", async () => {
+ createApiFetchClient({ prefixUrl: BASE_URL });
+ });
+
// superagent: N/A
});
@@ -69,10 +74,10 @@ describe("get (only URL)", () => {
baseUrl: BASE_URL,
});
const openapiTSFetchGET = openapiTSFetch.path("/url").method("get").create();
-
const axiosInstance = axios.create({
baseURL: BASE_URL,
});
+ const featureFetch = createApiFetchClient({ prefixUrl: BASE_URL });
bench("openapi-fetch", async () => {
await openapiFetch.GET("/url");
@@ -97,6 +102,10 @@ describe("get (only URL)", () => {
bench("superagent", async () => {
await superagent.get(`${BASE_URL}/url`);
});
+
+ bench("feature-fetch", async () => {
+ await featureFetch.get("/url");
+ });
});
describe("get (headers)", () => {
@@ -114,10 +123,13 @@ describe("get (headers)", () => {
init: { headers: { "x-base-header": 123 } },
});
const openapiTSFetchGET = openapiTSFetch.path("/url").method("get").create();
-
const axiosInstance = axios.create({
baseURL: BASE_URL,
});
+ const featureFetch = createApiFetchClient({
+ prefixUrl: BASE_URL,
+ headers: { "x-base-header": "123" },
+ });
bench("openapi-fetch", async () => {
await openapiFetch.GET("/url", {
@@ -150,4 +162,10 @@ describe("get (headers)", () => {
bench("superagent", async () => {
await superagent.get(`${BASE_URL}/url`).set("x-header-1", 123).set("x-header-2", 456);
});
+
+ bench("feature-fetch", async () => {
+ await featureFetch.get("/url", {
+ headers: { "x-header-1": "123", "x-header-2": "456" },
+ });
+ });
});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 06df6b9f0..39a446be1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -40,7 +40,7 @@ importers:
devDependencies:
vitepress:
specifier: 1.3.2
- version: 1.3.2(@algolia/client-search@4.24.0)(@types/node@20.14.7)(@types/react@18.3.3)(axios@1.7.2)(postcss@8.4.41)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.13.0)(typescript@5.5.4)
+ version: 1.3.2(@algolia/client-search@4.24.0)(@types/node@20.14.7)(@types/react@18.3.3)(axios@1.7.4)(postcss@8.4.41)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.13.0)(typescript@5.5.4)
packages/openapi-fetch:
dependencies:
@@ -49,8 +49,8 @@ importers:
version: link:../openapi-typescript-helpers
devDependencies:
axios:
- specifier: ^1.7.2
- version: 1.7.2
+ specifier: ^1.7.4
+ version: 1.7.4
del-cli:
specifier: ^5.1.0
version: 5.1.0
@@ -60,6 +60,9 @@ importers:
execa:
specifier: ^8.0.1
version: 8.0.1
+ feature-fetch:
+ specifier: ^0.0.15
+ version: 0.0.15
msw:
specifier: ^2.3.1
version: 2.3.1(typescript@5.4.5)
@@ -73,8 +76,8 @@ importers:
specifier: ^2.0.0
version: 2.0.0
superagent:
- specifier: ^9.0.2
- version: 9.0.2
+ specifier: ^10.0.1
+ version: 10.0.1
typescript:
specifier: ^5.4.5
version: 5.4.5
@@ -567,6 +570,12 @@ packages:
cpu: [x64]
os: [win32]
+ '@blgc/types@0.0.6':
+ resolution: {integrity: sha512-RyHXG7rLGNZCVyq9B9/WCd0MBTuXlUttE1qUukvvbTUZMa7NXjzoAFDarGf3RSraf5c9/kQgo3nPYIXqTiCbjw==}
+
+ '@blgc/utils@0.0.15':
+ resolution: {integrity: sha512-iVUnqJxIuukRVEcV1g0JjOaRqibUnwW0d+8MnsBLEGak6tUBNWvPJYnxexutts/epczDr9GgnugLFTXEWb7qJQ==}
+
'@bundled-es-modules/cookie@2.0.0':
resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==}
@@ -1764,8 +1773,8 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'}
- axios@1.7.2:
- resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==}
+ axios@1.7.4:
+ resolution: {integrity: sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==}
axobject-query@4.1.0:
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
@@ -2235,6 +2244,9 @@ packages:
fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
+ feature-fetch@0.0.15:
+ resolution: {integrity: sha512-fScwADZ5pZ5aXGTd4U+LUX1BQMNISIJHHEXZnAn0mDPDyhrGztXyU/A5EhnPNGXS9ajVxli99/YO3Vkvy/5Stg==}
+
fill-range@7.0.1:
resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==}
engines: {node: '>=8'}
@@ -3460,8 +3472,8 @@ packages:
babel-plugin-macros:
optional: true
- superagent@9.0.2:
- resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==}
+ superagent@10.0.1:
+ resolution: {integrity: sha512-kG7dZ4Z6s6VbCVxd0PJpkYND0X+SW+iIAuboIQyHE7eFSNVprFVTpG1uID3UsVS7Jw47tdPvSiCSGzgXDhFcGQ==}
engines: {node: '>=14.18.0'}
superjson@2.2.1:
@@ -3600,6 +3612,9 @@ packages:
resolution: {integrity: sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ==}
engines: {node: '>=12'}
+ ts-results-es@4.2.0:
+ resolution: {integrity: sha512-GfpRk+qvHxa/6gADH8WMN/jXvs5oHYbKtMQc6X9L3VhToy5Lri3iQowyYSytaRcvPDiTT2z3vurzQZXFQFXKRA==}
+
tslib@2.6.3:
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
@@ -4306,6 +4321,10 @@ snapshots:
'@biomejs/cli-win32-x64@1.8.1':
optional: true
+ '@blgc/types@0.0.6': {}
+
+ '@blgc/utils@0.0.15': {}
+
'@bundled-es-modules/cookie@2.0.0':
dependencies:
cookie: 0.5.0
@@ -5369,13 +5388,13 @@ snapshots:
- '@vue/composition-api'
- vue
- '@vueuse/integrations@10.11.1(axios@1.7.2)(focus-trap@7.5.4)(vue@3.4.37(typescript@5.5.4))':
+ '@vueuse/integrations@10.11.1(axios@1.7.4)(focus-trap@7.5.4)(vue@3.4.37(typescript@5.5.4))':
dependencies:
'@vueuse/core': 10.11.1(vue@3.4.37(typescript@5.5.4))
'@vueuse/shared': 10.11.1(vue@3.4.37(typescript@5.5.4))
vue-demi: 0.14.10(vue@3.4.37(typescript@5.5.4))
optionalDependencies:
- axios: 1.7.2
+ axios: 1.7.4
focus-trap: 7.5.4
transitivePeerDependencies:
- '@vue/composition-api'
@@ -5514,7 +5533,7 @@ snapshots:
dependencies:
possible-typed-array-names: 1.0.0
- axios@1.7.2:
+ axios@1.7.4:
dependencies:
follow-redirects: 1.15.6
form-data: 4.0.0
@@ -6079,6 +6098,12 @@ snapshots:
dependencies:
reusify: 1.0.4
+ feature-fetch@0.0.15:
+ dependencies:
+ '@blgc/types': 0.0.6
+ '@blgc/utils': 0.0.15
+ ts-results-es: 4.2.0
+
fill-range@7.0.1:
dependencies:
to-regex-range: 5.0.1
@@ -7341,11 +7366,11 @@ snapshots:
client-only: 0.0.1
react: 18.3.1
- superagent@9.0.2:
+ superagent@10.0.1:
dependencies:
component-emitter: 1.3.1
cookiejar: 2.1.4
- debug: 4.3.5(supports-color@9.4.0)
+ debug: 4.3.6(supports-color@9.4.0)
fast-safe-stringify: 2.1.1
form-data: 4.0.0
formidable: 3.5.1
@@ -7476,6 +7501,8 @@ snapshots:
trim-newlines@4.1.1: {}
+ ts-results-es@4.2.0: {}
+
tslib@2.6.3: {}
tty-table@4.2.3:
@@ -7616,7 +7643,7 @@ snapshots:
optionalDependencies:
vite: 5.3.5(@types/node@20.14.7)
- vitepress@1.3.2(@algolia/client-search@4.24.0)(@types/node@20.14.7)(@types/react@18.3.3)(axios@1.7.2)(postcss@8.4.41)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.13.0)(typescript@5.5.4):
+ vitepress@1.3.2(@algolia/client-search@4.24.0)(@types/node@20.14.7)(@types/react@18.3.3)(axios@1.7.4)(postcss@8.4.41)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.13.0)(typescript@5.5.4):
dependencies:
'@docsearch/css': 3.6.1
'@docsearch/js': 3.6.1(@algolia/client-search@4.24.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.13.0)
@@ -7627,7 +7654,7 @@ snapshots:
'@vue/devtools-api': 7.3.7
'@vue/shared': 3.4.37
'@vueuse/core': 10.11.1(vue@3.4.37(typescript@5.5.4))
- '@vueuse/integrations': 10.11.1(axios@1.7.2)(focus-trap@7.5.4)(vue@3.4.37(typescript@5.5.4))
+ '@vueuse/integrations': 10.11.1(axios@1.7.4)(focus-trap@7.5.4)(vue@3.4.37(typescript@5.5.4))
focus-trap: 7.5.4
mark.js: 8.11.1
minisearch: 7.1.0