ty-fetch 0.0.2-beta.9 → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Alister Norris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,51 +1,91 @@
1
- # ty-fetch
1
+ <p align="center">
2
+ <img src="logo.png" alt="ty-fetch" width="350" />
3
+ </p>
2
4
 
3
- Automatic TypeScript types for any REST API. No codegen step, no manual types — just install, write fetch calls, and get full type safety from OpenAPI specs.
5
+ <h1 align="center">ty-fetch</h1>
4
6
 
5
- ```ts
6
- import tf from "ty-fetch";
7
+ [![npm version](https://img.shields.io/npm/v/ty-fetch.svg)](https://www.npmjs.com/package/ty-fetch)
8
+ [![license](https://img.shields.io/npm/l/ty-fetch.svg)](https://github.com/alnorris/ty-fetch/blob/main/LICENSE)
9
+ [![CI](https://github.com/alnorris/ty-fetch/actions/workflows/ci.yml/badge.svg)](https://github.com/alnorris/ty-fetch/actions/workflows/ci.yml)
10
+
11
+ **A tiny, zero-dependency HTTP client that auto-discovers your OpenAPI specs and types your API calls on-the-fly. No codegen, no build step — types generated by a TS plugin.**
12
+
13
+ ```bash
14
+ npm install ty-fetch
15
+ ```
16
+
17
+ ```jsonc
18
+ // tsconfig.json
19
+ {
20
+ "compilerOptions": {
21
+ "plugins": [{
22
+ "name": "ty-fetch/plugin",
23
+ // Optional — point to specs manually. Without this, ty-fetch
24
+ // auto-discovers specs at /openapi.json, /.well-known/openapi.yaml, etc.
25
+ "specs": {
26
+ "api.mycompany.com": "https://api.mycompany.com/docs/openapi.json"
27
+ }
28
+ }]
29
+ }
30
+ }
31
+ ```
7
32
 
8
- // Fully typed response — no codegen, no manual types
9
- const customers = await tf.get("https://api.stripe.com/v1/customers").json();
10
- customers.data // Customer[] — autocomplete works
33
+ ```ts
34
+ import ty from "ty-fetch";
35
+
36
+ // Fully typed — response, body, path params, query params, headers
37
+ const { data, error } = await ty.post("https://api.mycompany.com/v1/users/{team}/invite", {
38
+ params: {
39
+ path: { team: "engineering" },
40
+ query: { notify: true },
41
+ },
42
+ body: { email: "jane@example.com", role: "admin" },
43
+ headers: { "x-api-key": process.env.API_KEY },
44
+ });
11
45
 
