Public entry point of web-base: the handler that wires the base together and the server lifecycle. A library the host calls; it never calls the host back except through the functions the host hands it (SPEC §3).
The wiring convention IS the product, so here it is, outermost first:
request-id → security headers → proxy (opt-in) → body limit → [/wb/ assets → sessionless routes → host static → ] error boundary → session → params → i18n → multipart (declared routes only) → csrf → subject → ring-handler router, per matched route: error → gate → render → coercion → handler default handler: 404 / 405 / nil-handler 500
Static assets answer before the session: a stylesheet fetch must not mint a
session cookie, and a cookie on an asset defeats shared caches. They still
carry the request id and the security headers. So do the host's :sessionless
routes — a health probe, a webhook — which read no session and write none, whatever
cookie arrives. The body limit sits outside all of them, so nothing reads more than
it allows; multipart sits outside csrf because the token is one of its fields, and
inside i18n so a refused upload's page speaks the negotiated language. Session sits
outside subject
(the subject function reads the session) and outside csrf (the token lives
in the session); params sits outside csrf because the token may arrive as a
form field; i18n sits outside csrf so a refused request's error page speaks
the negotiated language. Error sits outside gate and render inside the
router: a predicate or a layout may throw, and a refusal is an error datum.
The default handler runs outside router middleware, so the request id, the
session and the security headers reach it from the outer stack while errors
render on their own.
Public entry point of web-base: the handler that wires the base together and
the server lifecycle. A library the host calls; it never calls the host back
except through the functions the host hands it (SPEC §3).
The wiring convention IS the product, so here it is, outermost first:
request-id → security headers → proxy (opt-in) → body limit
→ [/wb/ assets → sessionless routes → host static → ] error boundary → session → params
→ i18n → multipart (declared routes only) → csrf → subject
→ ring-handler
router, per matched route: error → gate → render → coercion → handler
default handler: 404 / 405 / nil-handler 500
Static assets answer before the session: a stylesheet fetch must not mint a
session cookie, and a cookie on an asset defeats shared caches. They still
carry the request id and the security headers. So do the host's `:sessionless`
routes — a health probe, a webhook — which read no session and write none, whatever
cookie arrives. The body limit sits outside all of them, so nothing reads more than
it allows; multipart sits outside csrf because the token is one of its fields, and
inside i18n so a refused upload's page speaks the negotiated language. Session sits
outside subject
(the subject function reads the session) and outside csrf (the token lives
in the session); params sits outside csrf because the token may arrive as a
form field; i18n sits outside csrf so a refused request's error page speaks
the negotiated language. Error sits outside gate and render inside the
router: a predicate or a layout may throw, and a refusal is an error datum.
The default handler runs outside router middleware, so the request id, the
session and the security headers reach it from the outer stack while errors
render on their own.(expand config) → the plain config its :plugins stand for, merged by the rules of
dev.arkaitz.web-base.plugin — what handler builds from, to read at the REPL.
`(expand config)` → the plain config its `:plugins` stand for, merged by the rules of `dev.arkaitz.web-base.plugin` — what `handler` builds from, to read at the REPL.
(form-done request location fragment)The answer to a form the host accepted, for a page that works with and without
JavaScript: an htmx swap gets fragment with a 200, and a navigation a 303 to
location (response/see-other), so a reload never posts twice.
The answer to a form the host accepted, for a page that works with and without JavaScript: an htmx swap gets `fragment` with a 200, and a navigation a 303 to `location` (`response/see-other`), so a reload never posts twice.
(handler config)Builds the Ring handler from the host's config:
:routes reitit route data; per route :wb/layouts and :wb/gate, both
inherited by nested routes (layouts concatenate, a child's gate replaces);
:wb/log-path :template logs the route's template instead of its path,
for a path that carries a secret; :wb/multipart {:max-file-size n …}
parses a file upload for that route alone
:session {:key base64-or-bytes} or {:store s} (required)
:subject-fn request → subject or nil (default: always nil)
:login-path where a refusal without a subject goes (required iff a route has :wb/gate)
:coercion a reitit coercion, passed through (optional)
:static create-resource-handler options for the host's assets (optional)
:error-layout slot function for error pages (optional)
:i18n {:dict … :default-locale … :locales […] :locale-fn …} (optional;
:locales since 0.12.0, the default alone when absent)
:security {:frame-options … :csp … :hsts {:max-age seconds} :proxy-hops n} (optional)
:csrf false to disable the anti-forgery token (on for anything else, nil included)
:sessionless {"/health" handler "/api/" handler} — answered before the session,
CSRF, i18n and subject, with the request id, security headers and body
limit only; a path ending in / takes everything under it, an exact
path winning; a handler is a function or a var; a path or prefix that
covers one of :routes is refused, and so is / (optional)
:max-body-bytes the largest request body read, 200 000 by default; a route's
:wb/multipart sets its own (optional)
:assets [{:path "/name/" :root "classpath/prefix"}], served beside /wb/,
before the session (optional)
:stylesheets ["/app.css"], linked by the shell after the base's own (optional)
:plugins values that contribute these same keys, merged by expand (optional)
Unknown keys are the host's own business — all but the names above, :assets,
:stylesheets and :plugins among them since 0.11.0. Inside the maps the base owns —
:session, :security and its :hsts, :i18n, :static — an unknown key is
refused, naming its path. Every failure of a required or malformed value is raised
here, at construction.
Builds the Ring handler from the host's config:
:routes reitit route data; per route `:wb/layouts` and `:wb/gate`, both
inherited by nested routes (layouts concatenate, a child's gate replaces);
`:wb/log-path :template` logs the route's template instead of its path,
for a path that carries a secret; `:wb/multipart {:max-file-size n …}`
parses a file upload for that route alone
:session `{:key base64-or-bytes}` or `{:store s}` (required)
:subject-fn request → subject or nil (default: always nil)
:login-path where a refusal without a subject goes (required iff a route has :wb/gate)
:coercion a reitit coercion, passed through (optional)
:static create-resource-handler options for the host's assets (optional)
:error-layout slot function for error pages (optional)
:i18n `{:dict … :default-locale … :locales […] :locale-fn …}` (optional;
`:locales` since 0.12.0, the default alone when absent)
:security `{:frame-options … :csp … :hsts {:max-age seconds} :proxy-hops n}` (optional)
:csrf false to disable the anti-forgery token (on for anything else, nil included)
:sessionless `{"/health" handler "/api/" handler}` — answered before the session,
CSRF, i18n and subject, with the request id, security headers and body
limit only; a path ending in `/` takes everything under it, an exact
path winning; a handler is a function or a var; a path or prefix that
covers one of :routes is refused, and so is `/` (optional)
:max-body-bytes the largest request body read, 200 000 by default; a route's
`:wb/multipart` sets its own (optional)
:assets `[{:path "/name/" :root "classpath/prefix"}]`, served beside `/wb/`,
before the session (optional)
:stylesheets `["/app.css"]`, linked by the shell after the base's own (optional)
:plugins values that contribute these same keys, merged by `expand` (optional)
Unknown keys are the host's own business — all but the names above, `:assets`,
`:stylesheets` and `:plugins` among them since 0.11.0. Inside the maps the base owns —
`:session`, `:security` and its `:hsts`, `:i18n`, `:static` — an unknown key is
refused, naming its path. Every failure of a required or malformed value is raised
here, at construction.(redirect-for request path): a 303 for a navigation, an HX-Redirect for an htmx
swap, uncached — how the gate sends a refused visitor away, for a host's own detour.
`(redirect-for request path)`: a 303 for a navigation, an `HX-Redirect` for an htmx swap, uncached — how the gate sends a refused visitor away, for a host's own detour.
(refuse-form request path form fragment)The answer to a form the host refused, for a page that works with and without
JavaScript: an htmx swap gets fragment — the form with its errors, drawn by the
handler — as a 422 (response/unprocessable), and a navigation gets the page at
path rendered again with form under :wb/form (rerender, whose rules apply,
path never taken from the request among them).
(if-let [errors (validate values)] (wb/refuse-form request "/things" {:values values :errors errors} (thing-form request values errors)) (wb/form-done request "/things" (thing-row saved)))
The answer to a form the host refused, for a page that works with and without
JavaScript: an htmx swap gets `fragment` — the form with its errors, drawn by the
handler — as a 422 (`response/unprocessable`), and a navigation gets the page at
`path` rendered again with `form` under `:wb/form` (`rerender`, whose rules apply,
`path` never taken from the request among them).
(if-let [errors (validate values)]
(wb/refuse-form request "/things" {:values values :errors errors}
(thing-form request values errors))
(wb/form-done request "/things" (thing-row saved)))(rerender request path form)The page at path rendered again for the request a handler is answering, with
form — whatever the host's view reads, typically {:values … :errors …} — under
:wb/form, and status 422 when the page renders as an ordinary 200.
For a classic form that failed validation: the POST handler validates, and on
failure answers (rerender request "/things" {:values v :errors e}), so the
person gets the page they were on with what they typed and why it was refused —
without a redirect that loses both, and without the POST handler rebuilding the
page it does not own. The page's own :get handler runs, compiled as the router
compiled it: its gate, coercion and layouts apply as for any GET of that path. The
request keeps its session, subject, locale and CSRF token; its form, body and
parameters are dropped, and path's own path and query parameters take their
place. Views read (:wb/form request), whose shape is the host's.
A response the page answers with a redirect, an HX-Redirect or any status other
than 200 is returned as it is.
On an htmx request the page renders as any GET does under htmx: its content without
its layouts, which lands inside whatever the form targeted. A form that swaps only
itself, and still works without JavaScript, answers through refuse-form instead.
Throws when path has no :get route, and when the request already carries
:wb/form — a page that re-rendered into itself would never stop. Not a validation
helper: the base still knows no schema (SPEC §7).
path is the host's own route, never input from the request. Whatever GET it
names runs as a side effect of this POST, as the caller — a logout, a link's
redemption — so a path taken from a form field would let whoever submits it choose
which.
The page at `path` rendered again for the request a handler is answering, with
`form` — whatever the host's view reads, typically `{:values … :errors …}` — under
`:wb/form`, and status 422 when the page renders as an ordinary 200.
For a classic form that failed validation: the POST handler validates, and on
failure answers `(rerender request "/things" {:values v :errors e})`, so the
person gets the page they were on with what they typed and why it was refused —
without a redirect that loses both, and without the POST handler rebuilding the
page it does not own. The page's own `:get` handler runs, compiled as the router
compiled it: its gate, coercion and layouts apply as for any GET of that path. The
request keeps its session, subject, locale and CSRF token; its form, body and
parameters are dropped, and `path`'s own path and query parameters take their
place. Views read `(:wb/form request)`, whose shape is the host's.
A response the page answers with a redirect, an `HX-Redirect` or any status other
than 200 is returned as it is.
On an htmx request the page renders as any GET does under htmx: its content without
its layouts, which lands inside whatever the form targeted. A form that swaps only
itself, and still works without JavaScript, answers through `refuse-form` instead.
Throws when `path` has no `:get` route, and when the request already carries
`:wb/form` — a page that re-rendered into itself would never stop. Not a validation
helper: the base still knows no schema (SPEC §7).
**`path` is the host's own route, never input from the request.** Whatever GET it
names runs as a side effect of this POST, as the caller — a logout, a link's
redemption — so a path taken from a form field would let whoever submits it choose
which.(start handler {:port n}) → {:server s :port n}.
`(start handler {:port n})` → `{:server s :port n}`.
Stops the handle returned by start.
Stops the handle returned by `start`.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |