Guard
A guard function runs on every request to a route, before any server data is fetched. It is the enforced access control point — unauthorized requests are redirected before any database queries or data fetchers execute.
Basic usage
A guard function on any spec receives the same ctx object as server data fetchers — params, query, headers, and cookies.
export default {
route: '/dashboard',
guard: async (ctx) => {
if (!ctx.cookies.session) return { redirect: '/login' }
},
server: {
user: async (ctx) => getCurrentUser(ctx.cookies.session),
},
state: {},
view: (state, server) => `
<main id="main-content">
<h1>Welcome, ${server.user.name}</h1>
</main>
`,
}
When the guard returns { redirect }, the server responds with a 302 and all server data fetchers are skipped — no data is fetched for unauthorized requests. When the guard returns nothing, the request proceeds normally.
What ctx contains
| Property / Method | Type | Description |
|---|---|---|
ctx.cookies | object | Parsed cookies from the Cookie header |
ctx.headers | object | Raw request headers |
ctx.params | object | Route params e.g. { id: "42" } |
ctx.query | object | Parsed query string |
ctx.pathname | string | URL path e.g. /dashboard |
ctx.method | string | HTTP method e.g. GET, POST |
ctx.store | object | Resolved global store state (if a store is registered) |
ctx.nonce | string | CSP nonce for the current request |
await ctx.json() | object | null | Parse a JSON request body |
await ctx.text() | string | Read the body as a plain string |
await ctx.formData() | object | null | Parse a URL-encoded body into a plain object |
await ctx.buffer() | Buffer | Read the raw body as a Node.js Buffer |
Common patterns
Session check
Redirect to login when no session cookie is present.
guard: async (ctx) => {
if (!ctx.cookies.session) return { redirect: '/login' }
}
Role-based access
Fetch the user from the session and check their role. Keep the lookup fast — guard runs on every request to the route.
guard: async (ctx) => {
const user = await getUserFromSession(ctx.cookies.session)
if (!user) return { redirect: '/login' }
if (!user.isAdmin) return { redirect: '/403' }
}
Redirect authenticated users away from login
Useful for login and signup pages — send already-authenticated users somewhere useful.
export default {
route: '/login',
guard: async (ctx) => {
if (ctx.cookies.session) return { redirect: '/dashboard' }
},
state: {},
view: () => `<main id="main-content">...</main>`,
}
Custom status responses
Guard can return a custom HTTP response instead of (or alongside) a redirect. Return { status, json?, body?, headers? } to send any status code with an optional JSON or text body. This is useful for POST handlers that need to signal validation errors or API-style rejections:
guard: async (ctx) => {
const token = ctx.headers.authorization
if (!token) return { status: 401, json: { error: 'Unauthorized' } }
if (ctx.method === 'POST') {
const data = await ctx.formData()
if (!data.email) return { status: 422, json: { error: 'Email required' } }
await db.leads.create(data)
return { redirect: '/contact?sent=1' }
}
// return nothing to let a GET request proceed to the view
}
guard as a POST handler, the spec must declare methods: ['GET', 'POST']. Without it, POST requests are rejected with 405 before guard runs.Reference
| Property | Type | Required |
|---|---|---|
guard | async (ctx) => { redirect?: string } | { status, json?, body?, headers? } | void | No |