12
- tf.get("https://api.stripe.com/v1/cutsomers");
13
- // ~~~~~~~~~~
14
- // Error: Path '/v1/cutsomers' does not exist in Stripe API.
15
- // Did you mean '/v1/customers'?
46
+ if (error) return console.error(error);
47
+ console.log(data.user.id); // fully typed, autocomplete works
16
48
  ```
17
49
 
18
- ## How is this different?
50
+ If your API serves an OpenAPI spec at `/openapi.json` (or any well-known path), ty-fetch finds it automatically. No config needed.
19
51
 
20
- Most OpenAPI tools require a **codegen step** — you run a command, it generates a client, you import from the generated code. When the spec changes, you regenerate.
52
+ ---
21
53
 
22
- ty-fetch has **no codegen**. The TypeScript language service plugin reads OpenAPI specs and generates typed overloads on-the-fly as you write code. Types appear instantly in your editor. You write plain fetch calls with real URLs.
54
+ ## 🤔 How does it work?
23
55
 
24
- ## Features
56
+ ty-fetch is a **TypeScript language service plugin**. When you write a `ty.get("https://...")` call:
25
57
 
26
- - **Zero codegen** types are generated on-the-fly by a TS plugin, not a build step
27
- - **Typed responses** — `.json()` returns the actual response type from the spec
28
- - **Typed request bodies**body params validated against the spec
29
- - **Typed path & query params** — `params.path` and `params.query` based on the endpoint
30
- - **Typed headers** — required headers (API keys, auth) from security schemes
31
- - **Path validation** — red squiggles for typos, with "did you mean?" suggestions
32
- - **Autocomplete** — URL path completions inside string literals, filtered by HTTP method
33
- - **Hover info** — hover over a URL to see available methods and descriptions
34
- - **JSDoc descriptions** — property descriptions from the spec show up in hover tooltips
35
- - **Auto-discovery** — probes well-known paths (`/openapi.json`, `/.well-known/openapi.yaml`, etc.) when no spec is configured
36
- - **YAML support** — specs can be JSON or YAML, local files or remote URLs
37
- - **Example inference** — generates types from response `example` when `schema` is missing
38
- - **On-demand** — only fetches specs and generates types for APIs you actually call
58
+ 1. 🔍 It extracts the domain from the URL
59
+ 2. 📡 Auto-discovers the OpenAPI spec (checks `/openapi.json`, `/.well-known/openapi.yaml`, etc.)
60
+ 3. 🏗️ Generates typed overloads on-the-fly response types, query params, headers, everything
61
+ 4. Validates your API paths and suggests corrections for typos
39
62
 
40
- Works as both a **TS language service plugin** (editor) and a **CLI** (CI).
63
+ Types appear in your editor instantly. When the spec changes, types update automatically. No build step ever.
41
64
 
42
- ## Setup
65
+ ### Compared to other tools
66
+
67
+ | | ty-fetch | [openapi-typescript](https://github.com/openapi-ts/openapi-typescript) | [openapi-fetch](https://github.com/openapi-ts/openapi-typescript/tree/main/packages/openapi-fetch) | [orval](https://github.com/orval-labs/orval) |
68
+ |---|---|---|---|---|
69
+ | **Codegen step** | None | `npx openapi-typescript ...` | Needs openapi-typescript first | `npx orval` |
70
+ | **Build step** | None | Required | Required | Required |
71
+ | **Generated files** | None | `.d.ts` files | `.d.ts` files | Full client |
72
+ | **Spec changes** | Auto-updates | Re-run codegen | Re-run codegen | Re-run codegen |
73
+ | **Auto-discovery** | Probes well-known paths | Manual | Manual | Manual |
74
+ | **Editor integration** | TS plugin (autocomplete, hover, diagnostics) | Types only | Types only | Types only |
75
+ | **Path validation** | Typo detection with suggestions | None | None | None |
76
+ | **Runtime** | Thin fetch wrapper (~100 LOC) | None (types only) | Fetch wrapper | Axios/fetch client |
77
+
78
+ ---
79
+
80
+ ## 📦 Quick start
81
+
82
+ ### 1. Install
43
83
 
44
84
  ```bash
45
85
  npm install ty-fetch
46
86
  ```
47
87
 
48
- Add the plugin to your `tsconfig.json`:
88
+ ### 2. Add the plugin to tsconfig.json
49
89
 
50
90
  ```jsonc
51
91
  {
@@ -55,78 +95,43 @@ Add the plugin to your `tsconfig.json`:
55
95
  }
56
96
  ```
57
97
 
58
- In VS Code, use the workspace TypeScript version: command palette > **TypeScript: Select TypeScript Version** > **Use Workspace Version**.
59
-
60
- That's it. Start writing `tf.get("https://...")` and types appear automatically.
61
-
62
- ## Usage
98
+ ### 3. Start fetching
63
99
 
64
100
  ```ts
65
- import tf from "ty-fetch";
66
-
67
- // GET with typed response
68
- const customers = await tf.get("https://api.stripe.com/v1/customers").json();
69
-
70
- // POST with typed body
71
- const customer = await tf.post("https://api.stripe.com/v1/customers", {
72
- body: { name: "Jane Doe", email: "jane@example.com" },
73
- }).json();
101
+ import ty from "ty-fetch";
74
102
 
