Documentation menu

@gyral/ssr

Gyral server rendering: Declarative Shadow DOM pages with hydration seeds.

npm install @gyral/ssr

Writing code? Start from working code, not from this page. Server rendering. Adapt the closest example and check it, then look a name up here when you are about to call it.

Generated from the package's type declarations. Entry points: @gyral/ssr, @gyral/ssr/static, @gyral/ssr/node.

@gyral/ssr

Functions

Name Summary
contentSecurityPolicy A Content-Security-Policy header value whose style-src allows Gyral's <style> elements by hash, for the components registered when it is called (import them first).
formAction A request handler for a <form method="post"> that works with JavaScript disabled, and answers the JS path (submitForm in @gyral/http, Accept: application/json) with JSON: valid → the handler's response, a redirect turned into { _tag: 'Redirected', location }; invalid → 422 with the IntentRejected (without values).
rejectWith Rejects a submission that passed the schema but failed a server-only check (email taken, wrong password).
renderPage An HTML Response for a full page, its body pulled in chunks from a synchronous render (one component boundary per pull): load data first, since nothing is awaited and there is no async or suspense streaming.
renderToString Renders to a complete string (tests, caching, static generation), with this request's stores in scope (ADR 0013).
seeOther 303 See Other: the redirect that turns a POST into a GET (no resubmit on reload).
styleHashes 'sha256-…' hashes of every registered shadow component's declarative-shadow-root <style>, for a Content-Security-Policy style-src.
wantsJson Whether the request asks for JSON rather than a page (Accept: application/json without text/html): submitForm and other fetch calls do, a browser's form post doesn't.

contentSecurityPolicy

function contentSecurityPolicy(options?: CspOptions): Promise<string>;

A Content-Security-Policy header value whose style-src allows Gyral's <style> elements by hash, for the components registered when it is called (import them first). renderPage({ …, csp: options }) builds it at render time instead; pass this result to renderPage({ …, csp }) or set it on any response. Other inline styles (style attributes, hand-written <style> in head) are not covered.

formAction

function formAction<Schema extends StandardSchemaV1>(definition: FormDefinition<Schema>, handlers: FormActionHandlers<StandardSchemaV1.InferOutput<Schema>>): (request: Request) => Promise<Response>;

A request handler for a <form method="post"> that works with JavaScript disabled, and answers the JS path (submitForm in @gyral/http, Accept: application/json) with JSON: valid → the handler's response, a redirect turned into { _tag: 'Redirected', location }; invalid → 422 with the IntentRejected (without values). On the no-JS path values never includes the definition's secret fields (defineForm(schema, { secret })).

rejectWith

function rejectWith(issues: string | readonly FieldIssue[]): FormReject;

Rejects a submission that passed the schema but failed a server-only check (email taken, wrong password). A string is a form-level message (path ''). formAction turns it into the same IntentRejected as a schema failure, on both the no-JS and the JS path.

renderPage

function renderPage(options: PageOptions, init?: ResponseInit): Response;

An HTML Response for a full page, its body pulled in chunks from a synchronous render (one component boundary per pull): load data first, since nothing is awaited and there is no async or suspense streaming. Status and headers are final before the first byte. csp sets the Content-Security-Policy header: a string as is, or contentSecurityPolicy()'s options to build it now, with every component registered by the time the page renders (and the page's styles). With csp: { styleAttributes: 'hash' } the page is rendered to a string first, so the header can list its style attribute values by hash (ADR 0020); a render error then throws here. A component whose init or view throws renders its error view and the page goes on; onError hears it, and onError: 'throw' renders to a string first and throws here instead, before any byte is sent (ADR 0024).

renderToString

function renderToString(value: ChildValue, options?: RenderOptions): Promise<string>;

Renders to a complete string (tests, caching, static generation), with this request's stores in scope (ADR 0013).

seeOther

function seeOther(location: string): Response;

303 See Other: the redirect that turns a POST into a GET (no resubmit on reload).

styleHashes

function styleHashes(): Promise<readonly string[]>;

'sha256-…' hashes of every registered shadow component's declarative-shadow-root <style>, for a Content-Security-Policy style-src.

wantsJson

const wantsJson: (request: Request) => boolean

Whether the request asks for JSON rather than a page (Accept: application/json without text/html): submitForm and other fetch calls do, a browser's form post doesn't. The rule formAction answers by; for your own routes that serve both.

