Guards & responses
A page is a render; it does not write the response. Whether a request gets through, and what the answer carries beyond the page, is decided before the page by a guard.ts. This page covers writing one, ending a request and letting it through, adding to the answer with responseHeaders(), and what a guard covers.
guard.ts
A guard.ts can sit in a directory at any level, and runs before whatever answers below that directory. When several lie along a URL, they run outer first, one at a time.
src/routes/
layout.tsx
guard.ts
page.tsx
admin/
guard.ts
page.tsx
[id]/
page.tsx// src/routes/admin/guard.ts
import type { Guard } from '@k8ordo/server/runtime';
const guard: Guard<'/admin'> = ({ request }) => {
if (request.headers.get('cookie')?.includes('session=') === true) return;
return new Response(null, { status: 303, headers: { location: '/login' } });
};
export default guard;It receives { request, params }: the Request as it arrived, and the params of the pattern its directory puts it under, as the strings the URL carried — a guard runs above every layout, before any schema has typed them. The type is Guard<P> from @k8ordo/server/runtime, and the generated table checks each guard.ts against its directory’s pattern either way.
Ending the request, or letting it through
Returning a Response makes it the answer — a redirect, a 401, a 403, whatever it is — and neither the guards inside it nor the page below run. Returning nothing hands the request on to the next guard, and after the last one to whatever answers the URL.
Adding to the answer
Letting a request through can still add to its answer. responseHeaders() is the Headers the final response will carry, whatever answers — the page, its payload, the not-found, or a guard further in that ends the request.
// src/routes/guard.ts
import { responseHeaders } from '@k8ordo/server/runtime';
export default function guard() {
responseHeaders().set('x-content-type-options', 'nosniff');
}A header the answer already carries is replaced. responseHeaders() works while a guard runs and throws anywhere else: a page is a render, and a render that wrote the response would be a second handler.
There is no next()
There is no next() that runs the page and hands its answer back to be rewritten. A page streams, and its headers are on the wire before its body is written — so a guard decides before the page starts, never after.
What a guard covers
A guard runs before every URL below its directory:
- each page, and its payload for a client navigation
- a
HEADfor it - a Server Action posted to it
- a
not-found.tsxbelow it — and the root’s guard also runs for a URL nothing answers
A redirect.ts is answered before any guard runs: a directory that redirects has nothing below it to guard.
A guard does not protect a Server Action as such: an action is a function any page can call, posted to whichever URL calls it. An action checks what it needs itself. Actions & requests
When it runs
The guards run after the params schemas have matched the URL, and before the Server Action a POST carries and before the render — so a guard under [locale] runs in the locale the URL names.
Under @k8ordo/static
A file has no request to guard. @k8ordo/static refuses a guard.ts by name, in the build and in vite dev.