API stitching: turn any API into a typed, resilient function. Declare an endpoint once — its types, auth, and resilience — and call it like a local function. No server, no codegen, no config files. The same definition answers to your code, the CLI, and an AI agent alike.
📚 stitchapi.dev — documentation, guides & live playground
Zero runtime dependencies · ~22 kB min+gzip — a typical import { stitch } tree-shakes to ~18 kB, and with no transitive tree there is nothing else to install or audit. The size is an enforced budget in CI, not an aspiration.
Note
StitchAPI is at 1.0.0-rc.2. The core runtime is feature-complete, zero-dependency, covered by a green test gate, and already running in production in two projects. We're validating in the wild before stamping a stable 1.0.0 — pin an exact version and expect only small, documented changes. Feedback is very welcome.
- What is a stitch?
- Motivation
- Why StitchAPI
- What StitchAPI is not
- Features
- Install
- Quick start
- Composition: seam, extends,
.with() - Validation & leveled drift
- Resilience: retry, throttle, timeout
- Caching
- Auth as a boundary
- Surfaces: any request style
- Four front doors
- Agent-native
- Errors & pitfalls
- Zero-infra observability
- Packages
- Documentation
- Contributing
- License
A stitch is StitchAPI's core primitive: it takes a single endpoint and hands back a callable. You declare the contract once — input, output, auth, resilience — and call it like a local function. The same definition your code calls, the CLI runs and an AI agent invokes — and every caller gets a capability, not a credential. A service with more than one endpoint is a seam — a group of stitches that share a base URL, auth, and one runtime (throttle budget, store, trace sink).
import { stitch } from 'stitchapi';
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',
});
const user = await getUser({ params: { id: 1 } }); // typed · validatedKeep the fetch or axios you already have — it's the adapter underneath. A stitch sits above the transport and turns an endpoint into a function rather than replacing the call.
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.
A stitch folds all of that back into the call:
Around raw fetch, you hand-roll… |
…a stitch declares it once |
|---|---|
| Opaque bytes you parse and hope are the right shape | Schema-validated, typed results — drift caught on every call |
| A throw on the first failure, then it's on you | Retries with backoff + jitter, honoring Retry-After |
| One coarse timeout, if you remember it | Layered total / per-attempt timeouts with real aborts |
| No rate control — you meet the 429s in production | Proactive throttle: rate + concurrency caps, shared per host |
| A raw byte stream you frame and paginate yourself | SSE framing, delta concatenation, auto-pagination |
| Zero visibility into what the call did | A typed event stream + opt-in traces: latency, retries, drift |
There are plenty of ways to get a typed API client — spec-based generators, hand-authored contract clients, workflow platforms, or a folder of hand-rolled fetch wrappers. StitchAPI sits in a spot none of them cover: it turns one endpoint at a time into a resilient, validated, observable function — no spec, no codegen, no config files, no server; only explicit composition.
- Atomic, not spec-first. Spec-based generators (openapi-generator, Orval, Kubb, …) need a complete OpenAPI document first — and most real-world APIs (internal, undocumented, the long tail) never get one. A stitch needs a URL and one example response.
- A runtime, not a code generator. No generated SDK to commit, diff, and regenerate. The declaration is the client, validated on every live call — so a silently renamed field is a loud, leveled drift signal, not an
undefinedthree layers downstream. - Resilience is declared, not hand-rolled. Retries, throttling, timeouts, circuit breaking, pagination — configuration on the stitch, uniform across every integration.
- Auth is a boundary. A stitch owns its credential and lifecycle; callers get a capability, not the credential. That matters double when the caller is an AI agent.
- Agents are first-class callers. One definition is a function, a CLI command, an HTTP endpoint, and an MCP tool — returning structured, schema-validated, traceable results instead of opaque bytes.
- A library, not a platform. Zero runtime dependencies, embeds in your project, nothing to operate.
| Alternative | Needs | You maintain | StitchAPI instead |
|---|---|---|---|
| Spec-based codegen (openapi-generator, Orval, Kubb, …) | a complete OpenAPI spec | a generated SDK, regenerated on every API change | one endpoint at a time, validated live at runtime |
| Typed runtime clients (Zodios, ts-rest, …) | a hand-authored contract | your own retry / auth / rate-limit code around it | resilience, auth lifecycle, and drift detection built into the primitive |
| Workflow platforms (Windmill, n8n, …) | a server to deploy | flows inside someone else's runtime | a zero-dependency library; composition is just code |
Hand-rolled fetch wrappers |
nothing | bespoke auth, retries, limits — re-solved per project | the same concerns, declared once per stitch |
For the full competitive landscape and positioning, see the Overview.
Knowing what a tool refuses to be is how you trust what it is:
- Not an HTTP client or
fetchreplacement —fetch/axios are the substrate underneath; a stitch sits above the transport. - Not a code generator — no SDK to commit, diff, and regenerate; the declaration is the runtime, validated live.
- Not spec-first — no OpenAPI document required; a URL and one example response is enough.
- Not a server to deploy — a zero-dependency library you import, for the APIs you don't control.
- Not a workflow engine or iPaaS — no orchestration, queues, or visual builder; composition is plain TypeScript.
- No config files or hidden inheritance — nothing ambient a stitch silently reads.
No server, no codegen, no config files, no implicit inheritance — only explicit composition.
- One primitive, scoped to a surface —
stitch(url | config)returns a typed callable; a service with more than one endpoint is aseamthat shares base, auth, throttle budget, store, and trace sink across its members. - Event-stream core — every call yields a typed stream (
start → progress → drift → result → done);awaitis sugar that returns the final validated value. - Bring-your-own validation — Zod or any Standard Schema validator (Valibot, ArkType, …); types are inferred from the schemas.
- Leveled drift detection — live responses diffed against a committed contract snapshot; changes surface as
error/warn/infoinstead of a silentundefined. - Declared resilience — retry with backoff and
Retry-After, proactive throttle, layered timeouts, a circuit breaker, and idempotency keys. - Read-through caching — opt-in response cache + in-process coalescing, keyed by a derived, principal-scoped key, loaded lazily from
stitchapi/cache. - Auth as a boundary —
bearer,apiKey,basic,cookieSession(auto-login/re-login),oauth2; secrets resolve at call time and never reach the caller. - Any request style —
httpby default;graphql,sse,stream,download,llm,shell, andpostmessageare peer surfaces behind subpath imports. - Pluggable state store — throttle counters and sessions behind a 3-method store; swap in Redis/Postgres to go distributed.
- Zero-infra observability — tracing is off by default; opt in per stitch or via
STITCH_TRACE_*env vars. No collector, no dashboard. - Four front doors, one definition — in-process function, CLI (
stitch run), HTTP (stitch serve), and MCP (stitch mcp). - Zero runtime dependencies —
"dependencies": {}, built on globalfetch, tree-shakeable; ~22 kB min+gzip for the whole entry, ~18 kB for a typicalimport { stitch }.
npm install stitchapi # or: pnpm add stitchapi · yarn add stitchapiValidation is bring-your-own — pass a Zod schema or any Standard Schema validator; none is bundled. The examples below use Zod for familiarity, and hit the live demo API at demo.stitchapi.dev.
The smallest stitch is a URL — declare once, call many times:
import { stitch } from 'stitchapi';
const getUsers = stitch('https://demo.stitchapi.dev/users');
const users = await getUsers(); // GET, parsed JSONPath params use RFC 6570 URI templates; params, query, headers, and body all travel in one input object:
const getUser = stitch('https://demo.stitchapi.dev/users/{id}');
await getUser({ params: { id: 1 }, query: { expand: 'roles' } });
// → GET https://demo.stitchapi.dev/users/1?expand=rolesReach 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',
retry: 3, // ≡ { attempts: 3 }
timeout: '5s', // ≡ { total: '5s' }
cache: '1m', // ≡ { ttl: '1m' }
});
const user = await getUser({ params: { id: 42 } });
// → typed · validated · retried · cachedA stitch yields a typed event stream; await consumes it and returns the final value, while .safe() resolves to { ok, data, error } and .stream() yields progress, throttle waits, retries, and drift as they happen.
Everything reusable is a named value, and a stitch composes values — no global config is ever required. A service with more than one endpoint is a seam: declare the shared base, auth, and budget once; each endpoint is a member that inherits the config and shares one runtime (throttle bucket, store, trace sink):
import { seam } from 'stitchapi';
const api = seam({
baseUrl: 'https://demo.stitchapi.dev',
retry: { attempts: 3, on: [429, 503] },
timeout: { total: '30s' },
});
const listUsers = api.stitch({
path: '/users',
output: User.array(),
unwrap: 'data',
});
const getUser = api.stitch({
path: '/users/{id}',
output: User,
unwrap: 'data',
});For lighter reuse, extends: [fragment | stitch] merges config left→right (own fields win), and .with() pre-binds part of the input while reusing the same runtime. Full guide: Authoring.
Wrap an output schema in drift() and every live response is compared against the schema and a committed snapshot, with each difference classified by level:
| Level | Trigger | Behavior |
|---|---|---|
| error | a critical field went missing or changed type |
fails the call (error event) |
| warn | a watched / non-critical field changed | warn event; the call succeeds |
| info | a brand-new field appeared that the contract lacks | info event — “want to use it?” |
import { drift, stitch } from 'stitchapi';
import { z } from 'zod';
const listOrders = stitch({
path: 'https://demo.stitchapi.dev/users/{id}/orders',
unwrap: 'data',
output: drift(
z.array(z.object({ id: z.number(), total: z.number().optional() })),
{
critical: ['[].id'], // error when these break — you rely on them
watch: ['[].total'], // warn when this changes
snapshotFile: 'orders.contract.json',
},
),
});A silently renamed field — the classic integration breakage — becomes a loud, leveled signal. The request side validates too: input takes a schema per part and fails fast before any request is sent. Full guide: Validation & drift.
The things every src/api/ folder reinvents are configuration here — uniform across every stitch:
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' },
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.
A read-through response cache with in-process request coalescing — off by default, loaded lazily from stitchapi/cache only when a stitch sets cache. The key is derived from the resolved request (no caller-authored keys to drift) and principal-scoped by default, so user A is never served user B's cached response:
const getUser = stitch({
path: 'https://demo.stitchapi.dev/users/{id}',
output: User,
unwrap: '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',
cache: {
ttl: '1h',
scope: 'app',
vary: ['accept-language'],
maxEntries: 500,
},
});Caching is sound by construction: a stitch with an output schema caches only when that schema can be fingerprinted or you pin a version — otherwise it refuses to cache rather than serve a stale shape. Mark sensitive data sensitive: true to opt out entirely.
Auth is a field on the stitch — never global. Secrets resolve at call time (env(), secretsFile()), the declaration is committable, and the caller gets data without ever seeing the credential:
import { bearer, env, stitch } from 'stitchapi';
const getUser = stitch({
path: 'https://demo.stitchapi.dev/users/{id}',
auth: bearer(env('API_TOKEN')), // resolved per call; the caller passes no secret
});bearer, apiKey, and basic are header strategies; oauth2() runs the client-credentials grant and caches/refreshes the token; and cookieSession runs a login, captures the cookie, replays it, and re-logs-in when the wall returns — the caller never sees the password or writes the cookie dance. Full guide: Auth.
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 |
Full guide: Surfaces.
The same typed unit is reachable four ways, so humans and agents call exactly the same validated, observable thing:
| Front door | How | Example |
|---|---|---|
| In-process function | import and call | await listUsers() |
| CLI command | stitch run |
$ stitch run list-users |
| HTTP endpoint | stitch serve |
GET /list-users |
| MCP / agent tool | stitch mcp |
tool: list_users |
stitch run streams every event as one JSON line on stdout (ready for jq); stitch trace summarizes the run log — runs, failures, retries, drift, and latency percentiles. Full guide: Surfaces → CLI.
A stitch is built to be invoked by an AI agent. Auth lives on the stitch, so an agent gets a capability, not a credential. And rather than one MCP tool per endpoint (which floods the model's context), an agent drives a single code-mode tool, run_stitch: it writes a small TypeScript snippet that imports stitchapi and calls the stitch, with list_stitches and describe_stitch for discovery — so the context budget stays flat as the catalog grows.
Authoring is agent-friendly too — hand an agent one curl/HAR/doc example and it emits a stitch declaration, with a deterministic shortcut for the common case:
$ stitch from-curl 'curl https://demo.stitchapi.dev/users/7 -H "authorization: Bearer …"'
# prints a ready-to-commit stitch: id-like segments lifted to {params}, secrets → env()stitch init writes the “declare a stitch, don't hand-roll fetch” rule into the files coding agents read (AGENTS.md, Cursor/Windsurf/Cline rules, a CLAUDE.md section), and the docs build emits an auto-generated llms.txt. Full guide: Use from an agent.
A failed stitch throws a StitchError — an Error subclass carrying .status, .attempts, and (for response failures) .body (the parsed error payload, on a non-enumerable channel that never reaches a trace sink) and .url. Prefer branching? .safe() resolves to { ok, data, error }. Each failure mode has a stable code and a docs page:
| Code | When |
|---|---|
STITCH_VALIDATION |
a response failed its output schema |
STITCH_DRIFT |
a response drifted past the level you allowed |
STITCH_AUTH_WALL |
auth failed, or a soft 200 login wall couldn't be refreshed |
STITCH_TIMEOUT |
a call exceeded its timeout budget |
STITCH_CIRCUIT_OPEN |
the circuit breaker is open after repeated failures |
STITCH_GRAPHQL |
a GraphQL response came back 200 but carried an errors array |
RateLimitError |
a delegate-backoff stitch surfaced a rate-limit for an outer gate |
Full catalog: Errors & pitfalls.
Observability is a consumer of the event stream, not a separate system — and it's off by default. Opt in per stitch (trace: 'console' / fileSink(path) / a TraceSink) or globally with env vars — no collector, no dashboard, nothing to deploy:
STITCH_TRACE_CONSOLE=1 node app.js # live, colored, one line per event on stderr
STITCH_TRACE_FILE=./run.jsonl node app.js # append JSONL to a path
STITCH_EXPORT=otlp node app.js # also fan events to an OTLP collectorBuilt-in sinks scrub secrets at the sink boundary (the live request is never touched). stitch trace summarizes the JSONL; @stitchapi/pino ships the same events as structured logs.
This repository is a pnpm workspace. The published library is stitchapi; the rest are thin, peer-dependency integrations that add no capability of their own. The table below is generated by pnpm gen:readme (and verified in CI by pnpm check:readme): it lists every publishable workspace package, so it never drifts as packages are added. Each package's own README links to it on npm.
| Package | Description |
|---|---|
| Core | |
stitchapi | Turn any API into a typed, resilient function |
| Server frameworks | |
@stitchapi/elysia | Web-standard seam on the context; SSE and error mapping |
@stitchapi/express | Request-scoped seam on req, with SSE and error mapping |
@stitchapi/fastify | App/request seam with SSE, error and Pino-logger bridges |
@stitchapi/hono | Edge-ready seam on the request context; SSE and errors |
@stitchapi/nest | Injectable stitches wired into the Nest DI graph |
@stitchapi/next | Stream a stitch as an SSE Response in the App Router |
| Client & UI bindings | |
@stitchapi/angular | Stitch lifecycle as Angular signals and an RxJS observable |
@stitchapi/expo | Streaming over expo/fetch with a secure-store token store |
@stitchapi/query-core | Framework-agnostic reactive store behind the UI bindings |
@stitchapi/react | Tearing-free useStitch / useStitchStream hooks |
@stitchapi/react-native | Streaming XHR adapter and AsyncStorage-backed store |
@stitchapi/solid | createStitch primitives reconciled into a Solid store |
@stitchapi/svelte | Stitch stores for Svelte 4 and 5 (unary + streaming) |
@stitchapi/vue | Reactive useStitch / useStitchStream composables |
| Data-fetching libraries | |
@stitchapi/rtk-query | Run a stitch as an RTK Query endpoint, with stream updates |
@stitchapi/swr | Run a stitch as an SWR fetcher; SWR owns caching |
| State stores | |
@stitchapi/cloudflare-kv | Edge cache and shared sessions on Workers KV |
@stitchapi/deno-kv | Distributed throttle and sessions on Deno KV |
@stitchapi/redis | Distributed throttle and shared sessions via Redis |
| Auth | |
@stitchapi/aws-sigv4 | Sign requests with AWS SigV4 (edge-safe Web Crypto) |
| AI | |
@stitchapi/vercel-ai | Expose a stitch as a model-callable tool, credential-safe |
| Observability | |
@stitchapi/pino | The stitch event stream as structured Pino logs |
@stitchapi/sentry | Stitch events as Sentry breadcrumbs, with error capture |
| Surfaces | |
@stitchapi/shell | Run a static local command as a stitch (injection-proof) |
| Cache fingerprint adapters | |
@stitchapi/fingerprint-arktype | Cache-fingerprint strategy for ArkType schemas |
@stitchapi/fingerprint-effect | Cache-fingerprint strategy for Effect Schema |
@stitchapi/fingerprint-typebox | Cache-fingerprint strategy for TypeBox schemas |
@stitchapi/fingerprint-valibot | Cache-fingerprint strategy for Valibot schemas |
@stitchapi/fingerprint-zod | Cache-fingerprint strategy for Zod schemas |
The full documentation site lives at stitchapi.dev — Quickstart, per-feature Guides, Concepts, Surfaces, For agents, and the generated Reference.
The published library's own README (what npm renders) is in packages/core/README.md. Design notes live in docs/: Feature Lenses, Overview, Design.
Local setup, the dev loop, the bundle-size budget, and the worktree + PR workflow are documented in CONTRIBUTING.md.