Express
Routes call one Rungs command each. This code runs against a real Express server in CI.
const app = express();app.use(express.json());
app.post("/expenses/:id/submit", async (req, res, next) => { try { const request = await rungs.submit({ flow: "expense", requesterId: userId(req), // the actor comes from the session, never from the body subject: { type: "expense", id: req.params.id }, payload: { amount: Number(req.body.amount) }, idempotencyKey: req.header("idempotency-key"), // a retried POST submits once }); res.status(201).json(request); } catch (error) { next(error); }});
app.post("/requests/:id/approve", async (req, res, next) => { try { res.json(await rungs.approve(approveInput(req))); } catch (error) { next(error); }});
app.get("/inbox", async (req, res, next) => { try { res.json(await rungs.inbox({ userId: userId(req), after: req.query.after as string })); } catch (error) { next(error); }});Runs in CI:examples/docs-snippets/src/guides/express.ts
- The actor comes from authentication, here a header that stands in for your session or
token. Never accept an
actorIdfrom the request body. expectedVersioncarries the version the user saw, so a decision on stale data is refused (EC-15).idempotencyKeyfrom theIdempotency-Keyheader makes a retried POST create one request (EC-16).
Every command method has a named input type, so the code that builds a command from an HTTP request can be typed on its own:
// Each command method has a named input type (`ApproveInput`, `RejectInput`, ...), so the code// that turns an HTTP request into a command can be typed and tested on its own.function approveInput(req: Request): ApproveInput { return { requestId: String(req.params.id), actorId: userId(req), expectedVersion: req.body.version, // the version the approver was looking at comment: req.body.comment, };}Runs in CI:examples/docs-snippets/src/guides/express.ts
The others are SubmitInput, RejectInput, ReturnInput, ResubmitInput, WithdrawInput,
DelegateInput, ReassignInput, RetryInput and OverrideInput.
Errors
Section titled “Errors”Every Rungs error extends RungsError and has a stable code. Map them once:
// Map Rungs errors to HTTP by class (or by their stable `code`), never by message text.app.use((error: unknown, _req: Request, res: Response, next: NextFunction) => { if (!(error instanceof RungsError)) return next(error); const status = error instanceof RequestNotFoundError ? 404 : error instanceof NotAssignedError ? 403 : error instanceof ConflictError || error instanceof InvalidTransitionError ? 409 : error instanceof InvalidCommandError ? 400 : 422; res.status(status).json({ code: error.code, message: error.message });});Runs in CI:examples/docs-snippets/src/guides/express.ts
The errors reference lists every code.