Server routes and responses
tibi serve runs request-time JavaScript: API routes under pages/api/, and page frontmatter that can redirect, set the status, add headers and set cookies. Both run in a pool of long-lived node workers instead of a fresh process per evaluation.API routes
Any .ts, .mts, .js or .mjs file under pages/api/ is an endpoint, mapped like pages (pages/api/notes/[id].ts serves /api/notes/:id). Files starting with _ and .d.ts files are helpers, not routes. Export a handler per method:
Handlers: GET, POST, PUT, PATCH, DELETE, then ALL or default as a catch-all. HEAD falls back to GET. A method with no handler answers 405 with an Allow header.
The ctx argument
method,url,path,origin,params,query,headers(lower-cased),cookies,user(see Auth).body: parsed JSON (application/json), an object for form posts (application/x-www-form-urlencoded), else the raw text.textalways holds the raw body. Bodies are capped at 10 MiB.setStatus(n),setHeader(name, value),setCookie(name, value, opts),deleteCookie(name),redirect(to, status = 302).error(status, message, data?)throws; the response is{ "error": message, "data": data }with that status.json(data, init)isResponse.json.tether: a server-side Tether client when theTIBI_TETHER_*variables are set (see Tether); also on frontmatter'sctx.
Return values
- An object or array: JSON. A string:
text/plain.undefined: 204 (or the redirect). - A web
Response: used as-is (status, headers, multipleSet-Cookie, binary bodies). - Any other thrown error: 500
{"error":"Internal Server Error"}, with the stack logged to the server's stderr.
Cookie options: path (default /), domain, maxAge (seconds), expires, httpOnly (default true), secure (default true on https requests), sameSite (lax default, strict, none).
With esbuild in the project (npm i -D esbuild), each module is bundled for node into .tibi/server/, which handles TypeScript and relative imports; npm packages stay external and load from node_modules. Without it, the file is imported directly (node strips TypeScript types itself from 22.18). In dev mode any file change re-bundles on the next request. Several workers serve requests, so keep shared state in a database (or on disk), not in module variables. API routes aren't part of tibi build.
Response controls from frontmatter
Page and layout frontmatter (JS / TS) get the same helpers on ctx:
Other languages set the reserved keys directly: __status (number), __redirect ("/path" or { to, status }), __headers (object) and __cookies ({ name: "value" } or { name: { value, ...options } }). They never become template tokens.
- A page that redirects skips its layouts entirely. A layout can redirect too (an auth guard in
_layout.tibi); it runs after the page's frontmatter. - The page's status and redirect win over a layout's; headers and cookies from every layer add up.
- Responses that set cookies get
Cache-Control: private, no-storeunless a header says otherwise. - In
tibi builda redirecting page becomes a meta-refresh stub. ctx.routeis only set while rendering a request. Boot-time scans and builds evaluate frontmatter without it, so usectx.route?.cookies.
The worker pool
JS and TS frontmatter, and every API route, run in a small pool of persistent node processes (default min(4, cores)). Each worker handles many requests concurrently, so a slow await fetch() in one render doesn't block others. Rendering itself runs on a blocking thread pool, off the async server threads. Within one render, a component's frontmatter runs once per distinct context, however many times the component appears. console.log in user code goes to the server's stderr. Compiled Rust frontmatter binaries are cached by source, so rustc runs once per change rather than per request.
Environment variables:
TIBI_JS_WORKERS: pool size.TIBI_JS_TIMEOUT_MS: per-call timeout (default 15000). A worker that misses it is killed and replaced, so an infinite loop costs one request, not the server. API routes answer 504.TIBI_JS_POOL=0: go back to onenodeprocess per evaluation.