Types and interfaces

You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.

Name Summary
ComponentAssets The client side of automatic component loading, for renderPage({ components }): from componentsFromManifest() in production, handed over by productionServer and gyralDevServer.
ComponentModule One component's client module: what to preload and link when the page renders its tag.
CspDirectives CSP directives by name: a source list, as one string or a list of sources.
CspOptions
FormActionHandlers
FormReject A server-side rejection of a schema-valid submission (see rejectWith).
PageOptions The page shell's options.
RenderOptions

ComponentAssets

interface ComponentAssets {
    /** URL of the loader module, which imports each rendered tag's module when it's in the page. */
    readonly loader: string;
    /** Tag → its client module. A rendered tag missing here won't hydrate (development warns). */
    readonly modules: ReadonlyMap<string, ComponentModule>;
    /** The loader's own imports, preloaded with it (production). */
    readonly preload?: readonly string[];
    /** More module scripts for pages with components (development: Vite's client). */
    readonly scripts?: readonly string[];
}

The client side of automatic component loading, for renderPage({ components }): from componentsFromManifest() in production, handed over by productionServer and gyralDevServer.

ComponentModule

interface ComponentModule {
    /** The module's chunk and its static imports (production; empty in development). */
    readonly preload: readonly string[];
    /** Hashed CSS the module imports (production; empty in development). */
    readonly stylesheets: readonly string[];
}

One component's client module: what to preload and link when the page renders its tag.

CspDirectives

type CspDirectives = Readonly<Record<string, string | readonly string[]>>;

CSP directives by name: a source list, as one string or a list of sources.

CspOptions

interface CspOptions {
    /**
     * Precomputed component `<style>` hashes from `styleHashes()` at build time. Append them
     * to registered component hashes, allowing a policy-only server to read JSON metadata
     * without executing every component module. Regenerate with each release's actual CSS.
     */
    readonly componentStyleHashes?: readonly string[];
    /**
     * Your other directives (`default-src`, `script-src`, …). A `style-src` given here is kept
     * and the hashes are appended; without one it is `'self'` plus the hashes.
     */
    readonly directives?: CspDirectives;
    /**
     * The page's global styles, exactly as passed to `renderPage({ styles })`, hashed too. In
     * `renderPage({ csp })` it defaults to the page's own `styles`.
     */
    readonly styles?: string | readonly string[];
    /**
     * The page's inline head scripts, exactly as passed to `renderPage({ headScripts })`, allowed in
     * `script-src` by hash (a `script-src` given in `directives` is kept and the hashes appended;
     * without one it is `'self'` plus the hashes). In `renderPage({ csp })` it defaults to the
     * page's own `headScripts`.
     */
    readonly scripts?: string | readonly string[];
    /**
     * `'hash'` (`renderPage` only, ADR 0020): allow the `style` attributes the page writes by
     * hash, in `style-src-attr` with `'unsafe-hashes'`, so they apply on first paint under a
     * strict policy. The page is rendered to a string before the response is built (its
     * headers need every value), so the body isn't chunked. Covers static, bound, multi-part and
     * element-hook values, not `raw()` markup. A `style-src-attr` given in `directives` is kept;
     * when it allows `'unsafe-inline'`, no hashes are added (a hash would disable it).
     */
    readonly styleAttributes?: 'hash';
    /**
     * The most `style` attribute hashes one page's header lists (default 128, about 7 KB of
     * header; proxies reject large headers). Values past it are left out and logged once;
     * hydration applies them.
     */
    readonly maxStyleHashes?: number;
}

FormActionHandlers

interface FormActionHandlers<T> {
    /**
     * The form's `data-intent` name. It becomes `IntentRejected.intent`, exactly as on the
     * JS path, so the component's reducer can't tell the two apart.
     */
    readonly intent: string;
    /**
     * Valid submission: do the work, then usually `seeOther(url)` (Post/Redirect/Get), or return
     * `rejectWith(…)` when a server-only check fails.
     */
    readonly valid: (data: T, request: Request) => Response | FormReject | Promise<Response | FormReject>;
    /**
     * Invalid submission without JavaScript: re-render the page with the component's
     * `.initialMessages` set to `[rejected]`, typically with status 422. The component's own
     * `IntentRejected` reducer turns it into state, so the errors render through the same view as
     * with JavaScript. Leave it out for an endpoint only `submitForm` calls: every rejection is
     * then the JSON 422.
     */
    readonly invalid?: (rejected: IntentRejected, request: Request) => Response | Promise<Response>;
}

