HTTP & Dashboard
A minimal HTTP adapter that shares the runtime with your handlers, webhook-to-workflow sugar, and the embedded observability dashboard.
Routes#
app.get("/users/:id", (ctx) => ({
id: ctx.params.id, // ":id" segment
verbose: ctx.query.get("verbose"), // ?verbose=1
}));
app.post("/orders", async (ctx) => {
const order = ctx.body as NewOrder; // parsed JSON body
const { runId } = await orderFlow.trigger(order);
return ctx.status(201).json({ runId }); // explicit response
});
await app.start();
const { port, close } = await app.listen({ port: 3000 });// Handler semantics:
// return value → 200 JSON
// ctx.status(201).json(x) → explicit status + body
// return undefined → 204
// throw → 500 { error: message }
// no route → 404 { error: "not found" }Prefer Express?
Handlers also accept the Express (req, res, next) signature, and the app has app.use() middleware, routers, and built-ins. See Express & Middleware.
app.get/post/put/patch/deleteregister before start;app.listen()serves after start (port 0 picks a free one).- Servers close automatically in
app.stop()— intake stops before the queue drain begins.
The request context#
| Field | What it is |
|---|---|
params | path parameters from :name segments |
query | URLSearchParams |
body | parsed JSON (string for non-JSON, undefined for empty) |
headers | raw request headers |
status / json / text | response helpers |
req / res | the underlying node:http objects |
The real value: handlers live in the same process as the runtime, so queue.push(), workflow.trigger(), app.emit(), and machine.send() are nanosecond-cheap sync boundary calls away.
Webhook → workflow#
// One line: an HTTP endpoint that durably triggers a workflow.
app.workflow(
"stripe-events",
{ http: "POST /hooks/stripe", stepRetries: 5 },
async ({ step, input }) => {
// input = the webhook request body
await step.run("verify", () => verifySignature(input));
await step.run("apply", () => applyEvent(input));
},
);
await app.listen({ port: 3000 });
// POST /hooks/stripe → { runId: "…" } — retried webhooks are cheap to
// dedupe by triggering with an idempotency key derived from the body.The dashboard#
await app.start();
await app.dashboard(); // → http://127.0.0.1:4100
await app.dashboard({ port: 5000, host: "0.0.0.0" }); // or configureWhat it shows, live (2s polling):
- Overview cards — active runs, pending/running jobs, dead letters, schedules
- Workflow runs — status chips; click a run for the step-by-step detail (kind, attempts, errors, results)
- Queues — pending/running/dead per queue, with a one-click
requeue deadbutton - Schedules — spec, policies, next fire time
- Event feed — the outbox, newest first
It's served by your own process from embedded HTML — no build step, no separate deployment, nothing to install. The JSON API underneath (/api/overview, /api/runs/:id, …) is yours to script against.
Auth & roles
Bind to 127.0.0.1 by default. Set token for full (operator) access, and viewerToken for read-only access — a viewer can watch every view but is rejected (403) on mutations like requeue-dead. Tokens ride Authorization: Bearer … or ?token=; comparison is timing-safe.
Scope & positioning#
Per the benchmark verdict, ZenZip does not compete with Fastify/Hono on HTTP performance — this adapter exists so small apps need nothing else, and webhook-driven workflows get one-line ergonomics. For serious HTTP surface, run your favorite framework alongside and call trigger/push/emit from its handlers; dedicated Fastify/Hono mount helpers are tracked on the roadmap.