api-invoke 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 Jorge Roa
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 ADDED
@@ -0,0 +1,428 @@
1
+ # api-invoke
2
+
3
+ **Call any REST or GraphQL API at runtime. No code generation. No build step.**
4
+
5
+ Think of it as **reflection for APIs** — discover and invoke any API's operations at runtime, whether from a full spec, a GraphQL endpoint, or just a URL, no code generation required.
6
+
7
+ Give it an OpenAPI spec (v2 or v3), a GraphQL endpoint, a raw URL, or a manually defined endpoint — `api-invoke` parses it into a uniform interface, handles authentication, builds requests, classifies errors, and executes calls. Works in Node.js and the browser.
8
+
9
+ ```typescript
10
+ import { createClient } from 'api-invoke'
11
+
12
+ // Point at any OpenAPI spec → ready to call
13
+ const github = await createClient('https://api.github.com/openapi.json')
14
+ const stripe = await createClient('https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json')
15
+
16
+ // GraphQL endpoint → introspects schema, one operation per field
17
+ const countries = await createClient('https://countries.trevorblades.com/graphql')
18
+
19
+ // Or just a URL — no spec needed
20
+ const weather = await createClient('https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.01')
21
+ ```
22
+
23
+ ## Why
24
+
25
+ Most API clients are generated at build time from a spec — you run a CLI, it spits out typed functions, and you commit the result. This works well when you know your APIs ahead of time.
26
+
27
+ But some applications need to connect to APIs they've never seen before:
28
+
29
+ - **AI agents** that discover and call APIs on behalf of users
30
+ - **API explorers and testing tools** that load any spec and let users make live calls
31
+ - **Integration platforms** that connect to customer-provided endpoints at runtime
32
+ - **Internal tooling** that needs to hit dozens of microservices without maintaining a client for each
33
+
34
+ For these cases, you need a library that can take a spec (or just a URL), understand what operations are available, and execute them — all at runtime, with no code generation step.
35
+
36
+ `api-invoke` does exactly that. It parses OpenAPI 2 (Swagger), OpenAPI 3, and GraphQL schemas into a spec-agnostic intermediate representation, then executes operations against the live API with built-in auth injection, error classification, middleware, and CORS handling.
37
+
38
+ ## How It Compares
39
+
40
+ There are many ways to call APIs from JavaScript — from raw HTTP clients to full code generators. Here's how `api-invoke` fits into the landscape.
41
+
42
+ ### Feature comparison
43
+
44
+ | Feature | fetch / axios | got / ky | openapi‑generator | @hey‑api | openapi‑fetch | swagger‑client | openapi‑client‑axios | **api‑invoke** |
45
+ |:--------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
46
+ | No build step required | ✅ | ✅ | ❌ | ❌ | partial ¹ | ✅ | ✅ | ✅ |
47
+ | Parses OpenAPI 3 | — | — | build | build | build ¹ | ✅ | ✅ | ✅ |
48
+ | Parses Swagger 2 | — | — | build | build | ❌ | ✅ | ❌ | ✅ |
49
+ | GraphQL introspection | — | — | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
50
+ | Works without any spec | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
51
+ | Auto-detects input type | — | — | — | — | — | ❌ | ❌ | ✅ |
52
+ | Operation discovery | ❌ | ❌ | static | static | static | ✅ | ✅ | ✅ |
53
+ | Auth injection from spec | ❌ | ❌ | generated | generated | ❌ | ✅ | ❌ | ✅ |
54
+ | Error classification | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
55
+ | Built-in retry | ❌ | ✅ | — | — | ❌ | ❌ | ❌ | ✅ |
56
+ | CORS detection + proxy | ❌ | N/A | — | — | ❌ | ❌ | ❌ | ✅ |
57
+ | Middleware / hooks | interceptors | hooks | — | — | middleware | interceptors | interceptors | ✅ |
58
+ | Manual API definitions | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
59
+ | SSE streaming | manual ⁴ | manual ⁴ | ❌ | manual ⁴ | manual ⁴ | ❌ | ❌ | ✅ |
60
+ | Browser + Node.js | ✅ | partial ² | N/A | N/A | ✅ | ✅ | ✅ | ✅ |
61
+ | Static TypeScript types | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | runtime ³ |
62
+
63
+ <sup>¹ openapi-fetch makes runtime fetch calls, but requires running openapi-typescript at build time to generate types.</sup><br/>
64
+ <sup>² got is Node-only; ky is browser-first. Neither works universally out of the box.</sup><br/>
65
+ <sup>³ api-invoke provides TypeScript types for its own API (ParsedAPI, Operation, etc.) but does not generate per-endpoint types from specs. If you know your APIs at build time, code generators give you better IntelliSense.</sup><br/>
66
+ <sup>⁴ Raw stream access is available, but you must bring your own SSE parser (e.g. eventsource-parser). api-invoke includes a built-in WHATWG-compliant SSE parser returning `AsyncIterable<SSEEvent>`.</sup>
67
+
68
+ ### Positioning
69
+
70
+ <p align="center">
71
+ <img src="assets/positioning.svg" alt="Where api-invoke fits — quadrant chart showing spec awareness vs runtime/build-time" width="680"/>
72
+ </p>
73
+
74
+ `api-invoke` is the only tool that works across the entire top row — from raw URLs with no spec to full OpenAPI and GraphQL parsing, all at runtime.
75
+
76
+ ### Trade-offs
77
+
78
+ We believe in being upfront about where other tools are the better choice:
79
+
80
+ - **Static types** — Code generators like [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts) and [orval](https://orval.dev/) give you full IntelliSense with endpoint-specific types. If you know your APIs at build time, they provide better TypeScript DX. `api-invoke` trades compile-time type safety for runtime flexibility.
81
+ - **OAuth flows** — `api-invoke` intentionally accepts pre-obtained tokens — it doesn't implement auth code, device, or client-credentials flows. Platforms like [Composio](https://composio.dev/) and [Superface](https://superface.ai/) handle the full OAuth lifecycle.
82
+ - **Streaming** — `api-invoke` supports SSE (Server-Sent Events) streaming, which covers most LLM APIs. Raw chunked transfer or WebSocket streaming is not yet supported.
83
+ - **Language support** — [openapi-generator](https://openapi-generator.tech/) covers 40+ languages. `api-invoke` is JavaScript/TypeScript only.
84
+ - **Managed platforms** — Tools like [Superface OneSDK](https://superface.ai/) and [Composio](https://composio.dev/) solve a different problem: managed integration platforms with pre-built connectors, auth management, and monitoring. `api-invoke` is a library you embed in your own code.
85
+
86
+ ## Install
87
+
88
+ ```bash
89
+ npm install api-invoke
90
+ ```
91
+
92
+ ## Quick Start
93
+
94
+ ### From an OpenAPI spec
95
+
96
+ ```typescript
97
+ import { createClient } from 'api-invoke'
98
+
99
+ const spotify = await createClient(
100
+ 'https://developer.spotify.com/_data/documentation/web-api/reference/open-api-schema.yml'
101
+ )
102
+
103
+ // Discover all available operations
104
+ console.log(spotify.operations.map(op => `${op.method} ${op.path}`))
105
+ // ['GET /albums/{id}', 'GET /artists/{id}', 'GET /search', ...]
106
+
107
+ // Authenticate and execute
108
+ spotify.setAuth({ type: 'bearer', token: process.env.SPOTIFY_TOKEN })
109
+
110
+ const result = await spotify.execute('search', {
111
+ q: 'kind of blue',
112
+ type: 'album',
113
+ limit: 5,
114
+ })
115
+ console.log(result.status) // 200
116
+ console.log(result.data) // { albums: { items: [...] } }
117
+ ```
118
+
119
+ ### From a raw URL (no spec)
120
+
121
+ No spec? No problem. Pass any URL and `api-invoke` creates a single `query` operation. Query parameters in the URL become configurable operation parameters with their original values as defaults.
122
+
123
+ ```typescript
124
+ const weather = await createClient(
125
+ 'https://api.open-meteo.com/v1/forecast?latitude=48.85&longitude=2.35&current_weather=true'
126
+ )
127
+
128
+ // Execute with defaults from the URL
129
+ const paris = await weather.execute('query')
130
+ console.log(paris.data) // { current_weather: { temperature: 18.2, ... } }
131
+
132
+ // Override parameters
133
+ const tokyo = await weather.execute('query', {
134
+ latitude: 35.68,
135
+ longitude: 139.69,
136
+ })
137
+ ```
138
+
139
+ ### From a GraphQL endpoint
140
+
141
+ Point at any GraphQL endpoint — `api-invoke` runs an introspection query and creates one operation per query/mutation field. Arguments become typed parameters, and queries are auto-generated with depth-limited selection sets.
142
+
143
+ ```typescript
144
+ const countries = await createClient('https://countries.trevorblades.com/graphql')
145
+
146
+ // Each GraphQL field becomes an operation
147
+ console.log(countries.operations.map(op => op.id))
148
+ // ['continents', 'continent', 'countries', 'country', 'languages', 'language']
149
+
150
+ const result = await countries.execute('country', { code: 'BR' })
151
+ console.log(result.data) // { data: { country: { name: 'Brazil', capital: 'Brasília', ... } } }
152
+ ```
153
+
154
+ GraphQL returns HTTP 200 even on errors. Use the error helpers to check:
155
+
156
+ ```typescript
157
+ import { throwOnGraphQLErrors, getGraphQLErrors } from 'api-invoke'
158
+
159
+ const result = await countries.execute('country', { code: 'INVALID' })
160
+ throwOnGraphQLErrors(result) // throws ApiInvokeError for total failures (no data)
161
+
162
+ // For partial errors (data + errors both present), inspect manually:
163
+ const errors = getGraphQLErrors(result)
164
+ if (errors.length > 0) console.warn('Partial errors:', errors)
165
+ ```
166
+
167
+ ### With authentication
168
+
169
+ ```typescript
170
+ const client = await createClient('https://api.stripe.com/openapi/spec3.json')
171
+
172
+ // Bearer token
173
+ client.setAuth({ type: 'bearer', token: 'sk_live_...' })
174
+
175
+ // API key (header or query)
176
+ client.setAuth({ type: 'apiKey', location: 'header', name: 'X-API-Key', value: 'secret' })
177
+
178
+ // Basic auth
179
+ client.setAuth({ type: 'basic', username: 'user', password: 'pass' })
180
+
181
+ const result = await client.execute('listCustomers', { limit: 10 })
182
+ ```
183
+
184
+ ## Examples
185
+
186
+ The [`examples/`](./examples) folder has runnable scripts demonstrating each feature:
187
+
188
+ | Example | What it shows |
189
+ |---------|--------------|
190
+ | [`01-quick-start.ts`](./examples/01-quick-start.ts) | Parse a spec, discover operations, execute a call |
191
+ | [`02-raw-url.ts`](./examples/02-raw-url.ts) | Call any URL with no spec |
192
+ | [`03-parser-executor.ts`](./examples/03-parser-executor.ts) | Use parser and executor separately |
193
+ | [`04-discover-operations.ts`](./examples/04-discover-operations.ts) | Browse operations, parameters, and auth schemes |
194
+ | [`05-authentication.ts`](./examples/05-authentication.ts) | Full auth lifecycle: Bearer, Basic, API key, OAuth2, Cookie |
195
+ | [`06-error-handling.ts`](./examples/06-error-handling.ts) | Error classification and non-throwing mode |
196
+ | [`07-middleware.ts`](./examples/07-middleware.ts) | Retry, logging, and custom middleware |
197
+ | [`08-streaming.ts`](./examples/08-streaming.ts) | Stream real-time SSE events (Wikimedia live edits) |
198
+ | [`browser/index.html`](./examples/browser/index.html) | Browser usage with CORS proxy |
199
+
200
+ ```bash
201
+ npm run build && npx tsx examples/01-quick-start.ts
202
+ ```
203
+
204
+ ## Three Tiers of Usage
205
+
206
+ ### Tier 1: High-level client (recommended)
207
+
208
+ `createClient` auto-detects the input type (spec URL, GraphQL endpoint, raw URL, or spec object) and returns a ready-to-use client.
209
+
210
+ ```typescript
211
+ const client = await createClient('https://api.github.com/openapi.json')
212
+ const repos = await client.execute('listRepos', { per_page: 5 })
213
+ ```
214
+
215
+ ### Tier 2: Parser + executor (more control)
216
+
217
+ Use the parser and executor separately when you need to inspect or transform the parsed API before executing.
218
+
219
+ ```typescript
220
+ import { parseOpenAPISpec, executeOperation } from 'api-invoke'
221
+
222
+ const api = await parseOpenAPISpec(specObject)
223
+
224
+ // Inspect operations, filter, transform...
225
+ const op = api.operations.find(o => o.id === 'getAlbum')!
226
+
227
+ const result = await executeOperation(api.baseUrl, op, { id: '4aawyAB9vmqN3uQ7FjRGTy' })
228
+ ```
229
+
230
+ ### Tier 3: Raw execution (zero spec)
231
+
232
+ For one-off calls where you don't have or need a spec. Still gets error classification, response parsing, and timing.
233
+
234
+ ```typescript
235
+ import { executeRaw } from 'api-invoke'
236
+
237
+ const result = await executeRaw('https://api.spacexdata.com/v4/launches/latest')
238
+ console.log(result.data) // { name: 'Crew-9', ... }
239
+ console.log(result.elapsedMs) // 142
240
+ ```
241
+
242
+ ## Streaming (SSE)
243
+
244
+ For APIs that return Server-Sent Events — LLM token streaming, live feeds, real-time notifications — use the streaming variants. They return an `AsyncIterable<SSEEvent>` you can consume with `for await`.
245
+
246
+ ### Client streaming
247
+
248
+ ```typescript
249
+ import { ApiInvokeClient, defineAPI } from 'api-invoke'
250
+
251
+ const api = defineAPI('Wikimedia EventStreams')
252
+ .baseUrl('https://stream.wikimedia.org')
253
+ .get('/v2/stream/recentchange', { id: 'recentChanges' })
254
+ .build()
255
+
256
+ const client = new ApiInvokeClient(api)
257
+ const result = await client.executeStream('recentChanges')
258
+
259
+ for await (const event of result.stream) {
260
+ const change = JSON.parse(event.data)
261
+ console.log(`[${change.wiki}] ${change.type}: "${change.title}" by ${change.user}`)
262
+ }
263
+ ```
264
+
265
+ ### Raw streaming (no spec)
266
+
267
+ ```typescript
268
+ import { executeRawStream } from 'api-invoke'
269
+
270
+ const result = await executeRawStream('https://api.openai.com/v1/chat/completions', {
271
+ body: JSON.stringify({ model: 'gpt-4o', messages: [{ role: 'user', content: 'Hi' }], stream: true }),
272
+ auth: { type: 'bearer', token: process.env.OPENAI_API_KEY! },
273
+ })
274
+
275
+ for await (const event of result.stream) {
276
+ if (event.data === '[DONE]') break
277
+ const chunk = JSON.parse(event.data)
278
+ process.stdout.write(chunk.choices[0]?.delta?.content ?? '')
279
+ }
280
+ ```
281
+
282
+ Each `SSEEvent` gives you the raw fields from the stream:
283
+
284
+ ```typescript
285
+ interface SSEEvent {
286
+ data: string // The data field (always present)
287
+ event?: string // Named event type
288
+ id?: string // Last event ID
289
+ retry?: number // Reconnection interval (ms)
290
+ }
291
+ ```
292
+
293
+ ## Middleware
294
+
295
+ Middleware hooks into the request/response lifecycle:
296
+
297
+ ```typescript
298
+ import { createClient, withRetry, corsProxy, logging } from 'api-invoke'
299
+
300
+ const client = await createClient(specUrl, {
301
+ middleware: [
302
+ withRetry({ maxRetries: 3 }),
303
+ corsProxy(),
304
+ logging({ logger: console.log }),
305
+ ],
306
+ })
307
+ ```
308
+
309
+ ### Built-in middleware
310
+
311
+ | Middleware | Description |
312
+ |---|---|
313
+ | `withRetry(options?)` | Exponential backoff with Retry-After support |
314
+ | `corsProxy(options?)` | Rewrite URLs through a CORS proxy |
315
+ | `logging(options?)` | Log requests, responses, and errors |
316
+
317
+ ### Custom middleware
318
+
319
+ ```typescript
320
+ import type { Middleware } from 'api-invoke'
321
+
322
+ const timing: Middleware = {
323
+ name: 'timing',
324
+ async onRequest(url, init) {
325
+ console.log(`-> ${init.method} ${url}`)
326
+ return { url, init }
327
+ },
328
+ async onResponse(response) {
329
+ console.log(`<- ${response.status}`)
330
+ return response
331
+ },
332
+ onError(error) {
333
+ console.error('Request failed:', error.message)
334
+ },
335
+ }
336
+ ```
337
+
338
+ ## Error Handling
339
+
340
+ Every error is an `ApiInvokeError` with a `kind` for programmatic handling, a human-readable `suggestion`, and a `retryable` flag:
341
+
342
+ ```typescript
343
+ import { ApiInvokeError, ErrorKind } from 'api-invoke'
344
+
345
+ try {
346
+ await client.execute('getUser', { id: '123' })
347
+ } catch (err) {
348
+ if (err instanceof ApiInvokeError) {
349
+ switch (err.kind) {
350
+ case ErrorKind.AUTH: // 401/403 — bad credentials or insufficient permissions
351
+ case ErrorKind.CORS: // Blocked by browser CORS policy
352
+ case ErrorKind.NETWORK: // Connection failed, DNS error, etc.
353
+ case ErrorKind.HTTP: // Other 4xx/5xx responses
354
+ case ErrorKind.RATE_LIMIT: // 429 — too many requests
355
+ case ErrorKind.TIMEOUT: // Request timed out
356
+ case ErrorKind.PARSE: // Response was not valid JSON
357
+ }
358
+ console.log(err.suggestion) // "Check your credentials. The server rejected your authentication."
359
+ console.log(err.retryable) // true for network/rate-limit/timeout, false for auth/cors/parse
360
+ }
361
+ }
362
+ ```
363
+
364
+ To get error responses as data instead of exceptions:
365
+
366
+ ```typescript
367
+ const result = await executeOperation(baseUrl, operation, args, {
368
+ throwOnHttpError: false,
369
+ })
370
+ // result.status may be 404, 500, etc. — data is still parsed
371
+ ```
372
+
373
+ ## Execution Result
374
+
375
+ Every execution returns an `ExecutionResult`:
376
+
377
+ ```typescript
378
+ interface ExecutionResult {
379
+ status: number // HTTP status code
380
+ data: unknown // Parsed response body
381
+ headers: Record<string, string> // Response headers
382
+ request: { method: string; url: string; headers: Record<string, string> } // What was sent
383
+ elapsedMs: number // Round-trip time in ms
384
+ }
385
+ ```
386
+
387
+ ## Types
388
+
389
+ The parsed API uses spec-agnostic types that work regardless of the source format:
390
+
391
+ ```typescript
392
+ import type {
393
+ ParsedAPI, // Parsed spec with operations, auth schemes, and metadata
394
+ Operation, // Single API operation (id, path, method, parameters, body)
395
+ Parameter, // Path, query, header, or cookie parameter with schema
396
+ RequestBody, // POST/PUT/PATCH body definition
397
+ Auth, // Authentication credentials (bearer, basic, apiKey, oauth2)
398
+ AuthScheme, // Auth scheme detected from the spec
399
+ ExecutionResult, // Response from an executed operation
400
+ SSEEvent, // Single event from an SSE stream
401
+ StreamingExecutionResult, // Response from a streaming execution
402
+ Middleware, // Request/response interceptor
403
+ Enricher, // Post-parse API transformer
404
+ } from 'api-invoke'
405
+ ```
406
+
407
+ ## Local Development
408
+
409
+ ```bash
410
+ git clone https://github.com/jorgeroa/api-invoke.git
411
+ cd api-invoke
412
+ pnpm install
413
+ pnpm build
414
+ pnpm test
415
+ ```
416
+
417
+ To test local changes in a project that depends on `api-invoke`:
418
+
419
+ ```bash
420
+ cd /path/to/your-project
421
+ pnpm link /path/to/api-invoke
422
+ ```
423
+
424
+ This creates a symlink — edits to `api-invoke` source are reflected instantly. Re-run `pnpm link` after every `pnpm install` in the consumer project.
425
+
426
+ ## License
427
+
428
+ MIT