75
- // Path params
76
- const repo = await tf.get("https://api.github.com/repos/{owner}/{repo}", {
77
- params: { path: { owner: "anthropics", repo: "claude-code" } },
78
- }).json();
79
-
80
- // Query params
81
- const pets = await tf.get("https://petstore3.swagger.io/api/v3/pet/findByStatus", {
82
- params: { query: { status: "available" } },
83
- }).json();
84
-
85
- // Headers (typed from security schemes)
86
- const data = await tf.get("https://api.scrapecreators.com/v1/reddit/search", {
87
- params: { query: { query: "typescript" } },
88
- headers: { "x-api-key": process.env.API_KEY },
89
- }).json();
103
+ // If your API has a spec at /openapi.json — types just work
104
+ const { data, error } = await ty.get("https://api.mycompany.com/v1/users");
90
105
  ```
91
106
 
92
- Response methods:
107
+ That's it. ✨
93
108
 
94
- | Method | Returns |
95
- |---|---|
96
- | `.json()` | `Promise<T>` (typed from spec) |
97
- | `.text()` | `Promise<string>` |
98
- | `.blob()` | `Promise<Blob>` |
99
- | `.arrayBuffer()` | `Promise<ArrayBuffer>` |
100
- | `await` directly | `T` (same as `.json()`) |
109
+ > **VS Code users:** Make sure you're using the workspace TypeScript version, not the built-in one. Command Palette → **TypeScript: Select TypeScript Version** → **Use Workspace Version**
101
110
 
102
- ## Spec configuration
111
+ Want to try it without setting up a project? Check out the [playground](./playground/).
103
112
 
104
- ### Built-in specs (no config needed)
113
+ ---
105
114
 
106
- These APIs are typed out of the box:
115
+ ## 🔍 Spec discovery
107
116
 
108
- | Domain | API |
109
- |---|---|
110
- | `api.stripe.com` | Stripe API |
111
- | `api.github.com` | GitHub REST API |
112
- | `petstore3.swagger.io` | Swagger Petstore |
117
+ ### Auto-discovery (zero config)
113
118
 
114
- ### Auto-discovery
119
+ When you call `ty.get("https://api.example.com/...")`, ty-fetch automatically probes the domain for an OpenAPI spec at these well-known paths:
115
120
 
116
- For unknown domains, ty-fetch probes well-known paths automatically:
121
+ ```
122
+ /.well-known/openapi.json /.well-known/openapi.yaml
123
+ /openapi.json /openapi.yaml
124
+ /api/openapi.json /docs/openapi.json
125
+ /swagger.json /api-docs/openapi.json
126
+ ```
117
127
 
118
- - `/.well-known/openapi.json`, `.yaml`, `.yml`
119
- - `/openapi.json`, `.yaml`, `.yml`
120
- - `/api/openapi.json`, `.yaml`
121
- - `/docs/openapi.json`, `.yaml`
122
- - `/swagger.json`
123
- - `/api-docs/openapi.json`
128
+ If any path returns a valid OpenAPI spec, types are generated automatically.
124
129
 
125
- If any of these return a valid OpenAPI spec, types are generated automatically.
130
+ **This means if your internal API serves a spec, ty-fetch will find it with zero configuration.**
126
131
 
127
- ### Custom specs
132
+ ### Point to specific specs
128
133
 
129
- Map domains to local files or remote URLs in your tsconfig:
134
+ For APIs that don't serve specs at standard paths, or for local spec files:
130
135
 
131
136
  ```jsonc
132
137
  {
@@ -135,7 +140,13 @@ Map domains to local files or remote URLs in your tsconfig:
135
140
  {
136
141
  "name": "ty-fetch/plugin",
137
142
  "specs": {
138
- "api.internal.company.com": "./specs/internal-api.yaml",
143
+ // Remote spec URL
144
+ "api.mycompany.com": "https://api.mycompany.com/docs/v2/openapi.json",
145
+
146
+ // Local file (resolved relative to tsconfig)
147
+ "payments.internal.com": "./specs/payments.yaml",
148
+
149
+ // Third-party API
139
150
  "api.partner.com": "https://partner.com/openapi.json"
140
151
  }
141
152
  }
@@ -144,14 +155,218 @@ Map domains to local files or remote URLs in your tsconfig:
144
155
  }
