Server Actions

Write a mutation as a server function, call it from the client, and revalidate the cache it affects — all type-checked, CSRF-protected, and served at one endpoint.

Defining an action

Wrap a server function with action(). Give it a stable id so the client can dispatch it, and optionally declare what to revalidate on success.

// src/actions/posts.js
import { action, revalidatePath } from 'what-framework/server';
import { createPost } from '../db.js';

export const createPostAction = action(
  async ({ title, body }) => {
    const post = createPost({ title, body });
    return { ok: true, slug: post.slug };
  },
  { id: 'createPost', revalidate: ['/'], revalidateTags: ['posts'] }
);

Serving actions

Actions are dispatched over POST /__what_action. The deploy adapters mount this for you; importing the action module (e.g. from your routes file) registers it. With a manual server you can mount it directly:

import { createActionHandler } from 'what-framework/server';
const handler = createActionHandler({ getCsrfToken });

Calling from the client

A progressively-enhanced form works with no JavaScript — it POSTs to the endpoint with a data-action attribute:

// src/pages/new-post.jsx — submits even before any client JS loads
export default function NewPost() {
  return (
    <form method="post" action="/__what_action" data-action="createPost">
      <input name="title" required />
      <textarea name="body" required></textarea>
      <button>Publish</button>
    </form>
  );
}

Or call it imperatively and get the typed return value:

const res = await fetch('/__what_action', {
  method: 'POST',
  headers: { 'x-what-action': 'createPost', 'content-type': 'application/json' },
  body: JSON.stringify({ args: [{ title, body }] }),
});
const { slug } = await res.json();

Revalidating after a mutation

The revalidate / revalidateTags options fire automatically after the action resolves, purging the origin ISR cache (and any CDN) so the next request re-renders with fresh data. You can also call them by hand inside the action:

import { revalidatePath, revalidateTag } from 'what-framework/server';

revalidatePath('/');          // purge one path
revalidateTag('posts');       // purge everything tagged 'posts'

Two revalidates, on purpose

Server revalidatePath/revalidateTag (cache) are distinct from the client router's invalidatePath (in-memory nav pub-sub). Server purges the rendered cache; the client one re-runs a client loader. See Caching & ISR.

CSRF & error masking

The handler validates a CSRF token by default (inject one via getCsrfToken; a meta tag is emitted into the document). It fails closed: a missing or bad token is rejected. Thrown errors are masked to a generic 500 so internal details never reach the client — the real error is logged server-side.