@shipworthy/openapi-fetch 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -33
- package/dist/index.cjs +250 -49
- package/dist/index.d.cts +64 -29
- package/dist/index.d.ts +64 -29
- package/dist/index.js +250 -49
- package/dist/transform-casing.cjs +161 -0
- package/dist/transform-casing.d.cts +138 -0
- package/dist/transform-casing.d.ts +138 -0
- package/dist/transform-casing.js +160 -0
- package/package.json +14 -4
package/README.md
CHANGED
|
@@ -8,18 +8,20 @@ A small, type-safe `fetch` wrapper for OpenAPI 3.x APIs. You give it the `paths`
|
|
|
8
8
|
bun add @shipworthy/openapi-fetch
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
To generate the `paths` type, install the CLI
|
|
11
|
+
To generate the `paths` type, install the CLI and its TypeScript generator as dev dependencies:
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
bun add -d @shipworthy/openapi-cli @shipworthy/openapi-typescript
|
|
14
|
+
bun add -d @shipworthy/openapi-cli @shipworthy/openapi-typescript
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
These packages are build-time tooling
|
|
17
|
+
These packages are build-time tooling. The fetch package has no runtime or peer dependencies.
|
|
18
18
|
|
|
19
19
|
Requirements:
|
|
20
20
|
|
|
21
|
-
- **No
|
|
22
|
-
- **ESM and CommonJS.**
|
|
21
|
+
- **No runtime or peer dependencies.** Compile optional case mappings with `@shipworthy/openapi-typescript` at build time.
|
|
22
|
+
- **ESM and CommonJS.** Both runtime entries contain both formats with matching `.d.ts` and
|
|
23
|
+
`.d.cts` declarations in `dist/`; no TypeScript transpilation of `node_modules` is needed.
|
|
24
|
+
Declarations type-check with TypeScript 5.0 and later.
|
|
23
25
|
- **No Node.js engine requirement for the client.** The code-generation CLI needs Node.js `>=22.19.0`, or Bun with `bunx --bun`.
|
|
24
26
|
- **Fetch API globals.** The client uses `fetch`, `Request`, `Response`, `Headers`, `FormData`, and `URLSearchParams` from `globalThis`, so it runs in browsers, Bun, Deno, and Node.js 18+. To use a different implementation, pass `fetch` and/or `Request` to `createClient`.
|
|
25
27
|
- **TypeScript** with `strict` (or at least `strictNullChecks`) is recommended. Required/optional inference for `params` and `body` depends on it.
|
|
@@ -29,16 +31,16 @@ Requirements:
|
|
|
29
31
|
### 1. Generate types
|
|
30
32
|
|
|
31
33
|
```sh
|
|
32
|
-
bunx --bun @shipworthy/openapi-cli generate typescript ./openapi.yaml -o ./src/api
|
|
34
|
+
bunx --bun @shipworthy/openapi-cli generate typescript -i ./openapi.yaml -o ./src/api
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
The generated file exports `paths`, `components`, and `operations` interfaces. `openapi-fetch` only needs `paths`.
|
|
37
|
+
This writes `src/api/types.ts`. The generated file exports `paths`, `components`, and `operations` interfaces. `openapi-fetch` only needs `paths`.
|
|
36
38
|
|
|
37
39
|
### 2. Create a client and call it
|
|
38
40
|
|
|
39
41
|
```ts
|
|
40
42
|
import createClient from "@shipworthy/openapi-fetch";
|
|
41
|
-
import type { paths } from "./api/
|
|
43
|
+
import type { paths } from "./api/types";
|
|
42
44
|
|
|
43
45
|
const client = createClient<paths>({ baseUrl: "https://api.example.com/v1" });
|
|
44
46
|
|
|
@@ -85,7 +87,7 @@ Each call returns `Promise<FetchResponse>`, a discriminated union:
|
|
|
85
87
|
|
|
86
88
|
## API reference
|
|
87
89
|
|
|
88
|
-
The
|
|
90
|
+
The main entry point (`@shipworthy/openapi-fetch`) provides these exports.
|
|
89
91
|
|
|
90
92
|
### `createClient<Paths, Media>(options?)` (default and named export)
|
|
91
93
|
|
|
@@ -111,16 +113,17 @@ function createClient<Paths extends {}, Media extends MediaType = MediaType>(
|
|
|
111
113
|
| `querySerializer` | `QuerySerializer \| QuerySerializerOptions` | form/explode for arrays, deepObject/explode for objects | Global query string serialization (see [Query serialization](#query-serialization)). |
|
|
112
114
|
| `bodySerializer` | `BodySerializer` | `defaultBodySerializer` | Global request body serializer. |
|
|
113
115
|
| `pathSerializer` | `PathSerializer` | `defaultPathSerializer` | Global path template serializer. |
|
|
116
|
+
| `caseMapping` | `CaseMapping` | none | Installs schema-based casing middleware for every request and parsed result. |
|
|
114
117
|
| `requestInitExt` | `Record<string, unknown>` | `undefined` | Passed as the second argument to `fetch`. It is only used on Node.js 18+ with undici (detected through `process.versions.undici`) and ignored elsewhere, including Bun. Example: an undici `dispatcher`. |
|
|
115
118
|
|
|
116
119
|
#### `Client<Paths, Media>`
|
|
117
120
|
|
|
118
|
-
| Member | Description
|
|
119
|
-
| ------------------------------------------------------------------------------- |
|
|
120
|
-
| `GET(url, init?)`, `PUT`, `POST`, `DELETE`, `OPTIONS`, `HEAD`, `PATCH`, `TRACE` | Call an endpoint. `url` must be a path in `Paths` that defines that method. Use the template form, such as `"/pets/{petId}"`, not a pre-filled URL.
|
|
121
|
-
| `request(method, url, init?)` | Same as above with a lowercase `HttpMethod` (`"get"`, `"post"`, ...). The method is uppercased at runtime.
|
|
122
|
-
| `use(...middleware)` | Register client-level middleware. Falsy entries are skipped.
|
|
123
|
-
| `eject(...middleware)` | Unregister middleware by reference.
|
|
121
|
+
| Member | Description |
|
|
122
|
+
| ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
|
+
| `GET(url, init?)`, `PUT`, `POST`, `DELETE`, `OPTIONS`, `HEAD`, `PATCH`, `TRACE` | Call an endpoint. `url` must be a path in `Paths` that defines that method. Use the template form, such as `"/pets/{petId}"`, not a pre-filled URL. |
|
|
124
|
+
| `request(method, url, init?)` | Same as above with a lowercase `HttpMethod` (`"get"`, `"post"`, ...). The method is uppercased at runtime. |
|
|
125
|
+
| `use(...middleware)` | Register client-level middleware. Falsy entries are skipped. Invalid objects throw `Error("Middleware must be an object with a supported hook")`. |
|
|
126
|
+
| `eject(...middleware)` | Unregister middleware by reference. |
|
|
124
127
|
|
|
125
128
|
The `init` argument is required when the operation has required parameters or a required body. Otherwise it is optional.
|
|
126
129
|
|
|
@@ -170,7 +173,7 @@ import createClient, {
|
|
|
170
173
|
createPathBasedClient,
|
|
171
174
|
wrapAsPathBasedClient,
|
|
172
175
|
} from "@shipworthy/openapi-fetch";
|
|
173
|
-
import type { paths } from "./api/
|
|
176
|
+
import type { paths } from "./api/types";
|
|
174
177
|
|
|
175
178
|
const api = createPathBasedClient<paths>({ baseUrl: "https://api.example.com/v1" });
|
|
176
179
|
const { data, error } = await api["/pets/{petId}"].GET({ params: { path: { petId: "123" } } });
|
|
@@ -209,7 +212,8 @@ client.use(auth);
|
|
|
209
212
|
// later: client.eject(auth);
|
|
210
213
|
```
|
|
211
214
|
|
|
212
|
-
A `Middleware`
|
|
215
|
+
A `Middleware` has at least one of `onRequestInit`, `onRequest`, `onError`, `onResponse`, or
|
|
216
|
+
`onResult`. The existing HTTP hooks receive `MiddlewareCallbackParams`:
|
|
213
217
|
|
|
214
218
|
| Field | Description |
|
|
215
219
|
| ------------ | ------------------------------------------------------------------------------------------------------------- |
|
|
@@ -223,6 +227,10 @@ A `Middleware` is an object with at least one of `onRequest`, `onResponse`, or `
|
|
|
223
227
|
|
|
224
228
|
The order of the hooks and their valid return values:
|
|
225
229
|
|
|
230
|
+
- **`onRequestInit`** runs before path, query, header, cookie, and body serialization, in
|
|
231
|
+
registration order. It receives `method`, `schemaPath`, `{ params, body }` as `input`, and
|
|
232
|
+
`headers` merged from client, request, and parameter headers. Return a complete replacement
|
|
233
|
+
`{ params, body }`, or `undefined` to keep the input. Custom serializers receive the result.
|
|
226
234
|
- **`onRequest`** runs in registration order, client-level first and then per-request `middleware`.
|
|
227
235
|
- Return a `Request` to replace the request.
|
|
228
236
|
- Return a `Response` to skip `fetch`, the remaining `onRequest` hooks, and **all** `onResponse` hooks. The returned `Response` is used as-is: no `onResponse` hook runs for it, including hooks registered before the one that returned it. This matches upstream openapi-fetch.
|
|
@@ -237,6 +245,56 @@ The order of the hooks and their valid return values:
|
|
|
237
245
|
- Return a `Response` to replace it.
|
|
238
246
|
- Return `undefined` to keep it.
|
|
239
247
|
- Any other truthy value throws `Error("onResponse: must return new Response() when modifying the response")`.
|
|
248
|
+
- **`onResult`** runs after response parsing, in reverse order. It receives the parsed `result`,
|
|
249
|
+
original `method` and `schemaPath`, and the final `Response`. Return a `data` and/or `error`
|
|
250
|
+
update, or `undefined` to keep the parsed values. It also runs for a synthetic `Response`
|
|
251
|
+
returned by `onRequest`, a response recovered by `onError`, and empty bodies. It cannot replace
|
|
252
|
+
`result.response`.
|
|
253
|
+
|
|
254
|
+
### Schema-based case mapping
|
|
255
|
+
|
|
256
|
+
Pass a `caseMapping` to `createClient` to convert app-facing names in typed path, query,
|
|
257
|
+
header, and cookie parameters and JSON request bodies to the names declared by OpenAPI. It
|
|
258
|
+
converts parsed JSON success and error bodies back before returning `data` or `error`. The raw
|
|
259
|
+
`Response` and dictionary keys retain their wire spelling. Other response parsing modes leave
|
|
260
|
+
successful data unchanged.
|
|
261
|
+
|
|
262
|
+
Only fields declared by the selected schema are renamed. Undeclared fields keep their API
|
|
263
|
+
spelling, and an error response with no matching status schema or `default` response is returned
|
|
264
|
+
without conversion. String values that happen to name fields also keep their API spelling, for
|
|
265
|
+
example a JSON Pointer such as `errors[].pointer: "/slot_id"`.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import createClient from "@shipworthy/openapi-fetch";
|
|
269
|
+
import { caseMapping } from "./generated/case-mapping";
|
|
270
|
+
import type { paths } from "./generated/types";
|
|
271
|
+
|
|
272
|
+
const client = createClient<paths>({ baseUrl: "https://api.example.com", caseMapping });
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
For manual registration, import `createCaseMappingMiddleware` from
|
|
276
|
+
`@shipworthy/openapi-fetch/middleware/transform-casing` and call
|
|
277
|
+
`client.use(createCaseMappingMiddleware(caseMapping))`.
|
|
278
|
+
|
|
279
|
+
The mapping must come from the same OpenAPI document and camelCase naming rule as `paths`.
|
|
280
|
+
At build time, `buildCaseMapping(parsedService)` from
|
|
281
|
+
`@shipworthy/openapi-typescript/case-mapping` returns an
|
|
282
|
+
`Effect<CaseMapping, CaseMappingGenerationError>`. Include the resulting plain object in
|
|
283
|
+
generated code and pass it to the client:
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
import { buildCaseMapping } from "@shipworthy/openapi-typescript/case-mapping";
|
|
287
|
+
import { decode } from "@shipworthy/openapi-transformer";
|
|
288
|
+
import { Effect } from "effect";
|
|
289
|
+
|
|
290
|
+
const caseMapping = await Effect.runPromise(
|
|
291
|
+
decode(document).pipe(Effect.flatMap(buildCaseMapping)),
|
|
292
|
+
);
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`openapi generate tanstack-query --camel-case` generates the matching types and embeds this
|
|
296
|
+
mapping automatically. `openapi generate typescript --camel-case` without the `tanstack-query`
|
|
297
|
+
plugin generates types only; it does not install runtime conversion.
|
|
240
298
|
|
|
241
299
|
### Headers
|
|
242
300
|
|
|
@@ -482,20 +540,21 @@ function removeTrailingSlash(url: string): string; // removes one trailing "/"
|
|
|
482
540
|
|
|
483
541
|
Client and request types:
|
|
484
542
|
|
|
485
|
-
| Type
|
|
486
|
-
|
|
|
487
|
-
| `Client<Paths, Media>`
|
|
488
|
-
| `ClientOptions`
|
|
489
|
-
| `PathBasedClient<Paths, Media>`, `ClientForPath<PathInfo, Media>`
|
|
490
|
-
| `ClientMethod`, `ClientRequestMethod`
|
|
491
|
-
| `FetchOptions<Op>`, `RequestOptions<Op>`, `MaybeOptionalInit<PathItem, Method>`
|
|
492
|
-
| `ParamsOption<Op>`, `RequestBodyOption<Op>`, `DefaultParamsOption`
|
|
493
|
-
| `FetchResponse<Op, Init, Media>`
|
|
494
|
-
| `ParseAs`, `ParseAsResponse<T, Options>`
|
|
495
|
-
| `HeadersOptions`, `QuerySerializer<T>`, `QuerySerializerOptions`, `BodySerializer<T>`, `PathSerializer`
|
|
496
|
-
| `Middleware`, `MiddlewareCallbackParams`, `MiddlewareRequestParams`, `MergedOptions`
|
|
497
|
-
| `
|
|
498
|
-
| `
|
|
543
|
+
| Type | Purpose |
|
|
544
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
|
|
545
|
+
| `Client<Paths, Media>` | Return type of `createClient`. |
|
|
546
|
+
| `ClientOptions` | Options for `createClient`. |
|
|
547
|
+
| `PathBasedClient<Paths, Media>`, `ClientForPath<PathInfo, Media>` | Path-based client shapes. |
|
|
548
|
+
| `ClientMethod`, `ClientRequestMethod` | Signatures of `GET`/`POST`/... and of `request`. |
|
|
549
|
+
| `FetchOptions<Op>`, `RequestOptions<Op>`, `MaybeOptionalInit<PathItem, Method>` | Shape of the `init` argument for an operation. |
|
|
550
|
+
| `ParamsOption<Op>`, `RequestBodyOption<Op>`, `DefaultParamsOption` | The `params` and `body` parts of `init`. |
|
|
551
|
+
| `FetchResponse<Op, Init, Media>` | The `{ data, error, response }` union. |
|
|
552
|
+
| `ParseAs`, `ParseAsResponse<T, Options>` | `parseAs` values and the resulting `data` type. |
|
|
553
|
+
| `HeadersOptions`, `QuerySerializer<T>`, `QuerySerializerOptions`, `BodySerializer<T>`, `PathSerializer` | Option types. |
|
|
554
|
+
| `Middleware`, `MiddlewareCallbackParams`, `MiddlewareRequestInitParams`, `MiddlewareRequestInput`, `MiddlewareRequestParams`, `MiddlewareResult`, `MiddlewareResultParams`, `MergedOptions` | Middleware types. |
|
|
555
|
+
| `CaseMapping` (from `./middleware/transform-casing`) | Schema mapping accepted by `createCaseMappingMiddleware`. |
|
|
556
|
+
| `ClientPathsWithMethod<Client, Method>` | The paths of a client instance that support `Method`. |
|
|
557
|
+
| `MethodResponse<Client, Method, Path, Options?>` | The non-nullable `data` type for a call. |
|
|
499
558
|
|
|
500
559
|
Schema helpers:
|
|
501
560
|
|
|
@@ -557,7 +616,7 @@ Without the flag, the types are unchanged.
|
|
|
557
616
|
## Key facts (for AI agents)
|
|
558
617
|
|
|
559
618
|
- Import: `import createClient from "@shipworthy/openapi-fetch"`, a **default** export, also available as the named export `createClient`. Compiled ESM and CommonJS with `.d.ts`/`.d.cts` declarations and no dependencies.
|
|
560
|
-
- Typical setup: `createClient<paths>({ baseUrl })`, where `paths` comes from `openapi generate typescript <schema> -o
|
|
619
|
+
- Typical setup: `createClient<paths>({ baseUrl })`, where `paths` comes from `openapi generate typescript -i <schema> -o <dir>` (`<dir>/types.ts`).
|
|
561
620
|
- Call signature: `client.GET("/template/{param}", { params: { path, query, header, cookie }, body, headers, parseAs, signal })`. Methods are uppercase: `GET PUT POST DELETE OPTIONS HEAD PATCH TRACE`. `client.request("get", path, init)` is also available.
|
|
562
621
|
- Result: `{ data, error, response }`. The promise does not reject on 4XX/5XX. It rejects on network errors, invalid JSON in a successful response that has a `Content-Length`, and misbehaving middleware.
|
|
563
622
|
- Default serialization:
|
|
@@ -567,7 +626,8 @@ Without the flag, the types are unchanged.
|
|
|
567
626
|
- Query arrays repeat the key (`a=1&a=2`), and query objects use `deepObject` (`o[x]=1`).
|
|
568
627
|
- Header precedence: client `headers` < request `headers` < `params.header`. `null` deletes a header. `headers` accepts a plain object, a `Headers` instance, or `[name, value]` tuples.
|
|
569
628
|
- `params.cookie` is sent as a `Cookie` header (form/explode, URI-encoded), appended to any existing `Cookie` header.
|
|
570
|
-
- Middleware: `client.use({ onRequest, onResponse,
|
|
629
|
+
- Middleware: `client.use({ onRequestInit, onRequest, onError, onResponse, onResult })`.
|
|
630
|
+
Input and request hooks run in order; error, response, and parsed-result hooks run in reverse.
|
|
571
631
|
- Path-based alternative: `createPathBasedClient<paths>(opts)["/pets/{id}"].GET(init)`.
|
|
572
632
|
- Response data is not validated at runtime.
|
|
573
633
|
|