Elysia
An Elysia plugin for StitchAPI — a principal-bound seam on the request context, an SSE bridge with streamStitchSse returning a Response, and stitch errors mapped to HTTP via the plugin's onError.
Use @stitchapi/elysia when you serve an API with Elysia.
.use() the plugin and every request gets a principal-bound seam on its context,
plus two bridges into Elysia's Web-standard world: an SSE writer and a stitch-error
→ HTTP mapping.
Web-standard by construction. Elysia is Bun-first, but the plugin
imports only elysia and stitchapi — there are no node:* imports,
so it runs unchanged on Bun, Node, Deno, and the edge.
Example
Install the package alongside core and Elysia:
npm install @stitchapi/elysia@rc stitchapi@rc elysiaBuild (and own) the seam once at startup, then .use() the plugin. With a
principal resolver, each request's context stitch is a seam.as(id) handle — a
principal-bound seam with separate auth sessions per principal over one shared
throttle. A .derive runs per request and puts it on the context, so a handler
reads it straight off the destructured context:
import { stitch } from '@stitchapi/elysia';
import { Elysia } from 'elysia';
import { seam } from 'stitchapi';
import { z } from 'zod';
const api = seam({ baseUrl: 'https://api.example.com' });
const app = new Elysia()
.use(
stitch({
seam: api,
principal: ({ request }) =>
request.headers.get('x-tenant') ?? undefined,
}),
)
.get('/me', ({ stitch }) =>
stitch.stitch({
path: '/me',
output: z.object({ id: z.string(), name: z.string() }),
})(),
);The principal lives in the closure, never in a call argument, so a handler can
never name another identity. Returning undefined falls back to the unbound root
seam for that request (e.g. anonymous). Borrow, don't own: the plugin never
calls seam.close() — you build the seam at startup and await api.close() on
shutdown.
Streaming — streamStitchSse
Stream a streaming/SSE stitch's .stream() to the client by returning the
text/event-stream Response streamStitchSse builds. Each delta becomes a
data: message (shape it with data, name it with event, make the stream
resumable with id); a terminal error (or a throw) becomes a final
event: error message whose data is a generic error token by default —
the raw upstream message (an internal hostname, an HTTP 401) never reaches the
client. Opt in to a client-facing message with error, and observe the real
failure server-side with error.observe. Control events are consumed but not
forwarded, and a client disconnect cancels the body and aborts the upstream
stitch stream.
import { stitch, streamStitchSse } from '@stitchapi/elysia';
import { Elysia } from 'elysia';
import { seam } from 'stitchapi';
import { sseSurface } from 'stitchapi/sse';
const api = seam({ baseUrl: 'https://api.example.com' });
const app = new Elysia()
.use(stitch({ seam: api }))
.get('/chat', ({ stitch, query }) => {
const chat = stitch.stitch({ kind: sseSurface, path: '/v1/messages' });
return streamStitchSse(chat.stream({ body: { prompt: query.q } }), {
delta: (chunk) => (chunk as { data: string }).data,
});
});By default the terminal error frame carries a generic data: error token — the raw
event.message is not echoed, since it can disclose internal network topology (a
transport failure reads like getaddrinfo ENOTFOUND payments.internal.corp) or the
upstream's status (HTTP 401) to the client. Pass error to opt in when the upstream
messages are known safe (error: (e) => e.message); the object form error: { observe }
still receives the real failure server-side for logging. Both delta and error also take
a full object (delta: { data, event, id }, error: { data, event, observe }).
Anti-pattern: return the streamStitchSse Response as the handler's
result — don't also set set.status / set.headers or return a second
value from the same handler. The helper builds the complete
text/event-stream response (status, headers, and the frame body); a
competing write corrupts the stream.
Errors
A failed stitch throws a StitchError carrying the upstream status. The plugin
registers an .onError that maps it 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 — so handlers need no try/catch:
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,
// the default is a safe 502; propagate the upstream status instead:
onError: { status: (e) => e.status ?? 502 },
}),
);Set onError: false to register none and wire your own with stitchOnError
(an .onError-compatible mapper) or stitchErrorResponse(err, options) (a one-off
StitchError → Response); isStitchError narrows an unknown error first.
On Bun clusters and other multi-runtime deployments, pair the seam with a shared store so throttle and sessions are fleet-wide rather than per-process.
See also
Integrations
First-party companion packages that bridge a stitch to the framework, frontend, logger, and store you already run — each a thin layer over a stable seam, never a re-implementation.
Express
Express 4/5 middleware for StitchAPI — a principal-bound seam on req.stitch, an SSE bridge with streamStitchSse, and stitch errors mapped to JSON by a 4-arg error handler.