Core Concepts

Server APIs & SSR

Everything in a .wizz script ships to the browser. Code that must stay private — API keys, database calls, third-party requests — lives in src/server/api/*.js and runs on the server behind a same-origin /api/* route.

Server API routes

Each file's default export handles its route. Routes mirror pages: index.js names the directory, nested directories nest the path, and everything lowercases.

file → route
src/server/api/health.js          ->  /api/health
src/server/api/index.js           ->  /api
src/server/api/users/index.js     ->  /api/users
src/server/api/users/search.js    ->  /api/users/search
src/server/api/_db.js             ->  (private helper: importable, never routed)
src/server/api/greet.js
// src/server/api/greet.js → /api/greet  (powers the sandbox below)
export default async function handler(request) {
  if (request.method !== "GET") {
    return { status: 405, headers: { allow: "GET" }, body: { error: "Method not allowed" } };
  }

  const name = (request.query.get("name") ?? "").trim().slice(0, 40) || "friend";

  return {
    message: "Hello, " + name + "!",
    method: request.method,
    path: request.path,
    runtime: "Node " + process.version,
    renderedAt: new Date().toISOString()
  };
}
  • Files and directories starting with _ are importable helpers, never routes.
  • Duplicate routes fail the build naming both files; /api and /server are reserved namespaces for pages.
  • Handler edits apply on the next request — no rebuild. Relative imports must stay inside src/server/.
  • Handler source is never served as a static file, in development or production.

The request context

Handlers receive a frozen plain object rather than Node's req/res, so they stay portable across hosts.

handler(request)
request = Object.freeze({
  method,    // "GET" | "POST" | ...
  path,      // decoded pathname, e.g. "/api/submit-form"
  query,     // URLSearchParams
  headers,   // plain object, lowercased names
  body,      // raw body text, or null when empty
  json()     // async: parses body; malformed JSON answers 400
})

Request bodies are capped at 1 MB; larger requests answer 413.

Return values

Handler returnsResponse
A plain value (object, array, number…)200 with a JSON body
A string200 plain text
undefined204 no content
{ status, headers, body }Full control over the response
A thrown error (or a module without a default function)Fixed 500 {"error":"Internal server error"} — details go to the server log only

Live: call a handler

This sandbox is a Wizz component fetching /api/greet from this site's own server. The JSON is produced by the handler above.

Live · ApiDemo.wizz → /api/greet

idle—

// Press “Call /api/greet” — the handler runs on the server.

Calling a handler from a component is an ordinary fetch:

src/server/api/submit-form.js
// src/server/api/submit-form.js → /api/submit-form
export default async function handler(request) {
  const payload = await request.json();   // 400 on malformed input
  const response = await fetch("https://third-party.example/v1/submit", {
    method: "POST",
    headers: { Authorization: "Bearer " + process.env.THIRD_PARTY_KEY },
    body: JSON.stringify(payload)
  });
  return { status: response.status, body: await response.json() };
}
Subscribe.wizz
<script>
  let result = "Not sent yet";

  async function send(event) {
    event.preventDefault();
    const response = await fetch("/api/submit-form", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email: event.target.email.value })
    });
    result = response.ok ? "Sent!" : "Failed: " + response.status;
  }
</script>

<form on:submit={send}>
  <input name="email" type="email" required />
  <button>Subscribe</button>
  <p>{result}</p>
</form>

Secrets & .env.server

Keep secrets in a gitignored .env.server at the project root — never in src/ or a .wizz file. The dev server loads it into process.env before the first request; handlers read keys at request time.

.env.server
# .env.server — project root, gitignored. KEY=VALUE lines, # comments allowed.
THIRD_PARTY_KEY=sk_live_...
DATABASE_URL=postgres://...
.gitignore
.env.server
dist/
  • Real environment variables always win over file entries.
  • The file is never copied into build output or served.
  • The node adapter reads .env.server from the directory it is started in; most platforms inject environment variables directly.

Server rendering

Pages server-render by default. wizz dev and the node host match each document request against the manifest, call the page's renderComponent(), put the HTML inside #app, place the state script beside it, and inject the head between wizz:head-start / wizz:head-end comment markers. The router then hydrates instead of re-rendering.

What can server-render

Everything with a deterministic first render: static markup, text and attribute expressions, props, the initially-taken {#if} branch, {#each} lists, and imported components — recursively. Eligibility is computed bottom-up over the import graph; a page whose child fails the gate stays client-only, and the build explains why:

build output
Note: server rendering skipped for src/App.wizz — Server rendering does not support
component tags; <Counter> cannot be rendered server-side at src/App.wizz:3:16.
Underlying reason: ... at src/components/Counter.wizz:3:15.
Serving the client build only.

Event handlers and lifecycle hooks are client-only. Child component state rides in the page's state under the reserved __wizz key, so reactive names may not start with __wizz.

Deploy with the node adapter

wizz build --adapter node adds dist/server.mjs: a standalone SSR + API host that imports only Node built-ins and the build's own artifacts.

terminal
wizz build --adapter node
node dist/server.mjs              # http://localhost:8080
PORT=3000 node dist/server.mjs    # PORT is validated (0-65535)
cd dist && node server.mjs        # dist resolves from the host's own location
  • Serves traversal-guarded static files from dist/ — encoded dots, backslashes, drive letters, NUL bytes, and symlinks pointing outside are all contained.
  • Server-renders manifest routes, runs /api/** handlers with the same context and return shapes as wizz dev, and answers unmatched API paths with 404 JSON.
  • Falls back to the document shell for other extensionless paths, and never serves /server/**.

With --json, the build envelope records the adapter:

wizz build --adapter node --json
{
  "format": "wizz-build-diagnostics@1",
  "ok": true,
  "diagnostics": [],
  "files": [{ "file": "App.wizz", "serverRenderable": true }],
  "adapter": "node"
}