145
156
  ```
146
157
 
147
- - File paths are resolved relative to the tsconfig directory
148
- - Supports JSON and YAML specs
149
- - Custom specs override built-in defaults for the same domain
150
- - Works in both the editor plugin and the CLI
158
+ Supported spec formats:
159
+
160
+ | Format | Versions |
161
+ |---|---|
162
+ | **OpenAPI** | 3.0, 3.1 |
163
+ | **Swagger** | 2.0 |
164
+ | **File types** | JSON, YAML |
165
+ | **Sources** | Local files, remote URLs, auto-discovered |
166
+
167
+ Custom specs override auto-discovery for the same domain.
168
+
169
+ ---
170
+
171
+ ## 📖 API Reference
172
+
173
+ ### `import ty from "ty-fetch"`
174
+
175
+ The default export is a pre-configured `TyFetch` instance.
176
+
177
+ ### HTTP Methods
178
+
179
+ ```ts
180
+ ty.get(url, options?) // GET
181
+ ty.post(url, options?) // POST
182
+ ty.put(url, options?) // PUT
183
+ ty.patch(url, options?) // PATCH
184
+ ty.delete(url, options?) // DELETE
185
+ ty.head(url, options?) // HEAD
186
+ ty(url, options?) // Custom method (set options.method)
187
+ ```
188
+
189
+ All methods return `Promise<{ data, error, response }>`.
190
+
191
+ When the plugin is active, the `url` parameter and all options are **typed from the OpenAPI spec**. Without the plugin, everything still works — just untyped.
192
+
193
+ ### Response Shape
194
+
195
+ Every method returns `{ data, error, response }`:
196
+
197
+ ```ts
198
+ const { data, error, response } = await ty.get("https://api.example.com/v1/users");
199
+
200
+ if (error) {
201
+ // error is the parsed error body (typed if spec defines error responses)
202
+ console.error(error.message);
203
+ console.log(response.status); // raw Response always available
204
+ return;
205
+ }
206
+
207
+ // data is the parsed response body — fully typed from the spec
208
+ console.log(data.users);
209
+ ```
210
+
211
+ - **`data`** — parsed response body (`undefined` if error). JSON is auto-parsed, otherwise text.
212
+ - **`error`** — parsed error body on non-2xx responses (`undefined` if success)
213
+ - **`response`** — the raw `Response` object (always present)
214
+
215
+ No `.json()` call needed — responses are parsed automatically.
216
+
217
+ ### Options
218
+
219
+ ```ts
220
+ ty.post("https://api.example.com/v1/users/{team}/invite", {
221
+ // Path params — replaces {placeholders} in the URL
222
+ params: {
223
+ path: { team: "engineering" },
224
+ query: { notify: true, role: "admin" },
225
+ },
226
+
227
+ // JSON request body (auto-serialized, Content-Type set automatically)
228
+ body: { email: "jane@example.com", name: "Jane Doe" },
229
+
230
+ // Headers (typed from security schemes when plugin is active)
231
+ headers: { "x-api-key": "sk_live_..." },
232
+
233
+ // Prefix URL — prepended to the url argument
234
+ prefixUrl: "https://api.example.com",
235
+
236
+ // All standard fetch options are supported
237
+ signal: AbortSignal.timeout(5000),
238
+ cache: "no-store",
239
+ credentials: "include",
240
+ });
241
+ ```
242
+
243
+ | Option | Type | Description |
244
+ |---|---|---|
245
+ | `body` | `object` | JSON body — auto-serialized, `Content-Type: application/json` set |
246
+ | `params.path` | `object` | Replaces `{placeholder}` segments in the URL |
247
+ | `params.query` | `object` | Appended as `?key=value` query string |
248
+ | `headers` | `object` | HTTP headers (typed from spec security schemes) |
249
+ | `prefixUrl` | `string` | Prepended to the URL (useful with `create`/`extend`) |
250
+
251
+ Plus all standard [`RequestInit`](https://developer.mozilla.org/en-US/docs/Web/API/RequestInit) options (`signal`, `cache`, `credentials`, `mode`, etc.)
252
+
253
+ ### Creating Instances
254
+
255
+ ```ts
256
+ // Create a pre-configured instance
257
+ const api = ty.create({
258
+ prefixUrl: "https://api.mycompany.com",
259
+ headers: { "x-api-key": process.env.API_KEY },
260
+ });
261
+
262
+ // Now use short paths
263
+ const { data } = await api.get("/v1/users");
264
+ const { data: user } = await api.post("/v1/users", {
265
+ body: { name: "Jane" },
266
+ });
267
+
268
+ // Extend an existing instance (merges options)
269
+ const adminApi = api.extend({
270
+ headers: { "x-admin-token": process.env.ADMIN_TOKEN },
271
+ });
272
+ ```
273
+
274
+ ### Middleware
275
+
276
+ Add middleware to intercept requests and responses:
277
+
278
+ ```ts
279
+ import ty from "ty-fetch";
280
+
281
+ // Add auth header to every request
282
+ ty.use({
283
+ onRequest(request) {
284
+ request.headers.set("Authorization", `Bearer ${getToken()}`);
285
+ return request;
286
+ },
287
+ });
288
+
289
+ // Log all responses
290
+ ty.use({
291
+ onResponse(response) {
292
+ console.log(`${response.status} ${response.url}`);
293
+ return response;
294
+ },
295
+ });
296
+
297
+ // Retry on 401
298
+ ty.use({
299
+ async onResponse(response) {
300
+ if (response.status === 401) {
301
+ await refreshToken();
302
+ return fetch(response.url, { headers: { Authorization: `Bearer ${getToken()}` } });
303
+ }
304
+ return response;
305
+ },
306
+ });
307
+ ```
308
+
309
+ | Hook | Signature | Description |
310
+ |---|---|---|
311
+ | `onRequest` | `(request: Request) => Request \| RequestInit \| void` | Modify the request before it's sent |
312
+ | `onResponse` | `(response: Response) => Response \| void` | Modify or replace the response |
313
+
314
+ Both hooks can be async. Middleware runs in the order it's added.
315
+
316
+ ### Streaming
317
+
318
+ Stream SSE (Server-Sent Events), NDJSON, or raw text responses:
319
+
320
+ ```ts
321
+ // Server-Sent Events (e.g. OpenAI, Anthropic streaming APIs)
322
+ for await (const event of ty.stream("https://api.example.com/v1/chat", {
323
+ method: "POST",
324
+ body: { prompt: "Hello", stream: true },
325
+ })) {
326
+ console.log(event); // each parsed SSE event
327
+ }
328
+
329
+ // NDJSON streaming
330
+ for await (const line of ty.stream("https://api.example.com/v1/logs")) {
331
+ console.log(line); // each parsed JSON line
332
+ }
333
+ ```
334
+
335
+ Auto-detects the format from `Content-Type`:
336
+ - `text/event-stream` → SSE (parses `data:` lines, stops at `[DONE]`)
337
+ - `application/x-ndjson` / `application/jsonl` → NDJSON (parses each line as JSON)
338
+ - Anything else → raw text chunks
339
+
340
+ ### Plugin Features (editor only)
151
341
 
152
- ## CLI
342
+ When the TS plugin is active, you get these extras on top of the runtime API:
343
+
344
+ | Feature | What it does |
345
+ |---|---|
346
+ | **Typed responses** | `data` is the actual response type from the spec, not `any` |
347
+ | **Typed body** | `body` option is validated against the spec's request body schema |
348
+ | **Typed query params** | `params.query` keys and types from the spec's parameter definitions |
349
+ | **Typed path params** | `params.path` keys from `{placeholder}` segments |
350
+ | **Typed headers** | Required headers from the spec's security schemes |
351
+ | **Path validation** | Red squiggles on invalid API paths with "did you mean?" |
352
+ | **Autocomplete** | URL completions inside string literals, filtered by HTTP method |
353
+ | **Hover docs** | Hover over a URL to see available methods and descriptions |
354
+ | **JSDoc** | Property descriptions from the spec appear in hover tooltips |
355
+ | **Example inference** | Types inferred from response `example` when `schema` is missing |
153
356
 
154
- Validate API calls in CI:
357
+ ---
358
+
359
+ ## 🖥️ CI / Type checking
360
+
361
+ ### Why not just `tsc`?
362
+
363
+ `tsc` doesn't run TypeScript language service plugins — it only sees the base `any` types. Your code will compile, but you won't get type errors for wrong API paths or mismatched params.
364
+
365
+ For CI, you have two options:
366
+
367
+ ### Option 1: ty-fetch CLI (recommended)
368
+
369
+ Validates API paths against OpenAPI specs — catches typos and invalid endpoints:
155
370
 
156
371
  ```bash
