rejifald/StitchAPI
π βοΈ - Semantic search over the StitchAPI documentation (the hosted docs MCP): searchdocs returns the most relevant doc sections with deep links, getdoc fetches a full page. Hosted endpoint https://stitchapi.dev/api/mcp, no auth.
Quick Install
{
"mcpServers": {
"rejifald-stitchapi": {
"command": "npx",
"args": [
"-y",
"rejifald-stitchapi"
]
}
}
}Using an AI coding agent (Claude Code, Cursor, etc.)? Copy a ready-made prompt that tells it to fetch the setup instructions and install this server for you.
Documentation Overview
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 Β· ~25Β kB min+gzip β a typical import { stitch } tree-shakes to ~20Β 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.6. 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 stable1.0.0β pin an exact version and expect only small, documented changes. Feedback is welcome.
Table of Contents
- 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
What is a stitch?
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() }),
pick: 'data',
});
const user = await getUser({ params: { id: 1 } }); // typed Β· validated
Keep 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.
Motivation
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:
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 |
Why StitchAPI
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.
What StitchAPI is not
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.
Features
- 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 validated against the declared schema (the contract); a required field missing/incompatible throws, while soft drift (a coercion, an undeclared or defaulted field) surfaces as a non-fatal
warn/info/verbosefinding instead 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; ~25 kB min+gzip for the whole entry, ~20 kB for a typicalimport { stitch }.
Install
npm install stitchapi@rc # or: pnpm add stitchapi@rc Β· yarn add stitchapi@rc
Validation 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.
Quick start
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 JSON
Path 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=roles
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
pick: 'data',
retry: 3, // β‘ { attempts: 3 }
timeout: '5s', // β‘ { total: '5s' }
cache: '1m', // β‘ { ttl: '1m' }
});
const user = await getUser({ params: { id: 42 } });
// β typed Β· validated Β· retried Β· cached
A 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.
Composition: seam, extends, .with()
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(),
pick: 'data',
});
const getUser = api.stitch({
path: '/users/{id}',
output: User,
pick: '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.
Validation & leveled drift
The declared output schema is the contract. Wrap it in drift(): a response is validated (the call returns the validated value β coerced, defaulted, unknown keys stripped), and the difference between the raw body and that validated value is reported as a leveled, non-fatal signal:
| Change | What it means | Level |
|---|---|---|
| invalid | a required field missing or incompatible β throws | error |
| coerced | a coercion ("42"β42): a wire-type shift validation hid | warn |
| undeclared | a key the schema strips | info |
| defaulted | a .default() fired (field absent) | verbose |
import { drift, stitch } from 'stitchapi';
import { z } from 'zod';
const listOrders = stitch({
path: 'https://demo.stitchapi.dev/users/{id}/orders',
pick: 'data',
output: drift(
z.array(z.object({ id: z.number(), total: z.number().optional() })),
{
ignore: ['[].meta'], // acknowledged, unconsumed β don't report it
severity: { coerced: 'info' }, // re-level a kind, or pass a level/list to filter
},
),
});
Drift is schema-anchored β no snapshot to manage. Severity lives in the schema: a required field missing/incompatible is a hard invalid that throws; everything else is non-fatal drift on the event stream. Declared variance (an optional field, a nullable, an empty array) validates clean, so it's never a false alarm; ignore silences known-but-unconsumed fields and severity filters or re-levels the soft signals. The request side validates too: input takes a schema per part and fails fast before any request is sent. Full guide: Validation & drift.
Resilience: retry, throttle, timeout
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, pool: 'host' },
timeout: { total: '30s', perAttempt: '10s' },
});
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.
Caching
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,
pick: 'data',
cache: '5m',
});
const listAnnouncements = stitch({
path: 'https://demo.stitchapi.dev/announcements',
output: z.array(z.object({ id: z.number(), title: z.string() })),
pick: 'data',
cache: {
ttl: '1h',
scope: 'app',
vary: ['accept-language'],
entries: 500,
version: 1, // pins the shape β cacheable without a fingerprinter
},
});
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 as a boundary
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.
Surfaces: any request style
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 }, 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.
Four front doors
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.
Agent-native
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.
Errors & pitfalls
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.
Zero-infra observability
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 collector
Built-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.
Packages
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 |
| Other | |
@stitchapi/docs-mcp | StitchAPI documentation search, running locally over MCP stdio |
@stitchapi/json-schema | Turn a runtime-discovered JSON Schema into a Standard Schema validator StitchAPI accepts |
@stitchapi/openapi | Eject selected operations from an OpenAPI document into ready-to-own StitchAPI source |
Documentation
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.
Contributing
Local setup, the dev loop, the bundle-size budget, and the worktree + PR workflow are documented in CONTRIBUTING.md.