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.
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 → /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;
/apiand/serverare 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.
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 returns | Response |
|---|---|
| A plain value (object, array, number…) | 200 with a JSON body |
| A string | 200 plain text |
undefined | 204 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
// 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 → /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() };
}<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 — project root, gitignored. KEY=VALUE lines, # comments allowed.
THIRD_PARTY_KEY=sk_live_...
DATABASE_URL=postgres://....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.serverfrom 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:
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.
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 aswizz dev, and answers unmatched API paths with404JSON. - Falls back to the document shell for other extensionless paths, and never serves
/server/**.
With --json, the build envelope records the adapter:
{
"format": "wizz-build-diagnostics@1",
"ok": true,
"diagnostics": [],
"files": [{ "file": "App.wizz", "serverRenderable": true }],
"adapter": "node"
}