Action Guards

← docs

Action Guards

Action guards are middleware-style functions that run before a server action executes. Use them for authentication, authorization, rate limiting, or any cross-cutting concern that should halt execution before the action body runs.

How it works

Export an actionGuards array from your .server.ts file alongside serverActions. actionsHandler runs the guard chain before dispatching to the action function. Guards that call next() allow the request through; guards that throw ActionGuardError reject it with a 4xx response.

Guards run for every action in the module, so define per-module guards for blanket protection and rely on action-level logic for fine-grained rules.

Defining guards

// src/pages/movies.server.ts
import { defineAction, defineActionGuard, ActionGuardError } from '@hono-preact/iso';
import type { Context } from 'hono';
import { getCurrentUser } from '@/server/auth.js';

export const actionGuards = [
  defineActionGuard(async ({ c }, next) => {
    const user = await getCurrentUser(c as Context);
    if (!user) throw new ActionGuardError('Authentication required', 401);
    return next();
  }),
];

export const serverActions = {
  addMovie: defineAction<{ title: string }, { ok: boolean }>(
    async (_ctx, payload) => {
      await db.insert({ title: payload.title });
      return { ok: true };
    }
  ),
  deleteMovie: defineAction<{ id: string }, { ok: boolean }>(
    async (_ctx, { id }) => {
      await db.delete(id);
      return { ok: true };
    }
  ),
};

Both addMovie and deleteMovie run through the guard chain before executing.

API

defineActionGuard

import { defineActionGuard } from '@hono-preact/iso';

const guard = defineActionGuard(async (ctx, next) => {
  // inspect ctx, then either throw or call next()
  await next();
});

defineActionGuard is a no-op at runtime — it exists only for type inference. A guard receives:

PropertyTypeDescription
ctx.cunknownThe Hono Context for the request. Cast to import type { Context } from 'hono' to access headers, cookies, etc.
ctx.modulestringThe module name derived from the .server.ts filename.
ctx.actionstringThe name of the action being called.
ctx.payloadunknownThe deserialized request payload.

ActionGuardError

Throw ActionGuardError to reject the request with an HTTP error response:

throw new ActionGuardError('Not allowed');         // 403 by default
throw new ActionGuardError('Unauthorized', 401);   // custom status
throw new ActionGuardError('Rate limited', 429);

The error message is returned as { "error": "..." } JSON. The status field sets the HTTP response status.

next()

Call next() to pass control to the next guard in the chain, or to the action if no more guards remain. A guard that returns without calling next() and without throwing will still allow the action to proceed — the only way to block is to throw.

Guard chains

Guards run in array order. The first to throw stops the chain — subsequent guards and the action are not called:

export const actionGuards = [
  requireAuth,        // runs first
  requireRole('editor'), // only runs if requireAuth passes
];

Composing guards

Guards are plain functions — extract and reuse them across modules:

// src/server/guards.ts
export const requireAuth = defineActionGuard(async ({ c }, next) => {
  const user = await getCurrentUser(c as Context);
  if (!user) throw new ActionGuardError('Authentication required', 401);
  return next();
});
// src/pages/admin.server.ts
import { requireAuth } from '@/server/guards.js';

export const actionGuards = [requireAuth];

See Route Guards — Composing guards for the full composition pattern — the same approach applies to action guards.

The server/client boundary

serverOnlyPlugin replaces any imported actionGuards in the client bundle with an empty array — guard logic never reaches the browser. serverLoaderValidationPlugin enforces that actionGuards is one of only three permitted named exports from .server.* files (serverGuards, serverActions, actionGuards). See Overview — The server/client boundary for the full explanation.