FormReject

interface FormReject {
    readonly _tag: 'FormReject';
    readonly issues: readonly FieldIssue[];
}

A server-side rejection of a schema-valid submission (see rejectWith).

PageOptions

interface PageOptions extends RenderOptions, Head {
    /** The hydratable app: an `html` template, usually one custom element. */
    readonly body: ChildValue;
    /**
     * Server-only head content the head model doesn't cover, written with `html` (icons that
     * never change, preconnect hints, …). Never touched by `setHead()`.
     */
    readonly extraHead?: ChildValue;
    /**
     * Global CSS for the document (your app's own stylesheet text, e.g. a `?raw` import), written
     * as `<style>` elements in the head. A `</style` inside the text is escaped, so it can't
     * close the element early. Trusted CSS only: never put user input here.
     */
    readonly styles?: string | readonly string[];
    /**
     * Stylesheet URLs, written as `<link rel="stylesheet">` before `styles`: in production,
     * `clientAssetsFromManifest()`'s `css` (the hashed CSS Vite emitted for what the client entry
     * imports), served immutable and allowed by `style-src 'self'` without hashes.
     */
    readonly stylesheets?: readonly string[];
    /** Module scripts to load, e.g. the client entry. */
    readonly scripts?: readonly string[];
    /**
     * Inline classic scripts written early in the head, after the managed head entries and
     * before any stylesheet, so they run before the page paints (a theme script that sets
     * `data-theme` from storage, say). Trusted code only. A `</script` inside the text is escaped.
     * `renderPage({ csp })` allows them in `script-src` by hash.
     */
    readonly headScripts?: string | readonly string[];
    /**
     * Modules to fetch early with `<link rel="modulepreload">`, written before `scripts`: in
     * production, `clientAssetsFromManifest()`'s `modulepreload` (the entry's static imports and
     * the hydration chunk), so hydration doesn't wait for extra round trips.
     */
    readonly modulepreload?: readonly string[];
    /**
     * `renderPage` only: the `Content-Security-Policy` header. Pass `contentSecurityPolicy()`'s
     * options (`{ directives }`; the page's `styles` are included) to build it when the page
     * renders, with every component registered by then and the `<style>` elements allowed by
     * hash; or a header value, e.g. from `await contentSecurityPolicy({ styles })` (development
     * warns when it lacks a registered component's hash).
     */
    readonly csp?: string | CspOptions;
}

The page shell's options. The Head fields (title, description, canonical, robots, meta, links, JSON-LD, lang, dir) are the managed head (ADR 0019): the same object a client router applies with setHead() after a navigation, so build both from one function.

RenderOptions

interface RenderOptions {
    /**
     * This request's store instances (ADR 0013). Components read them during the render; a
     * store not listed starts from its `init`. Create new instances per request.
     */
    readonly stores?: readonly AnyStoreInstance[];
    /**
     * Development markers and checks. Default: on when
     * `@gyral/core` resolves with the `development` condition (Vite's dev server, Vitest).
     */
    readonly dev?: boolean;
    /**
     * A component whose `init` or view throws (ADR 0024): by default the page goes on with that
     * component's error view (or an empty host the browser starts fresh) and the `GyralError` is
     * logged with `console.error`. A function hears each failure instead (logging, monitoring).
     * `'throw'` ends the render with the failure instead; `renderPage` then renders the whole page
     * first, so it throws before any byte is sent and the route can answer with an error page.
     */
    readonly onError?: ((error: GyralError) => void) | 'throw';
    /**
     * Automatic component loading: the page links the client modules of exactly the components
     * it rendered (their preload links and the loader), after the body, and nothing when it
     * rendered none. `productionServer` and `gyralDevServer` hand it to `createApp`.
     */
    readonly components?: ComponentAssets;
}

@gyral/ssr/static

Functions

