Skip to content

Expand Example Documentation at "openapi-ts.dev/examples" #1813

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 231 additions & 2 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
<details>
<summary><a href="/openapi-fetch/">openapi-fetch</a> (recommended)</summary>

::: 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<paths>({ 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",
},
});
```

:::

</details>

<details>
<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>

::: 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<paths>();

// 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);
}
```

:::

</details>

<details>
<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>

::: 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<paths>({
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);
}
}
```

:::

</details>


::: tip

Expand Down Expand Up @@ -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<paths>(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<paths>(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.
Expand Down
1 change: 1 addition & 0 deletions docs/openapi-fetch/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
6 changes: 4 additions & 2 deletions packages/openapi-fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
}
Expand Down
22 changes: 20 additions & 2 deletions packages/openapi-fetch/test/index.bench.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -58,6 +59,10 @@ describe("setup", () => {
});
});

bench("feature-fetch", async () => {
createApiFetchClient({ prefixUrl: BASE_URL });
});

// superagent: N/A
});

Expand All @@ -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");
Expand All @@ -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)", () => {
Expand All @@ -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", {
Expand Down Expand Up @@ -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" },
});
});
});
Loading