GioJSdocs
On this page

gio.config.ts

The optional JavaScript config file: Node plugins that run around every request the render worker handles. Everything declarative belongs in gio.toml.

gio.config.ts
import { defineConfig } from '@gio.js/core';
import { internalOnly, renderTiming } from './lib/plugins';

export default defineConfig({
  plugins: [internalOnly, renderTiming],
});

Reference

The file is gio.config.ts or gio.config.js (checked in that order) in the project root, next to gio.toml - the parent of app/ (or of GIO_APP_DIR). Its default export is a GioConfig; defineConfig returns its argument unchanged and only adds the type.

Keys

KeyTypeDefaultDescription
pluginsGioNodePlugin[][]Node plugins, run in array order around every request the worker handles. See GioNodePlugin.

That is the only key. Server settings - limits, headers, redirects, guards, caching - are gio.toml keys, and declarative rules can also live in middleware.ts.

GioNodePlugin

ts
import type { GioNodePlugin, IPCRequest, IPCResponse } from '@gio.js/core';

interface GioNodePlugin {
  name: string;
  version: string;
  onRequest?: (req: IPCRequest) => Promise<IPCRequest | IPCResponse>;
  onResponse?: (req: IPCRequest, res: IPCResponse) => Promise<IPCResponse>;
  onStartup?: () => Promise<void>;
  onShutdown?: () => Promise<void>;
}
FieldTypeDefaultDescription
name (required)string-Names the plugin in error logs and in the 500 body when a hook throws.
version (required)string-Your plugin's version (required by the type, informational).
onRequestfunction-Runs before routing. Return the request (changed or not) to continue, or a response to answer at once - later plugins and the page are skipped.
onResponsefunction-Runs on the finished response; return it, changed or not.
onStartupfunction-Runs when a worker process starts, before it serves.
onShutdownfunction-Runs when a worker process stops, plugins in reverse order.

IPCRequest carries method, path, query, lowercased headers, body, locale, and when known ip, scheme, host and requestId. IPCResponse carries status, headers, body, cacheable, cacheMaxAge and setCookies.

Behavior

  • Loading. Each worker imports the file at boot through tsx, so it can import TypeScript from your project. A missing file means no plugins.
  • Validation. The export is checked at boot: anything but an object, an unknown key, a plugins that is not an array, or a plugin without a string name stops the worker with an error naming the file: gio.config.ts: unknown key "plugin" - did you mean "plugins"? (gio.config takes plugins; server settings belong in gio.toml). The server prints it and exits 1 (see worker boot errors).
  • Which requests. Every request the Node worker handles: page renders (background revalidations included), page actions, route handlers and metadata routes. Requests Rust answers alone never reach a plugin: cache hits, public/ files and chunks, /_gio/image, and requests that a guard, redirect or rate limit from gio.toml / middleware.ts already answered. WebSocket messages do not go through plugins.
  • Errors. An onRequest that throws answers 500 with Internal Server Error (plugin: <name>). An onResponse that throws is logged and skipped: the response goes on as it was. Neither crashes the worker.
  • Streaming. An onResponse hook must see the whole body, so while any plugin has one, page renders do not stream (partial prerendering included) and route handlers stream only event streams. Streamed route-handler responses skip onResponse.
  • Caching. What onResponse returns is what Rust caches, so a cache hit replays it without running the hook again. An onRequest that reads credentials (cookies, authorization, the client address) and then rewrites the path, query or locale makes that render per-visitor: it is not cached. A page that reads a header a plugin changed is not cached either.
  • Workers. With [server] workers above 1, every worker loads the plugins and runs onStartup - and again when a worker is respawned. GIO_WORKER_INDEX is "0" in one worker per server.

Examples

Refuse a path from onRequest

lib/plugins.ts
import type { GioNodePlugin } from '@gio.js/core';

/** Answers /internal/* with 403 unless the request carries the ops key. */
export const internalOnly: GioNodePlugin = {
  name: 'internal-only',
  version: '1.0.0',
  async onRequest(req) {
    if (!req.path.startsWith('/internal/')) return req;
    const key = process.env.OPS_KEY;
    if (key && req.headers['x-ops-key'] === key) return req;
    return {
      id: req.id,
      status: 403,
      headers: { 'content-type': 'text/plain; charset=utf-8' },
      body: 'Forbidden',
      cacheable: false,
      cacheMaxAge: 0,
    };
  },
};

A response from onRequest must echo req.id; keep cacheable: false for an answer that depends on the request. Check that the key is set: with OPS_KEY unset, comparing it to a missing header would compare undefined with undefined and let everyone in. For a plain cookie or role check, a [[guards]] rule in gio.toml runs in Rust before Node and needs no plugin.

Add a header in onResponse

lib/plugins.ts
import type { GioNodePlugin } from '@gio.js/core';

export const renderTiming: GioNodePlugin = {
  name: 'render-timing',
  version: '1.0.0',
  async onRequest(req) {
    req.headers['x-render-start'] = String(Date.now());
    return req;
  },
  async onResponse(req, res) {
    const start = Number(req.headers['x-render-start']);
    return { ...res, headers: { ...res.headers, 'server-timing': `render;dur=${Date.now() - start}` } };
  },
};
text
$ curl -sI http://localhost:3000/about | grep -i server-timing
server-timing: render;dur=51

Strip cookies from a response

lib/plugins.ts
import type { GioNodePlugin } from '@gio.js/core';

export const noCookiesOnPublic: GioNodePlugin = {
  name: 'no-cookies-on-public',
  version: '1.0.0',
  async onResponse(req, res) {
    return req.path.startsWith('/public-api/') ? { ...res, setCookies: [] } : res;
  },
};

Cookies a page or route handler sets arrive in res.setCookies, one entry per Set-Cookie, never in res.headers['set-cookie']. Append to the array to add one.

Run a startup job once per server

lib/plugins.ts
import type { GioNodePlugin } from '@gio.js/core';
import { primeSearchIndex } from './search';

export const warmup: GioNodePlugin = {
  name: 'warmup',
  version: '1.0.0',
  async onStartup() {
    if (process.env.GIO_WORKER_INDEX !== '0') return;
    await primeSearchIndex(); // keep it idempotent: worker 0 can be respawned
  },
};

Good to know

  • giojs-server --check-config and gio doctor do not load gio.config.ts. A validation error shows when the worker boots: the worker exits with the message above in the log, and the server exits once it gives up waiting for a worker.
  • In gio dev, editing gio.config.ts or a module it imports restarts the worker. gio build standalone bundles it into worker.js, and the testing kit (renderPage, callRoute) runs your plugins too.
  • Prefer gio.toml or middleware.ts rules for redirects, rewrites, headers and guards: they run in Rust for every request, cache hits included, and cost no Node time.
  • There is no Rust plugin API in gio.config.ts; it configures the Node worker only.

Version history

VersionChanges
v0.1.0-beta.8Validated at boot (unknown keys and plugins without a name are errors). defineConfig and type GioConfig exported from @gio.js/core. onStartup / onShutdown run in every worker of a pool. Cookies reach onResponse in res.setCookies. Renders an onRequest plugin personalized are no longer cached.
v0.1.0-beta.1Introduced: plugins with onRequest / onResponse / onStartup / onShutdown; gio.config.js is read when there is no gio.config.ts.