Name Summary
assetHandler Serves content-hashed files from dir at prefix (/assets/ by default) for GET and HEAD, with cache-control: public, max-age=31536000, immutable, content-type by extension, content-length and x-content-type-options: nosniff.
clientAssetsFromManifest ClientAssets for entry, read from a Vite build manifest (build.manifest: true).
componentsFromManifest componentAssets read from a client build directory (dist/client): its .vite/manifest.json and the preset's .vite/gyral-components.json.
prerender Renders each path through the app (exactly what a request would get) and writes the HTML.
productionServer A production request handler (framework-neutral: mount its fetch in Hono, Node, …): - GET/HEAD /assets/*: content-hashed client build output, cached immutable (assetHandler); - GET/HEAD of a prerendered path: the static/ file, revalidated on every use; - everything else (other GETs, POSTs): the request-time app, no-cache unless it set one.
writeComponentStyleManifest After the client build, automatically import its discovered/listed component modules in a temporary SSR bundle and fresh Node process, then write their real styleHashes() as JSON.

assetHandler

function assetHandler(options: AssetHandlerOptions): AssetHandler;

Serves content-hashed files from dir at prefix (/assets/ by default) for GET and HEAD, with cache-control: public, max-age=31536000, immutable, content-type by extension, content-length and x-content-type-options: nosniff. Paths that are not under dir after decoding (.., encoded slashes leading out) or have a dot segment (.hidden) are refused; a malformed escape is a 400. Misses are 404 with cache-control: no-store, so a CDN never keeps a miss for a file the next deploy brings. Other methods get 405. A single-range Range request gets a 206 with that slice (media seeking: Safari won't play video without it); several ranges get the whole file. Hashed files never change, so If-Range isn't needed.

clientAssetsFromManifest

function clientAssetsFromManifest(manifestPath: string, entry: string, also?: readonly string[], options?: ManifestOptions): Promise<ClientAssets>;

ClientAssets for entry, read from a Vite build manifest (build.manifest: true). also: manifest keys of modules the page will import lazily (a route's module, by source path such as src/routes/product.ts), preloaded too, each with its static imports and CSS. options.base: Vite's base, which every URL starts with.

componentsFromManifest

function componentsFromManifest(clientDir: string, options?: ManifestOptions): Promise<ComponentAssets | undefined>;

componentAssets read from a client build directory (dist/client): its .vite/manifest.json and the preset's .vite/gyral-components.json. Undefined when the build wasn't made with components: true.

prerender

function prerender(options: PrerenderOptions): Promise<readonly PrerenderedPage[]>;

Renders each path through the app (exactly what a request would get) and writes the HTML. A non-200 response fails the build: an ssg route must render successfully.

productionServer

function productionServer<Env = unknown>(options: ProductionOptions<Env>): Promise<FetchApp<Env>>;

A production request handler (framework-neutral: mount its fetch in Hono, Node, …):

  • GET/HEAD /assets/*: content-hashed client build output, cached immutable (assetHandler);
  • GET/HEAD of a prerendered path: the static/ file, revalidated on every use;
  • everything else (other GETs, POSTs): the request-time app, no-cache unless it set one.

Its fetch passes the server's env (toNodeListener's { incoming, remoteAddress }) on to the app, so the app can read the client's address.

writeComponentStyleManifest

function writeComponentStyleManifest(options: ComponentStyleManifestOptions): Promise<ComponentStyleManifest>;

After the client build, automatically import its discovered/listed component modules in a temporary SSR bundle and fresh Node process, then write their real styleHashes() as JSON. This executes trusted application module initialization at BUILD time: modules must be SSR-safe. It is not AST guessing and must not run against untrusted source. Vite is loaded only when invoked; production reads the resulting JSON into CspOptions.componentStyleHashes.

Types and interfaces

You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.

Name Summary
AppAssets What productionServer hands createApp: the built entry and what pages link with it.
AssetHandler Answers a request under the prefix, undefined for any other path.
AssetHandlerOptions
ClientAssets What a server-rendered page loads: the client entry, the modules to preload, the CSS.
ComponentStyleManifest
ComponentStyleManifestOptions
FetchApp Anything with a fetch handler: a Hono app, or a plain function.
ManifestChunk One chunk of Vite's .vite/manifest.json (the fields serving uses).
ManifestOptions Where the build is served: Vite's base (default /), which the manifest's paths omit.
PageAssets The page-level parts of ClientAssets, named as renderPage takes them.
PrerenderedPage
PrerenderOptions
ProductionOptions
RenderMode How a route is rendered.
ViteManifest Vite's build manifest (build.manifest: true): chunks by source path or chunk key.

AppAssets

interface AppAssets extends PageAssets {
    /** The built client entry's URL; undefined when the build has none (components only). */
    readonly clientEntry: string | undefined;
    /**
     * For `renderPage({ components })` when the build was made with
     * `gyralVitePreset({ components: true })`: each page links exactly the components it rendered.
     */
    readonly components: ComponentAssets | undefined;
    /** `modulepreload` and `stylesheets` with `modules` added (cached per list). */
    readonly assets: (modules: readonly string[]) => PageAssets;
    /** `assets(modules).modulepreload`. */
    readonly preload: (modules: readonly string[]) => readonly string[];
}

