Skip to content
Open
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
42 changes: 21 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ import { z } from 'zod';
const getUser = stitch({
path: 'https://demo.stitchapi.dev/users/{id}',
output: z.object({ id: z.number(), name: z.string() }),
unwrap: 'data',
pick: 'data',
});

const user = await getUser({ params: { id: 1 } }); // typed · validated
Expand All @@ -103,7 +103,7 @@ Keep the `fetch` or axios you already have — it's the adapter underneath. A st

## Motivation

In almost every project there's a `src/api/` folder of thin functions that fire an HTTP request and unwrap the response. Everything that actually makes an integration reliable — auth lifecycle, retries, rate limits, timeouts, response validation, drift detection, observability — gets re-implemented at every call site, and each wrapper rots independently. `fetch` hands back opaque bytes, and raw bytes aren't what application code (or an AI agent) needs; both want structured, validated, observable results.
In almost every project there's a `src/api/` folder of thin functions that fire an HTTP request and pull the payload out of the response. Everything that actually makes an integration reliable — auth lifecycle, retries, rate limits, timeouts, response validation, drift detection, observability — gets re-implemented at every call site, and each wrapper rots independently. `fetch` hands back opaque bytes, and raw bytes aren't what application code (or an AI agent) needs; both want structured, validated, observable results.

A stitch folds all of that back into the call:

