The starter template for AI startups. Auth, checkout and i18n included. Français
Advanced

Connecting your API

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:

  1. 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.
  2. 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.
  3. Call it from a page module in src/js/pages/, registered in PAGE_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.

Launch offer: 20% off either licence with code LAUNCH20 at checkout.