@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 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, its TypeScript generator, and the generator's peers as dev dependencies:
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 effect@^4
14
+ bun add -d @shipworthy/openapi-cli @shipworthy/openapi-typescript
15
15
  ```
16
16
 
17
- These packages are build-time tooling; the fetch client has no runtime or peer dependencies.
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 dependencies or peer dependencies.**
22
- - **ESM and CommonJS.** Published packages contain compiled JavaScript in both formats with matching `.d.ts` and `.d.cts` declarations in `dist/`; no TypeScript transpilation of `node_modules` is needed. Declarations type-check with TypeScript 5.0 and later.
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/schema.ts
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/schema";
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 package's single entry point (`@shipworthy/openapi-fetch`) provides these exports.
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. Throws `Error("Middleware must be an object with one of `onRequest()`, `onResponse() or `onError()`")` for anything else that is invalid. |
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/schema";
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` is an object with at least one of `onRequest`, `onResponse`, or `onError`. Each hook receives `MiddlewareCallbackParams`:
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 | Purpose |
486
- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
487
- | `Client<Paths, Media>` | Return type of `createClient`. |
488
- | `ClientOptions` | Options for `createClient`. |
489
- | `PathBasedClient<Paths, Media>`, `ClientForPath<PathInfo, Media>` | Path-based client shapes. |
490
- | `ClientMethod`, `ClientRequestMethod` | Signatures of `GET`/`POST`/... and of `request`. |
491
- | `FetchOptions<Op>`, `RequestOptions<Op>`, `MaybeOptionalInit<PathItem, Method>` | Shape of the `init` argument for an operation. |
492
- | `ParamsOption<Op>`, `RequestBodyOption<Op>`, `DefaultParamsOption` | The `params` and `body` parts of `init`. |
493
- | `FetchResponse<Op, Init, Media>` | The `{ data, error, response }` union. |
494
- | `ParseAs`, `ParseAsResponse<T, Options>` | `parseAs` values and the resulting `data` type. |
495
- | `HeadersOptions`, `QuerySerializer<T>`, `QuerySerializerOptions`, `BodySerializer<T>`, `PathSerializer` | Option types. |
496
- | `Middleware`, `MiddlewareCallbackParams`, `MiddlewareRequestParams`, `MergedOptions` | Middleware types. |
497
- | `ClientPathsWithMethod<Client, Method>` | The paths of a client instance that support `Method`. |
498
- | `MethodResponse<Client, Method, Path, Options?>` | The non-nullable `data` type for a call. |
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 schema.ts`.
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, onError })`. `onRequest` runs in order; `onResponse` and `onError` run in reverse.
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