157
372
  npx ty-fetch # uses ./tsconfig.json
@@ -160,27 +375,106 @@ npx ty-fetch --verbose # show spec fetching details
160
375
  ```
161
376
 
162
377
  ```
163
- src/api.ts:21:11 - error TF99001: Path '/v1/cutsomers' does not exist in Stripe API. Did you mean '/v1/customers'?
378
+ src/api.ts:21:11 - error TF99001: Path '/v1/uusers' does not exist.
379
+ Did you mean '/v1/users'?
164
380
 
165
381
  1 error(s) found.
166
382
  ```
167
383
 
168
- ## How it works
384
+ Add to your CI pipeline:
385
+
386
+ ```yaml
387
+ # GitHub Actions
388
+ - run: npx ty-fetch tsconfig.json
389
+ ```
390
+
391
+ ### Option 2: ESLint plugin (coming soon)
392
+
393
+ If you prefer eslint over a separate CLI, you can use the ty-fetch eslint rule:
394
+
395
+ ```bash
396
+ npm install -D eslint-plugin-ty-fetch
397
+ ```
398
+
399
+ ```js
400
+ // eslint.config.mjs
401
+ import tyFetch from "eslint-plugin-ty-fetch";
402
+
403
+ export default [tyFetch.configs.recommended];
404
+ ```
405
+
406
+ This runs the same validation as the CLI but inside your existing eslint pipeline.
407
+
408
+ > **Note:** The eslint plugin is not yet published. For now, use the CLI.
409
+
410
+ ---
411
+
412
+ ## 🌍 Runtime compatibility
413
+
414
+ ty-fetch is a thin wrapper around the standard Fetch API. It works anywhere `fetch` is available:
415
+
416
+ | Runtime | Supported |
417
+ |---|---|
418
+ | **Node.js** | 18+ (native fetch) |
419
+ | **Bun** | ✅ |
420
+ | **Deno** | ✅ |
421
+ | **Browsers** | ✅ (all modern browsers) |
422
+ | **Cloudflare Workers** | ✅ |
169
423
 
170
- 1. The TS plugin intercepts the language service (`getSemanticDiagnostics`, `getCompletionsAtPosition`, `getQuickInfoAtPosition`)
171
- 2. Finds `tf.get()` / `tf.post()` / `fetch()` calls with string literal URLs
172
- 3. Extracts the domain and fetches the OpenAPI spec on-demand (cached after first fetch)
173
- 4. Validates paths against the spec, suggests corrections via Levenshtein distance
174
- 5. Generates typed overloads into `node_modules/ty-fetch/index.d.ts` via interface declaration merging — only for URLs actually used in your code
424
+ The TS plugin (type generation) runs in your editor's TypeScript server — it doesn't affect runtime behavior.
175
425
 
176
- Spec fetching is async. On first encounter, the plugin fires a background fetch and returns no extra diagnostics. When the spec arrives, `refreshDiagnostics()` re-checks the file. Same pattern as [graphqlsp](https://github.com/0no-co/graphqlsp).
426
+ ---
177
427
 
178
- Types are generated **only for endpoints you actually use** — not the entire spec. This keeps overload counts low and TypeScript fast.
428
+ ## FAQ
179
429
 
180
- ## Development
430
+ <details>
431
+ <summary><strong>Why do I see <code>any</code> types instead of typed responses?</strong></summary>
432
+
433
+ The TS plugin needs to be active. Check:
434
+ 1. Plugin is in `tsconfig.json` under `compilerOptions.plugins`
435
+ 2. In VS Code, you're using the **workspace** TypeScript version (not the built-in one)
436
+ 3. Restart the TS server after config changes (Command Palette → "TypeScript: Restart TS Server")
437
+ 4. The API's OpenAPI spec is reachable (try `curl https://your-api.com/openapi.json`)
438
+ </details>
439
+
440
+ <details>
441
+ <summary><strong>Why doesn't <code>tsc</code> catch type errors?</strong></summary>
442
+
443
+ `tsc` doesn't run language service plugins — it only sees the base `any` types. Use the ty-fetch CLI for CI validation: `npx ty-fetch tsconfig.json`
444
+ </details>
445
+
446
+ <details>
447
+ <summary><strong>Does ty-fetch work without the plugin?</strong></summary>
448
+
449
+ Yes. The runtime client works independently — you get `{ data, error, response }` back from every call. You just won't get typed responses or path validation. It's a perfectly usable HTTP client on its own.
450
+ </details>
451
+
452
+ <details>
453
+ <summary><strong>What if my API doesn't serve an OpenAPI spec?</strong></summary>
454
+
455
+ You can point to a local spec file in your tsconfig:
456
+ ```jsonc
457
+ "specs": { "api.mycompany.com": "./specs/my-api.yaml" }
458
+ ```
459
+ </details>
460
+
461
+ <details>
462
+ <summary><strong>How big is this package?</strong></summary>
463
+
464
+ The runtime client (`index.js`) is ~100 lines. The plugin code ships in `dist/` but only runs inside the TS server, not in your bundle.
465
+ </details>
466
+
467
+ ---
468
+
469
+ ## 🧪 Development
181
470
 
