The boundary
All server-side work goes through one module, src/js/dataservice.js. The UI calls
methods such as getCoupon(), submitLogin() and verifyCode() and never knows
whether the answer came from demo data or a real API. That makes it the one place you
change to go live.
Demo and live modes
DATA_MODE=demo, the default, returns fake answers in the browser. With
DATA_MODE=live and an absolute API_BASE_URL, the build exposes the API origin to
the browser and data service methods can call it through src/js/api-client.js:
import { apiBaseUrl, get } from "./api-client.js";
async function getCoupon(code) {
if (apiBaseUrl()) {
return get("/v1/coupons/{code}", { params: { code }, action: "apply_coupon" });
}
return DEMO_COUPONS[code] ? { pct: DEMO_COUPONS[code] } : null;
}
The client adds a 15-second timeout, supports an AbortSignal for cancellation, sends
a fresh reCAPTCHA token in the x-recaptcha-token header, and throws errors that
carry .status, or .code for timeouts and reCAPTCHA failures.
Calling an AI model
Keep model API keys on your server, never in the browser. A typical setup:
- Add an endpoint to your backend, such as
POST /v1/generate, that checks the reCAPTCHA token and the user's session, then calls your model provider. - Add a matching method to the data service, with a demo version that returns a canned response so the UI still works offline and in screenshot tests.
- Call it from a page module in
src/js/pages/, registered inPAGE_MODULES, so its code only loads on the page that needs it.
Page modules
A page module exports init(shared) and, optionally, render():
export function init({ state, t, render }) {
document.querySelector("[data-prompt-form]")?.addEventListener("submit", (e) => {
e.preventDefault();
// call DataService, update state, then render()
});
}
Set pageModule: <name> in the layout's front matter. A form submitted before the
chunk arrives is held and replayed once the module loads, and if the chunk fails to
load, the shared header, footer and checkout keep working.
Testing it
tests/fixtures.js blocks unexpected network calls in browser tests. Use
page.route() to return fixture responses from your API, so tests stay fast and
predictable.