Renderers
Renderers finalize responses in Arbor. When an action returns a value, Arbor selects a renderer whose contentType matches the current response Content-Type. The renderer receives the returned value and the request bundle, then writes the response.
If no renderer matches, Arbor uses the built-in fallback for JSON or text.
Creating a Renderer
import { createRenderer } from "@kequtech/arbor";
const rendererHtml = createRenderer({
contentType: "text/html",
action: (payload, { req, res }) => {
const html = `<p>${payload}</p>`;
res.setHeader("Content-Length", Buffer.byteLength(html));
if (req.method === "HEAD") {
res.end();
return;
}
res.end(html);
},
});
A renderer receives:
(payload: unknown, bundle: Bundle) => void
Returned values are ignored, renderers must finalize the response.
Matching Content Types
Arbor matches renderers using simple pattern logic:
"application/json"matches"application/json""text/*"matches"text/plain","text/html", etc.
Use specific content types for precise control or wildcards for broad formats.
Attaching Renderers to Branches
Renderers belong to branches. A renderer affects all routes inside that branch and its nested branches.
import { createBranch } from "@kequtech/arbor";
const branchDocs = createBranch({
url: "/docs",
renderers: [
rendererHtml,
],
});
Routes inside /docs will use rendererHtml unless another renderer is closer or more specific.
Built-in Renderers
Arbor ships with two renderers:
application/jsontext/*
These handle most cases. You override them by providing renderers with the same contentType.
Example: JSON Renderer Override
const rendererPrettyJson = createRenderer({
contentType: "application/json",
action: (payload, { req, res }) => {
const json = JSON.stringify(payload, null, 2);
res.setHeader("Content-Length", Buffer.byteLength(json));
if (req.method === "HEAD") {
res.end();
return;
}
res.end(json);
},
});
HEAD Behavior
Renderers are responsible for correct HEAD handling. The standard pattern:
if (req.method === "HEAD") {
res.end();
return;
}
Interaction with Actions
An action can:
- return a value to invoke a renderer
- throw an error to invoke an error handler
- finalize the response directly and prevent renderers from running
Renderers always run after an action returns a value.
Renderer Location
Best practice is to define renderers in a dedicated module and reference them inside branches. This keeps route files small and avoids duplication.
// renderers.ts
export const rendererHtml = createRenderer({ ... });
export const rendererJson = createRenderer({ ... });
Then:
const branchDocs = createBranch({
url: "/docs",
renderers: [rendererHtml],
});
Summary
Renderers convert returned values into final responses. Key points:
- They match on Content-Type
- They finalize the response
- They belong to branches
- They handle GET and HEAD explicitly
- They allow each part of the app to define its own output format
Renderers are one of Arbor’s core extension points and make it possible to support any response format you need.