GitHub

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 / MethodTypeDescription
ctx.cookiesobjectParsed cookies from the Cookie header
ctx.headersobjectRaw request headers
ctx.paramsobjectRoute params e.g. { id: "42" }
ctx.queryobjectParsed query string
ctx.pathnamestringURL path e.g. /dashboard
ctx.methodstringHTTP method e.g. GET, POST
ctx.storeobjectResolved global store state (if a store is registered)
ctx.noncestringCSP nonce for the current request
await ctx.json()object | nullParse a JSON request body
await ctx.text()stringRead the body as a plain string
await ctx.formData()object | nullParse a URL-encoded body into a plain object
await ctx.buffer()BufferRead 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>`,
}
Guard runs server-side on every request, including client-side navigation requests — those go through the same server pipeline. There is no way to bypass guard from the browser.

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
}
To use guard as a POST handler, the spec must declare methods: ['GET', 'POST']. Without it, POST requests are rejected with 405 before guard runs.

Reference

PropertyTypeRequired
guardasync (ctx) => { redirect?: string } | { status, json?, body?, headers? } | voidNo