Expand Down Expand Up @@ -199,7 +199,7 @@ Reach for the full set of knobs only when you need them — they default off:
const getUser = stitch({
path: 'https://demo.stitchapi.dev/users/{id}',
output: User, // a validator of your choice
unwrap: 'data',
pick: 'data',
retry: 3, // ≡ { attempts: 3 }
timeout: '5s', // ≡ { total: '5s' }
cache: '1m', // ≡ { ttl: '1m' }
Expand Down Expand Up @@ -227,12 +227,12 @@ const api = seam({
const listUsers = api.stitch({
path: '/users',
output: User.array(),
unwrap: 'data',
pick: 'data',
});
const getUser = api.stitch({
path: '/users/{id}',
output: User,
unwrap: 'data',
pick: 'data',
});
```

Expand All @@ -255,7 +255,7 @@ import { z } from 'zod';

const listOrders = stitch({
path: 'https://demo.stitchapi.dev/users/{id}/orders',
unwrap: 'data',
pick: 'data',
output: drift(
z.array(z.object({ id: z.number(), total: z.number().optional() })),
{
Expand All @@ -277,12 +277,12 @@ const listUsers = stitch({
baseUrl: 'https://demo.stitchapi.dev',
path: '/users',
retry: { attempts: 4, on: [429, 502, 503], respectRetryAfter: true },
throttle: { rate: '1/s', concurrency: 2, scope: 'host' },
throttle: { rate: '1/s', concurrency: 2, pool: 'host' },
timeout: { total: '30s', perAttempt: '10s' },
});
```

`throttle` is proactive (keeps you under a limit before it bites; `scope: 'host'` shares a limiter across stitches), `retry` is reactive (backoff + `Retry-After`), and `timeout` aborts with a real `AbortSignal`. Three more knobs round it out: **`circuit`** fast-fails a dependency that's already down, **`idempotency`** injects a stable `Idempotency-Key` on writes, and **`acceptStatus`** treats a non-2xx (e.g. `404`) as a normal result instead of a throw. Full guide: [Resilience](https://stitchapi.dev/docs/guides/resilience/retry).
`throttle` is proactive (keeps you under a limit before it bites; `pool: 'host'` shares a limiter across stitches), `retry` is reactive (backoff + `Retry-After`), and `timeout` aborts with a real `AbortSignal`. Three more knobs round it out: **`circuit`** fast-fails a dependency that's already down, **`idempotency`** injects a stable `Idempotency-Key` on writes, and **`acceptStatus`** treats a non-2xx (e.g. `404`) as a normal result instead of a throw. Full guide: [Resilience](https://stitchapi.dev/docs/guides/resilience/retry).

## Caching

Expand All @@ -292,19 +292,19 @@ A read-through response cache with in-process request coalescing — **off by de
const getUser = stitch({
path: 'https://demo.stitchapi.dev/users/{id}',
output: User,
unwrap: 'data',
pick: 'data',
cache: '5m',
});

const listAnnouncements = stitch({
path: 'https://demo.stitchapi.dev/announcements',
output: z.array(z.object({ id: z.number(), title: z.string() })),
unwrap: 'data',
pick: 'data',
cache: {
ttl: '1h',
scope: 'app',
vary: ['accept-language'],
maxEntries: 500,
entries: 500,
version: 1, // pins the shape — cacheable without a fingerprinter
},
});
Expand All @@ -331,16 +331,16 @@ const getUser = stitch({

A **surface** is the request _style_ a stitch speaks. `http` is the default; the rest are peer surfaces on the same engine — `auth`, `retry`, `throttle`, `timeout`, validation, and the event stream compose with every one. Each ships as its own subpath import, so `import { stitch }` pulls in `http` alone.

| Surface | Import | Shapes | `await` resolves to |
| ------------- | ----------------------------- | ------------------------------------------ | ------------------------------ |
| `http` | `stitch` (default) | a JSON-over-HTTP call | the validated body |
| `graphql` | `stitchapi/graphql` | POST `{ query, variables }`, unwrap `data` | the `data` payload |
| `sse` | `stitchapi/sse` | a `text/event-stream` reader (over fetch) | every parsed event, collected |
| `stream` | `stitchapi/stream` | a raw `ReadableStream` reader | every decoded chunk, collected |
| `download` | `stitchapi/download` | a buffered binary GET | `{ blob, filename }` |
| `llm` | `stitchapi/llm` | a chat-completion via a provider contract | the normalised `{ text, … }` |
| `shell` | `@stitchapi/shell` (peer pkg) | a local command, args + stdin | the command's stdout |
| `postmessage` | `stitchapi/postmessage` | a typed iframe ↔ parent RPC / event call | the typed RPC response |
| Surface | Import | Shapes | `await` resolves to |
| ------------- | ----------------------------- | ----------------------------------------- | ------------------------------ |
| `http` | `stitch` (default) | a JSON-over-HTTP call | the validated body |
| `graphql` | `stitchapi/graphql` | POST `{ query, variables }`, picks `data` | the `data` payload |
| `sse` | `stitchapi/sse` | a `text/event-stream` reader (over fetch) | every parsed event, collected |
| `stream` | `stitchapi/stream` | a raw `ReadableStream` reader | every decoded chunk, collected |
| `download` | `stitchapi/download` | a buffered binary GET | `{ blob, filename }` |
| `llm` | `stitchapi/llm` | a chat-completion via a provider contract | the normalised `{ text, … }` |
| `shell` | `@stitchapi/shell` (peer pkg) | a local command, args + stdin | the command's stdout |
| `postmessage` | `stitchapi/postmessage` | a typed iframe ↔ parent RPC / event call | the typed RPC response |

Full guide: [Surfaces](https://stitchapi.dev/docs/reference/surfaces).

Expand Down
6 changes: 3 additions & 3 deletions apps/docs/AUTHORING.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ const listUsers = stitch({
loginInput: () => ({
body: { email: env('APP_USER')(), password: env('APP_PASS')() },
}),
refreshOn: 401,
refresh: 401,
}),
});
```
Expand All @@ -117,8 +117,8 @@ const listUsers = stitch({

{/* Only the options that matter here, with defaults. Link to Reference for the
exhaustive table — never paste a full type table into a guide (rule 6). */}
`refreshOn` re-runs the login on a status code; `refreshWhen` re-runs it on a
content predicate (for soft 200 walls). See
`refresh` re-runs the login on a status code (bare value or `{ on }`); the
`{ when }` form re-runs it on a content predicate (for soft 200 walls). See
[Reference → Auth strategies](/docs/reference/auth-strategies) for every field.

<Callout type="warn">
Expand Down
14 changes: 7 additions & 7 deletions apps/docs/app/(home)/demo/scene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,22 +202,22 @@ const user = await getUser({
len: 4.8,
},
{
key: 'unwrap',
key: 'pick',
label: 'Data shaping',
filename: 'unwrap.ts',
// Mirrors the README hero example: unwrap peels the transport
filename: 'pick.ts',
// Mirrors the README hero example: pick peels the transport
// envelope before validation, so callers get the value itself.
code: `const getUser = stitch({
baseUrl: 'https://demo.stitchapi.dev',
path: '/users/{id}',
output: User,
unwrap: 'data', // peel the envelope
pick: 'data', // peel the envelope
});

const user = await getUser({
params: { id: '42' },
});`,
chip: `unwrap: 'data'`,
chip: `pick: 'data'`,
chipMono: true,
rows: [
{
Expand All @@ -233,7 +233,7 @@ const user = await getUser({
icon: Scissors,
tone: 'brand',
text: 'envelope peeled before validation',
meta: "unwrap: 'data'",
meta: "pick: 'data'",
},
{
at: 2.5,
Expand All @@ -243,7 +243,7 @@ const user = await getUser({
meta: 'clean shape',
},
],
doing: 'unwrapping',
doing: 'picking',
done: 'just the data',
doneAt: 3.4,
len: 5.0,
Expand Down
Loading
Loading