Skip to content

Commit be08562

Browse files
authored
Expand Example Documentation at "openapi-ts.dev/examples" (#1813)
* #main expanded example section (wip) * #main updated example section and expanded benchmark * #main undo dev command change * #main fixed pnpm-lock? * #main lint fix --------- Co-authored-by: Benno <[email protected]>
1 parent 7f0a391 commit be08562

File tree

5 files changed

+299
-22
lines changed

5 files changed

+299
-22
lines changed

docs/examples.md

+231-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,135 @@ The types generated by openapi-typescript are universal, and can be used in a va
1111

1212
Fetching data can be done simply and safely using an **automatically-typed fetch wrapper**:
1313

14-
- [openapi-fetch](/openapi-fetch/) (recommended)
15-
- [openapi-typescript-fetch](https://www.npmjs.com/package/openapi-typescript-fetch) by [@ajaishankar](https://github.com/ajaishankar)
14+
<details>
15+
<summary><a href="/openapi-fetch/">openapi-fetch</a> (recommended)</summary>
16+
17+
::: code-group
18+
19+
```ts [test/my-project.ts]
20+
import createClient from "openapi-fetch";
21+
import type { paths } from "./my-openapi-3-schema"; // generated by openapi-typescript
22+
23+
const client = createClient<paths>({ baseUrl: "https://myapi.dev/v1/" });
24+
25+
const {
26+
data, // only present if 2XX response
27+
error, // only present if 4XX or 5XX response
28+
} = await client.GET("/blogposts/{post_id}", {
29+
params: {
30+
path: { post_id: "123" },
31+
},
32+
});
33+
34+
await client.PUT("/blogposts", {
35+
body: {
36+
title: "My New Post",
37+
},
38+
});
39+
```
40+
41+
:::
42+
43+
</details>
44+
45+
<details>
46+
<summary><a href="https://www.npmjs.com/package/openapi-typescript-fetch" target="_blank" rel="noreferrer">openapi-typescript-fetch</a> by <a href="https://github.com/ajaishankar" target="_blank" rel="noreferrer">@ajaishankar</a></summary>
47+
48+
::: code-group
49+
50+
```ts [test/my-project.ts]
51+
import { Fetcher } from 'openapi-typescript-fetch';
52+
import type { paths } from './my-openapi-3-schema'; // generated by openapi-typescript
53+
54+
const fetcher = Fetcher.for<paths>();
55+
56+
// GET request
57+
const getBlogPost = fetcher.path('/blogposts/{post_id}').method('get').create();
58+
59+
try {
60+
const { status, data } = await getBlogPost({ pathParams: { post_id: '123' } });
61+
console.log(data);
62+
} catch (error) {
63+
console.error('Error:', error);
64+
}
65+
66+
// PUT request
67+
const updateBlogPost = fetcher.path('/blogposts').method('put').create();
68+
69+
try {
70+
await updateBlogPost({ body: { title: 'My New Post' } });
71+
} catch (error) {
72+
console.error('Error:', error);
73+
}
74+
```
75+
76+
:::
77+
78+
</details>
79+
80+
<details>
81+
<summary><a href="https://www.npmjs.com/package/feature-fetch" target="_blank" rel="noreferrer">feature-fetch</a> by <a href="https://github.com/builder-group" target="_blank" rel="noreferrer">builder.group</a></summary>
82+
83+
::: code-group
84+
85+
```ts [test/my-project.ts]
86+
import { createOpenApiFetchClient } from 'feature-fetch';
87+
import type { paths } from './my-openapi-3-schema'; // generated by openapi-typescript
88+
89+
// Create the OpenAPI fetch client
90+
const fetchClient = createOpenApiFetchClient<paths>({
91+
prefixUrl: 'https://myapi.dev/v1'
92+
});
93+
94+
// Send a GET request
95+
const response = await fetchClient.get('/blogposts/{post_id}', {
96+
pathParams: {
97+
post_id: '123',
98+
},
99+
});
100+
101+
// Handle the response (Approach 1: Standard if-else)
102+
if (response.isOk()) {
103+
const data = response.value.data;
104+
console.log(data); // Handle successful response
105+
} else {
106+
const error = response.error;
107+
if (error instanceof NetworkError) {
108+
console.error('Network error:', error.message);
109+
} else if (error instanceof RequestError) {
110+
console.error('Request error:', error.message, 'Status:', error.status);
111+
} else {
112+
console.error('Service error:', error.message);
113+
}
114+
}
115+
116+
// Send a PUT request
117+
const putResponse = await fetchClient.put('/blogposts', {
118+
body: {
119+
title: 'My New Post',
120+
},
121+
});
122+
123+
// Handle the response (Approach 2: Try-catch)
124+
try {
125+
const putData = putResponse.unwrap().data;
126+
console.log(putData); // Handle the successful response
127+
} catch (error) {
128+
// Handle the error
129+
if (error instanceof NetworkError) {
130+
console.error('Network error:', error.message);
131+
} else if (error instanceof RequestError) {
132+
console.error('Request error:', error.message, 'Status:', error.status);
133+
} else {
134+
console.error('Service error:', error.message);
135+
}
136+
}
137+
```
138+
139+
:::
140+
141+
</details>
142+
16143

17144
::: tip
18145

@@ -60,6 +187,108 @@ TypeChecking in server environments can be tricky, as you’re often querying da
60187

61188
:::
62189

190+
## Hono with [`@blgc/openapi-router`](https://github.com/builder-group/community/tree/develop/packages/openapi-router)
191+
192+
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.
193+
194+
::: tip Good To Know
195+
196+
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.
197+
198+
:::
199+
200+
::: code-group
201+
202+
```ts [src/router.ts]
203+
import { createHonoOpenApiRouter } from '@blgc/openapi-router';
204+
import { Hono } from 'hono';
205+
import { zValidator } from 'validation-adapters/zod';
206+
import * as z from 'zod';
207+
208+
import { paths } from './gen/v1'; // OpenAPI-generated types
209+
import { PetSchema } from './schemas'; // Custom reusable Zod schema for validation
210+
211+
export const router = new Hono();
212+
export const openApiRouter = createHonoOpenApiRouter<paths>(router);
213+
214+
// GET /pet/{petId}
215+
openApiRouter.get('/pet/{petId}', {
216+
pathValidator: zValidator(
217+
z.object({
218+
petId: z.number() // Validate that petId is a number
219+
})
220+
),
221+
handler: (c) => {
222+
const { petId } = c.req.valid('param'); // Access validated params
223+
return c.json({ name: 'Falko', photoUrls: [] });
224+
}
225+
});
226+
227+
// POST /pet
228+
openApiRouter.post('/pet', {
229+
bodyValidator: zValidator(PetSchema), // Validate request body using PetSchema
230+
handler: (c) => {
231+
const { name, photoUrls } = c.req.valid('json'); // Access validated body data
232+
return c.json({ name, photoUrls });
233+
}
234+
});
235+
```
236+
237+
:::
238+
239+
[Full example](https://github.com/builder-group/community/tree/develop/examples/openapi-router/hono/petstore)
240+
241+
## Express with [`@blgc/openapi-router`](https://github.com/builder-group/community/tree/develop/packages/openapi-router)
242+
243+
[`@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.
244+
245+
::: tip Good To Know
246+
247+
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.
248+
249+
:::
250+
251+
::: code-group
252+
253+
```ts [src/router.ts]
254+
import { createExpressOpenApiRouter } from '@blgc/openapi-router';
255+
import { Router } from 'express';
256+
import * as v from 'valibot';
257+
import { vValidator } from 'validation-adapters/valibot';
258+
259+
import { paths } from './gen/v1'; // OpenAPI-generated types
260+
import { PetSchema } from './schemas'; // Custom reusable Zod schema for validation
261+
262+
export const router: Router = Router();
263+
export const openApiRouter = createExpressOpenApiRouter<paths>(router);
264+
265+
// GET /pet/{petId}
266+
openApiRouter.get('/pet/{petId}', {
267+
pathValidator: vValidator(
268+
v.object({
269+
petId: v.number() // Validate that petId is a number
270+
})
271+
),
272+
handler: (req, res) => {
273+
const { petId } = req.params; // Access validated params
274+
res.send({ name: 'Falko', photoUrls: [] });
275+
}
276+
});
277+
278+
// POST /pet
279+
openApiRouter.post('/pet', {
280+
bodyValidator: vValidator(PetSchema), // Validate request body using PetSchema
281+
handler: (req, res) => {
282+
const { name, photoUrls } = req.body; // Access validated body data
283+
res.send({ name, photoUrls });
284+
}
285+
});
286+
```
287+
288+
:::
289+
290+
[Full example](https://github.com/builder-group/community/tree/develop/examples/openapi-router/express/petstore)
291+
63292
## Mock-Service-Worker (MSW)
64293

65294
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.

docs/openapi-fetch/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ openapi-fetch is a type-safe fetch client that pulls in your OpenAPI schema. Wei
1010
| :------------------------- | ---------: | :------------------------- |
1111
| openapi-fetch | `5 kB` | `300k` ops/s (fastest) |
1212
| openapi-typescript-fetch | `4 kB` | `150k` ops/s (2× slower) |
13+
| feature-fetch | `15 kB` | `300k` ops/s (not slower) |
1314
| axios | `32 kB` | `225k` ops/s (1.3× slower) |
1415
| superagent | `55 kB` | `50k` ops/s (6× slower) |
1516
| openapi-typescript-codegen | `367 kB` | `100k` ops/s (3× slower) |

packages/openapi-fetch/package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"test:ts": "tsc --noEmit",
6262
"test:ts-no-strict": "tsc --noEmit -p test/no-strict-null-checks/tsconfig.json",
6363
"test-e2e": "playwright test",
64+
"bench:js": "vitest bench",
6465
"e2e-vite-build": "vite build test/fixtures/e2e",
6566
"e2e-vite-start": "vite preview test/fixtures/e2e",
6667
"version": "pnpm run prepare && pnpm run build"
@@ -69,15 +70,16 @@
6970
"openapi-typescript-helpers": "workspace:^"
7071
},
7172
"devDependencies": {
72-
"axios": "^1.7.2",
73+
"axios": "^1.7.4",
7374
"del-cli": "^5.1.0",
7475
"esbuild": "^0.20.2",
7576
"execa": "^8.0.1",
77+
"feature-fetch": "^0.0.15",
7678
"msw": "^2.3.1",
7779
"openapi-typescript": "workspace:^",
7880
"openapi-typescript-codegen": "^0.25.0",
7981
"openapi-typescript-fetch": "^2.0.0",
80-
"superagent": "^9.0.2",
82+
"superagent": "^10.0.1",
8183
"typescript": "^5.4.5",
8284
"vite": "^5.3.5"
8385
}

packages/openapi-fetch/test/index.bench.js

+20-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import superagent from "superagent";
66
import { afterAll, bench, describe } from "vitest";
77
import createClient, { createPathBasedClient } from "../dist/index.js";
88
import * as openapiTSCodegen from "./fixtures/openapi-typescript-codegen.min.js";
9+
import { createApiFetchClient } from "feature-fetch";
910

1011
const BASE_URL = "https://api.test.local";
1112

@@ -58,6 +59,10 @@ describe("setup", () => {
5859
});
5960
});
6061

62+
bench("feature-fetch", async () => {
63+
createApiFetchClient({ prefixUrl: BASE_URL });
64+
});
65+
6166
// superagent: N/A
6267
});
6368

@@ -69,10 +74,10 @@ describe("get (only URL)", () => {
6974
baseUrl: BASE_URL,
7075
});
7176
const openapiTSFetchGET = openapiTSFetch.path("/url").method("get").create();
72-
7377
const axiosInstance = axios.create({
7478
baseURL: BASE_URL,
7579
});
80+
const featureFetch = createApiFetchClient({ prefixUrl: BASE_URL });
7681

7782
bench("openapi-fetch", async () => {
7883
await openapiFetch.GET("/url");
@@ -97,6 +102,10 @@ describe("get (only URL)", () => {
97102
bench("superagent", async () => {
98103
await superagent.get(`${BASE_URL}/url`);
99104
});
105+
106+
bench("feature-fetch", async () => {
107+
await featureFetch.get("/url");
108+
});
100109
});
101110

102111
describe("get (headers)", () => {
@@ -114,10 +123,13 @@ describe("get (headers)", () => {
114123
init: { headers: { "x-base-header": 123 } },
115124
});
116125
const openapiTSFetchGET = openapiTSFetch.path("/url").method("get").create();
117-
118126
const axiosInstance = axios.create({
119127
baseURL: BASE_URL,
120128
});
129+
const featureFetch = createApiFetchClient({
130+
prefixUrl: BASE_URL,
131+
headers: { "x-base-header": "123" },
132+
});
121133

122134
bench("openapi-fetch", async () => {
123135
await openapiFetch.GET("/url", {
@@ -150,4 +162,10 @@ describe("get (headers)", () => {
150162
bench("superagent", async () => {
151163
await superagent.get(`${BASE_URL}/url`).set("x-header-1", 123).set("x-header-2", 456);
152164
});
165+
166+
bench("feature-fetch", async () => {
167+
await featureFetch.get("/url", {
168+
headers: { "x-header-1": "123", "x-header-2": "456" },
169+
});
170+
});
153171
});

0 commit comments

Comments
 (0)