defineMiddleware
Type the rules of middleware.ts - redirects, rewrites, response headers and guards that the Rust server runs before any Node code.
middleware.ts
import { defineMiddleware } from '@gio.js/core';
export default defineMiddleware({
redirects: [{ from: '/blog/:slug', to: '/posts/:slug', status: 308 }],
});Reference
defineMiddleware(rules) returns rules unchanged: it exists for the MiddlewareRules type, so your editor checks the shape. The file is middleware.ts (or middleware.js) at the project root, next to app/, and the rules are its default export.
| Key | Type | Default | Description |
|---|---|---|---|
redirects | { from, to, status? }[] | - | Answer a matching path with a redirect to to. status is 301, 302, 307 or 308; the server uses 302 when it is left out. |
rewrites | { from, to }[] | - | Serve to for a matching path while the browser keeps the URL it asked for. Routing and the page cache see the rewritten path. |
headers | { path, headers: Record<string, string> }[] | - | Set response headers on every response whose requested path matches, redirect and guard responses included. Every matching rule applies, and a rule's value replaces the response's own - except set-cookie, which is added next to the response's cookies. An empty value removes a default security header (x-frame-options, referrer-policy, CSP, ...) for the rule's paths. |
guards | MiddlewareGuard[] | - | Redirect (302) a request without a credential to redirectTo. { path, requireSession: true, redirectTo } verifies a session cookie's signature and expiry; { path, requireCookie: 'name', redirectTo } only checks that the cookie is present. With both, the session is read from the named cookie instead of gio_session. |
Patterns use the routing conventions: literal segments, :param for one segment and *rest for the rest of the path (possibly empty, so /admin/*rest covers /admin too). Captures fill the same names in to.
Behavior
- The worker loads the file at startup and sends the rules to the Rust server, which compiles them and runs them on every request after rate limiting and the CSRF check, before routing. No request reaches Node without passing them. The rules are reloaded each time the worker restarts - in development, on every source change.
- Per request: guards, then redirects, then rewrites, and
gio.tomlrules are checked beforemiddleware.tsrules in every phase. Every matching guard must admit the request; among redirects and among rewrites the first match wins. Header rules are applied independently, matched against the path that was asked for (not a rewritten one). - The original query string is kept: appended to a redirect's or a guard's
Location, and kept on a rewritten request. /_gio/*endpoints are never matched.
Validation
- Strict, like
gio.toml: a rule that cannot be enforced as written - an invalid pattern, a malformed field (requireSession: 'true'), an unknown key, aredirectToortothat is not a path on this site (//hostand/\hostare another site to a browser), a guard without a requirement - stops the worker at boot with every problem listed. Nothing is dropped while the app serves. - So does a file that throws while it loads, a default export that is not an object, and a file without a default export. See
middleware.tsvalidation.
Examples
All four rule kinds
middleware.ts
import { defineMiddleware } from '@gio.js/core';
export default defineMiddleware({
redirects: [
{ from: '/blog/:slug', to: '/posts/:slug', status: 308 },
],
rewrites: [
{ from: '/docs/*rest', to: '/api/docs/*rest' },
],
headers: [
{ path: '/api/*rest', headers: { 'cache-control': 'no-store' } },
],
guards: [
{ path: '/admin/*rest', requireSession: true, redirectTo: '/login' },
],
});Requests against this file:
text
GET /blog/hello?ref=x -> 308 Location: /posts/hello?ref=x
GET /admin/users?y=1 -> 302 Location: /login?y=1 (no valid session)
GET /api/posts/1 -> 200 Cache-Control: no-storeRules built from code
What only code can express is the reason to use middleware.ts over gio.toml:
middleware.ts
import { readFileSync } from 'node:fs';
import { defineMiddleware } from '@gio.js/core';
// { "/old-path": "/new-path", ... } exported from the CMS
const moved: Record<string, string> = JSON.parse(
readFileSync(new URL('./data/moved-pages.json', import.meta.url), 'utf8'),
);
export default defineMiddleware({
redirects: Object.entries(moved).map(([from, to]) => ({
from,
to,
status: 301 as const,
})),
});Good to know
- Declarative only. Unlike Next.js middleware, there is no function that runs per request: the rules are data that Rust evaluates. For per-request logic, use
getServerSideProps, a route handler or a Node plugin. - A file that fails to load stops the worker. When importing
middleware.tsthrows, the worker refuses to boot with<path>/middleware.ts failed to load: <error>: production exits 1, development waits for you to save a fix. Its guards are never dropped while the app serves. Seemiddleware.tsvalidation. - Guards do not pass the path on. The redirect keeps the query string but not the path that was asked for; read the path in the login page if you need a
?next=target. - Same rules, two homes. Everything here can also live in
gio.tomlas[[redirects]],[[rewrites]],[[headers]]and[[guards]](snake_case keys there:require_session,redirect_to).
Related
- Middleware - the pattern language, evaluation order, guards and header rules in depth
- middleware.ts
- [[redirects]], [[rewrites]], [[headers]], [[guards]]
- defineConfig
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | Guards gain requireSession, verified in Rust; a malformed rule or a file that throws stops the worker at boot instead of being dropped. *rest matches zero segments too. Header rules also apply to redirect and guard responses and match the requested path rather than a rewritten one. |
v0.1.0-beta.6 | Introduced. |