182
471
  ```bash
183
472
  npm run build # compile TypeScript
184
473
  npm run watch # compile in watch mode
185
- npm test # run unit tests (74 tests)
474
+ npm test # run unit tests (132 tests)
475
+ npm run check # lint with biome + eslint
186
476
  ```
477
+
478
+ ## License
479
+
480
+ MIT
package/base.d.ts CHANGED
@@ -17,29 +17,36 @@ export interface Options<
17
17
  prefixUrl?: string;
18
18
  }
19
19
 
20
- export interface ResponsePromise<T = unknown> extends PromiseLike<T> {
21
- json(): Promise<T>;
22
- text(): Promise<string>;
23
- blob(): Promise<Blob>;
24
- arrayBuffer(): Promise<ArrayBuffer>;
25
- formData(): Promise<FormData>;
20
+ export type FetchResult<TData = unknown, TError = unknown> =
21
+ | { data: TData; error: undefined; response: Response }
22
+ | { data: undefined; error: TError; response: Response };
23
+
24
+ export interface Middleware {
25
+ onRequest?: (request: Request) => Request | RequestInit | void | Promise<Request | RequestInit | void>;
26
+ onResponse?: (response: Response) => Response | void | Promise<Response | void>;
27
+ }
28
+
29
+ export interface StreamResult<T = unknown> extends AsyncIterable<T> {
30
+ response: Promise<Response>;
26
31
  }
27
32
 
28
33
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
34
  type BaseOptions = Options<any, Record<string, any>, Record<string, any>>;
30
35
 
31
36
  export interface TyFetch {
32
- (url: string, options?: BaseOptions): ResponsePromise<any>;
33
- get(url: string, options?: BaseOptions): ResponsePromise<any>;
34
- post(url: string, options?: BaseOptions): ResponsePromise<any>;
35
- put(url: string, options?: BaseOptions): ResponsePromise<any>;
36
- patch(url: string, options?: BaseOptions): ResponsePromise<any>;
37
- delete(url: string, options?: BaseOptions): ResponsePromise<any>;
38
- head(url: string, options?: BaseOptions): ResponsePromise<any>;
37
+ (url: string, options?: BaseOptions): Promise<FetchResult<any>>;
38
+ get(url: string, options?: BaseOptions): Promise<FetchResult<any>>;
39
+ post(url: string, options?: BaseOptions): Promise<FetchResult<any>>;
40
+ put(url: string, options?: BaseOptions): Promise<FetchResult<any>>;
41
+ patch(url: string, options?: BaseOptions): Promise<FetchResult<any>>;
42
+ delete(url: string, options?: BaseOptions): Promise<FetchResult<any>>;
43
+ head(url: string, options?: BaseOptions): Promise<FetchResult<any>>;
44
+ stream(url: string, options?: BaseOptions): StreamResult;
39
45
  create(defaults?: BaseOptions): TyFetch;
40
46
  extend(defaults?: BaseOptions): TyFetch;
47
+ use(middleware: Middleware): TyFetch;
41
48
  HTTPError: typeof HTTPError;
42
49
  }
43
50
 
44
- declare const tf: TyFetch;
45
- export default tf;
51
+ declare const ty: TyFetch;
52
+ export default ty;