What productionServer hands createApp: the built entry and what pages link with it.

AssetHandler

type AssetHandler = (request: Request) => Promise<Response | undefined>;

Answers a request under the prefix, undefined for any other path. Never throws.

AssetHandlerOptions

interface AssetHandlerOptions {
    /** The directory served under `prefix`, e.g. `dist/client/assets`. */
    readonly dir: string;
    /** The URL path prefix, with both slashes. Default `/assets/`. */
    readonly prefix?: string;
    /**
     * Keep served files in memory (hashed files never change): `true` (default) up to 64 MiB,
     * `{ maxBytes }` for another bound (least recently served files are dropped first), `false`
     * to read every request from disk. Misses are never cached.
     */
    readonly cache?: boolean | {
        readonly maxBytes: number;
    };
}

ClientAssets

interface ClientAssets {
    /** The entry chunk's URL, for `renderPage({ scripts })`. */
    readonly entry: string;
    /**
     * Chunks the entry is known to need, for `renderPage({ modulepreload })`: the entry itself first
     * (when there is anything else), then its static imports and Gyral's lazily loaded hydration
     * chunk with its imports. Without the hints the browser
     * finds them only after it has fetched and parsed the entry (a server-rendered page would
     * fetch the hydration chunk a round trip later still).
     */
    readonly modulepreload: readonly string[];
    /**
     * The hashed CSS files of those chunks, for `renderPage({ stylesheets })`: what Vite emitted for
     * CSS imported by the entry, its static imports and the `also` modules, each chunk's after
     * its imports' (module order), each file once.
     */
    readonly css: readonly string[];
}

What a server-rendered page loads: the client entry, the modules to preload, the CSS.

ComponentStyleManifest

interface ComponentStyleManifest {
    readonly schema: 1;
    /** Sorted, deduplicated, quoted CSP SHA-256 sources for actual serialized shadow styles. */
    readonly hashes: readonly string[];
}

ComponentStyleManifestOptions

interface ComponentStyleManifestOptions {
    /** Application root used by its client build. Its component modules must be SSR-safe. */
    readonly root: string;
    /** Client output from `gyralVitePreset({ components: true })`, including `.vite` metadata. */
    readonly clientDir: string;
    /** Standalone JSON deployment artifact; no component module is needed to read it later. */
    readonly outFile: string;
    /** Build-time aliases/plugins used by application modules. No config file is loaded. */
    readonly vite?: InlineConfig;
}

FetchApp

interface FetchApp<Env = unknown> {
    readonly fetch: (request: Request, env?: Env) => Response | Promise<Response>;
}

