Request surfaces
http, graphql, sse, stream, and download — the request styles a stitch can speak, each a subpath import riding one engine.
A surface is the request style a stitch speaks. http is the default — a
plain JSON-over-HTTP call, what most of these docs use. GraphQL, Server-Sent
Events, a raw byte stream, and a file download are peer surfaces: each shapes
and interprets its own request, but they all ride the same engine, so auth,
retry, throttle, timeout, validation, and the event
stream compose with every one.
A request surface is how a stitch shapes its request. Don't confuse it with the four invocation surfaces — function, CLI, HTTP, MCP — which are how you call a stitch. The two are orthogonal: any request surface is reachable through any invocation surface.
Every non-http surface ships as its own subpath import, so
import { stitch } from the root pulls in only the http engine — a surface's
code (and its parser/decoder) loads only when you import it.
| 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 } |
http — the default
stitch(...) with no kind is the http surface: send the request, parse JSON
when the response is JSON-ish, validate, return. Nothing to import.
import { stitch } from 'stitchapi';
const getUser = stitch('https://api.example.com/users/{id}');graphql
graphql() POSTs { query, variables } and unwraps data; a 200 carrying
errors[] is treated as a failure, never silently passed.
import { graphql } from 'stitchapi/graphql';
const getThing = graphql({
baseUrl: 'https://api.example.com',
query: 'query ($id: ID) { thing(id: $id) { name } }',
});
const thing = await getThing({ variables: { id: 1 } });sse
sse parses the text/event-stream wire format over fetch + Web Streams —
never EventSource, so it keeps your auth, headers, and retries. Each event
becomes a delta chunk; await collects them, .stream() yields them live.
import { sse } from 'stitchapi/sse';
const ticks = sse({ url: 'https://api.example.com/ticks' });
for await (const ev of ticks.stream()) {
if (ev.type === 'delta') handle(ev.chunk); // a parsed SseEvent
}data is JSON-parsed when it parses, else kept as the raw string:
Prop
Type
stream
stream is the raw streaming sibling — it hands back the response body decoded
per stream.decode: 'bytes' (the default, lossless Uint8Array chunks),
'lines' (UTF-8 lines), or 'ndjson' (a parsed value per line).
import { stream } from 'stitchapi/stream';
const logs = stream({
url: 'https://api.example.com/logs',
stream: { decode: 'ndjson' },
});
for await (const ev of logs.stream()) {
if (ev.type === 'delta') console.log(ev.chunk); // one parsed JSON value per line
}Prop
Type
Streaming members open on the same auth/retry/throttle spine, with one twist: opening a stream charges the rate limiter once but never holds a concurrency slot, so a long-lived connection can't pin a seam's concurrency budget.
Validating streamed values
A streaming surface has no single response body, so an output contract
validates each delta before it is emitted — never the collected array. A
value that fails a critical/schema check fails the stream and is never
delivered; a drift watch path warns and the value still flows, so a
.stream() consumer only ever sees values that passed the contract.
import { stream } from 'stitchapi/stream';
import { z } from 'zod';
const logs = stream({
url: 'https://api.example.com/logs',
stream: { decode: 'ndjson' },
output: z.object({ level: z.string(), msg: z.string() }), // checked per record
});For sse the contract describes the event's data payload, not the
{ event, data, id, retry } envelope. transform and unwrap reshape a whole
buffered body and aren't applied to the delta path, and drift snapshots are
a whole-body concept — not taken per-delta.
download
download fetches a file: a GET buffered into a Blob, with byte progress,
a Content-Disposition filename (with a URL-last-segment fallback), and a
per-call AbortSignal. It never writes to disk — saving the Blob is your
call, which keeps it browser-first.
import { download } from 'stitchapi/download';
const getReport = download({ url: 'https://api.example.com/report.pdf' });
const { blob, filename } = await getReport({
signal: controller.signal,
onProgress: (p) => console.log(p.loaded, '/', p.total),
});Prop
Type
Both spellings, one engine
Every surface has two co-existing spellings. The monomorphic helper above
(sse(...), download(...), …) is pre-bound to its surface and gives
surface-specific types. The generic form passes the surface as kind on the
core stitch, so a seam can mint members of any surface without forking:
import { seam, stitch } from 'stitchapi';
import { downloadSurface } from 'stitchapi/download';
const file = stitch({ kind: downloadSurface, url: 'https://x/report.pdf' });
const api = seam({ baseUrl: 'https://api.example.com' });
const member = api.stitch({ kind: downloadSurface, path: '/report.pdf' });Either way the surface is named in __config by a stable string id
('sse', 'download', …), so a stitch's declaration still round-trips to JSON
for MCP, the playground, and llms.txt — the behaviour hooks stay a runtime
detail. An id a consumer doesn't recognise degrades to "an http-shaped call of
unknown style", never a crash.