ZenZip logozenzip

HTTP & Dashboard

A minimal HTTP adapter that shares the runtime with your handlers, webhook-to-workflow sugar, and the embedded observability dashboard.

Routes#

server.ts
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/delete register 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#

FieldWhat it is
paramspath parameters from :name segments
queryURLSearchParams
bodyparsed JSON (string for non-JSON, undefined for empty)
headersraw request headers
status / json / textresponse helpers
req / resthe 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#

webhooks.ts
// 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 configure

What 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 dead button
  • 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.