@stitchapi/elysia
An Elysia plugin for StitchAPI.
.use() it and every request gets a seam on its
context — a request-scoped principal bound per request — plus two bridges into
Elysia's Web-standard world: SSE streaming and a stitch-error → HTTP mapping.
import { stitch } from '@stitchapi/elysia';
import { Elysia } from 'elysia';
import { seam } from 'stitchapi';
const api = seam({ baseUrl: 'https://api.example.com' });
const app = new Elysia()
.use(stitch({ seam: api, principal: ({ request }) => userOf(request) }))
.get('/me', ({ stitch }) => stitch.stitch('/me')())
.listen(3000);Elysia is Bun-first, but the plugin imports only elysia and stitchapi
(no node:*), so it runs unchanged on Bun, Node, Deno and the edge.
What it does
- Puts a seam on the context.
.deriveruns per request and addsstitchto the context, so a handler reads it off the destructured context:({ stitch }) => stitch.stitch('/path')(). - Binds a request-scoped principal. With a
principalresolver, every request'sstitchis aseam.as(principal)handle — a separate session/token over the shared store + throttle. This is the trusted boundary StitchAPI's seam exists for: the principal lives in the closure, so a handler can never name another identity (ADR 0002). - SSE bridge.
streamStitchSse(stream)turns a stitch's.stream()output into atext/event-streamResponseyou return straight from a handler. - Error bridge. A thrown
StitchErroris mapped to an HTTP response (502by default) via the plugin's.onError, so handlers need no try/catch.
Seam: borrow, don't own
The plugin borrows the seam — it never closes it. Build it once at startup and
seam.close() it on shutdown yourself (the seam outlives any single request):
const api = seam({ baseUrl: 'https://api.example.com' });
const app = new Elysia().use(stitch({ seam: api }));
// on shutdown: await api.close();Principal binding
new Elysia().use(
stitch({
seam: api,
principal: ({ request }) =>
request.headers.get('x-tenant') ?? undefined,
}),
);Return undefined to fall back to the unbound root seam for that request (e.g.
anonymous). The principal-bound handle is lifecycle-free — only the root seam
the app owns carries close / flush / invalidate.
SSE streaming
import { streamStitchSse } from '@stitchapi/elysia';
import { sseSurface } from 'stitchapi/sse';
app.get('/chat', ({ stitch, query }) => {
const chat = stitch.stitch({ kind: sseSurface, path: '/v1/messages' });
return streamStitchSse(chat.stream({ body: { prompt: query.q } }), {
data: (chunk: any) => chunk.data, // pull text out of a structured chunk
});
});Each delta chunk becomes one SSE message; an error event ends the stream as a
named event: error message; stream end closes the response; and a client
disconnect cancels the body and aborts the upstream stitch stream rather than
leaving it running. Control events (start/progress/result/done/…) are not
forwarded.
Error handling
The plugin registers an .onError that maps a StitchError to an HTTP response
(502 by default, so an upstream's status is never leaked) and lets every other
error fall through to Elysia's default handling:
new Elysia().use(
stitch({
seam: api,
// propagate the upstream status instead of the safe 502 default:
errorHandler: { status: (e) => e.status ?? 502 },
}),
);Set errorHandler: false to register none and wire your own with
stitchOnError(options) or stitchErrorResponse(err, options).
API
| Export | Kind | Purpose |
|---|---|---|
stitch |
function | The plugin — .use(stitch({ seam, principal? })) |
streamStitchSse |
function | Stream a stitch's .stream() as an SSE Response |
stitchOnError |
function | An .onError-compatible StitchError → HTTP mapper |
stitchErrorResponse |
function | Map a StitchError to a Response (one-off) |
isStitchError |
function | Narrow an unknown error to a StitchError |
stitchapi and elysia are peer dependencies — bring your own.