Anything with a fetch handler: a Hono app, or a plain function. env is what the server passes along (toNodeListener's { incoming, remoteAddress }); a prerender passes none.

ManifestChunk

interface ManifestChunk {
    readonly file: string;
    readonly imports?: readonly string[];
    readonly dynamicImports?: readonly string[];
    readonly css?: readonly string[];
}

One chunk of Vite's .vite/manifest.json (the fields serving uses).

ManifestOptions

interface ManifestOptions {
    /** Vite's `base`, e.g. `/app/` (a missing trailing slash is added). Default `/`. */
    readonly base?: string;
}

Where the build is served: Vite's base (default /), which the manifest's paths omit.

PageAssets

interface PageAssets {
    readonly modulepreload: readonly string[];
    readonly stylesheets: readonly string[];
}

The page-level parts of ClientAssets, named as renderPage takes them.

PrerenderedPage

interface PrerenderedPage {
    readonly path: string;
    readonly file: string;
    readonly status: number;
}

PrerenderOptions

interface PrerenderOptions {
    readonly app: FetchApp;
    /** URL paths to render, e.g. every `ssg` route of the route table. */
    readonly paths: readonly string[];
    /** Directory the HTML files go to (`/about` → `<outDir>/about/index.html`). */
    readonly outDir: string;
    /** Origin used for the requests (absolute URLs in canonical links etc.). */
    readonly origin?: string;
}

ProductionOptions

interface ProductionOptions<Env = unknown> {
    /** Build output: `client/` (Vite, with `.vite/manifest.json`) and `static/` (prerendered). */
    readonly distDir: string;
    /**
     * The client entry as named in the Vite manifest. Default `src/entry-client.ts`. A build made
     * with `gyralVitePreset({ components: true })` may have none: pages then load components
     * through `components` alone.
     */
    readonly entry?: string;
    /**
     * The directory served at `/assets/`. Default `<distDir>/client/assets`; point it at a
     * directory that keeps older releases' hashed files too, so tabs opened before a deploy keep
     * loading their chunks.
     */
    readonly assetsDir?: string;
    /**
     * Prerendered pages. Default `<distDir>/static`; `false` when the app prerenders nothing, so
     * page requests don't look for files first.
     */
    readonly staticDir?: string | false;
    /**
     * Vite's `base` when the build is served under a path (e.g. `/app/`): the entry, preload and
     * stylesheet URLs start with it, and assets are served at `<base>assets/`. Default `/`.
     */
    readonly base?: string;
    /** Keep served assets in memory (`assetHandler`'s `cache`). Default `true`. */
    readonly cache?: boolean | {
        readonly maxBytes: number;
    };
    /**
     * Builds the request-time renderer (the same app the prerender step used). Pass
     * `modulepreload` and `stylesheets` on to `renderPage` so pages preload what the entry needs
     * and link its hashed CSS. A page that imports modules lazily (a route's chunk) spreads
     * `assets(modules)` instead: both lists plus those modules, their imports and their CSS
     * (`clientAssetsFromManifest`' `also`). `preload(modules)` is `assets(modules).modulepreload`.
     */
    readonly createApp: (options: AppAssets) => FetchApp<Env>;
    /**
     * Sees every response before it goes out (built assets, prerendered pages and the app's), to
     * add headers such as a CSP or security headers, or cookies. Return the response to send.
     */
    readonly onResponse?: (response: Response, request: Request) => Response | Promise<Response>;
}

RenderMode

type RenderMode = 'ssg' | 'ssr' | 'csr';

How a route is rendered.

ViteManifest

type ViteManifest = Readonly<Record<string, ManifestChunk>>;

Vite's build manifest (build.manifest: true): chunks by source path or chunk key.

@gyral/ssr/node

Functions

Name Summary
gyralDevServer Starts the development server of a server-rendered app:
toNodeListener A node:http request listener that runs fetch for each request: - the Request gets the method, URL (on origin), headers and, for methods other than GET/HEAD, the body as a stream; its signal aborts when the client disconnects; - the Response body is piped with backpressure (a slow client pauses the stream, so a renderPage body renders as fast as it is read) and cancelled on disconnect; - HEAD sends the headers only; each set-cookie stays its own header; - an unparsable request target is a 400; a throwing handler a 500 (onError sees it); - the handler's second argument is a {@link NodeEnv}: { incoming, remoteAddress }.

gyralDevServer

function gyralDevServer<M, S = undefined>(options: DevServerOptions<M, S>): Promise<DevServer>;

Starts the development server of a server-rendered app:

await gyralDevServer({
  entry: '/server/app.ts',
  app: (mod: typeof import('./app.js'), { components }) => mod.createApp({ components }),
});

Client modules, CSS and HMR come from Vite (middleware mode); any other request renders through the app, streamed. A render error answers 500 with Vite's error overlay.

toNodeListener

function toNodeListener(fetch: FetchHandler, options?: NodeListenerOptions): NodeListener;

A node:http request listener that runs fetch for each request:

  • the Request gets the method, URL (on origin), headers and, for methods other than GET/HEAD, the body as a stream; its signal aborts when the client disconnects;
  • the Response body is piped with backpressure (a slow client pauses the stream, so a renderPage body renders as fast as it is read) and cancelled on disconnect;
  • HEAD sends the headers only; each set-cookie stays its own header;
  • an unparsable request target is a 400; a throwing handler a 500 (onError sees it);
  • the handler's second argument is a {@link NodeEnv}: { incoming, remoteAddress }.

Types and interfaces

You rarely write most of these yourself: TypeScript infers them from the functions above. Look one up when an example or a compiler error names it.

Name Summary
DevApp A request handler: a Hono app, productionServer's result, or { fetch }.
DevServer
DevServerContext What gyralDevServer hands the app factory with each freshly loaded module.
DevServerOptions
FetchHandler A fetch handler: productionServer(…).fetch, a Hono app's fetch, or a plain function.
NodeEnv What toNodeListener passes as the handler's second argument: the Node request and the client's address.
NodeListener A Node request listener: createServer(toNodeListener(app.fetch)).
NodeListenerOptions

DevApp

interface DevApp {
    readonly fetch: (request: Request, env: NodeEnv) => Response | Promise<Response>;
}

A request handler: a Hono app, productionServer's result, or { fetch }.

DevServer

interface DevServer {
    /** `http://host:port/` the server listens on. */
    readonly url: string;
    readonly server: Server;
    readonly vite: ViteDevServer;
    /** Stops the HTTP server and Vite. */
    readonly close: () => Promise<void>;
}

DevServerContext

interface DevServerContext<S> {
    /**
     * For `renderPage({ components })` when the Vite config uses `gyralVitePreset({ components })`:
     * the loader and the tags it knows. Undefined otherwise.
     */
    readonly components: ComponentAssets | undefined;
    /** The value `state` created once, kept across module reloads. */
    readonly state: S;
}

What gyralDevServer hands the app factory with each freshly loaded module.

DevServerOptions

interface DevServerOptions<M, S> {
    /** The server module, from Vite's root (e.g. `/server/app.ts`), loaded per request. */
    readonly entry: string;
    /** Builds the request handler from the module just loaded. */
    readonly app: (module: M, context: DevServerContext<S>) => DevApp;
    /**
     * State that must outlive module reloads (an in-memory database, say): created once, from the
     * first module loaded, and passed to every `app` call.
     */
    readonly state?: (module: M) => S;
    /** Default: the `PORT` environment variable, else 5173. `0` picks a free port. */
    readonly port?: number;
    /** Default: Node's (all interfaces). */
    readonly host?: string;
    /** The HMR WebSocket's port, so several dev servers can run side by side. Default Vite's. */
    readonly hmrPort?: number;
    /** `toNodeListener`'s `origin`: the public origin when absolute URLs matter. */
    readonly origin?: string;
    /** More Vite settings (root, configFile, plugins, …), merged under the server settings. */
    readonly vite?: InlineConfig;
}

FetchHandler

type FetchHandler = (request: Request, env: NodeEnv) => Response | Promise<Response>;

A fetch handler: productionServer(…).fetch, a Hono app's fetch, or a plain function. Under toNodeListener it also receives a {@link NodeEnv}; handlers that don't need it ignore it.

NodeEnv

interface NodeEnv {
    /** The Node request, for anything `Request` doesn't carry (socket, TLS details). */
    readonly incoming: IncomingMessage;
    /**
     * The peer's IP address (`req.socket.remoteAddress`), or `undefined` once the socket is gone.
     * Behind a proxy this is the proxy: read `X-Forwarded-For` only when the proxy is known.
     */
    readonly remoteAddress: string | undefined;
}

What toNodeListener passes as the handler's second argument: the Node request and the client's address. { incoming } is the shape Hono's Node adapter uses, so getConnInfo from @hono/node-server/conninfo works unchanged.

NodeListener

type NodeListener = (req: IncomingMessage, res: ServerResponse) => void;

A Node request listener: createServer(toNodeListener(app.fetch)).

NodeListenerOptions

interface NodeListenerOptions {
    /**
     * The origin request URLs are built on, e.g. `https://example.com` behind a proxy. Default:
     * `http://` (`https://` on a TLS socket) plus the request's `Host` header, which the client
     * chooses: set it when the app writes absolute URLs (canonical links, redirects).
     */
    readonly origin?: string;
    /**
     * Called when the handler throws or the response body fails, before the 500 (or, once the
     * headers are out, before the connection is cut). Default: `console.error`.
     */
    readonly onError?: (error: unknown, request: Request) => void;
}