Route actions and CSR forms
Handle mutations with route actions, predictable response defaults, origin checks, and explicit pure-CSR form transport.
Define the mutation beside its route
A route action implements PageAction, receives PageActionContext, and may return a native Response or the documented data, redirect, or error object forms. defineAction adds origin and content-type policy to that handler.
tsximport {
defineAction,
type PageActionContext
} from "@tavojs/core/router";
import { Button, Page, Stack, TextField } from "@tavojs/ui";
type NewProjectInput = { name: string };
export const action = defineAction(
async ({ request }: PageActionContext) => {
const {
authorizeProjectCreation,
createProject,
requireUser
} = await import("src/server/projects");
const form = await request.formData();
const input: NewProjectInput = {
name: String(form.get("name") ?? "").trim()
};
const user = await requireUser(request);
authorizeProjectCreation(user);
const project = await createProject(input);
return {
redirect: `/projects/${project.id}`
};
},
{
contentType: "form-data"
}
);
export default function NewProjectPage() {
return (
<Page>
<form method="post">
<Stack>
<TextField name="name" label="Project name" required />
<Button type="submit">Create project</Button>
</Stack>
</form>
</Page>
);
}The Node route handler sends non-GET and non-HEAD requests to the matched page action before normal page rendering.
If the route has no action, the handler returns 405 Method Not Allowed with Allow: GET, HEAD.
A body above the configured request limit returns 413. An unhandled action failure becomes a generic 500 response.
Return a Response to control the full response, or return an
ActionResultforTavo.jsto normalize and harden.
Know the action response defaults
| Handler result | Default response |
|---|---|
undefined | 204 with an empty body |
{ redirect } | 303 with Location |
{ json } | 200 with application/json; charset=utf-8 and a serialized body |
{ body } | 200 when body is present; otherwise 204 |
Response | Its status, body, and headers are preserved; default security headers are added when absent |
An explicit status or headers value in these object forms overrides the corresponding default. External redirect targets are rejected unless the runtime explicitly enables them; validate any user-derived redirect again at the application boundary.
Apply transport checks before business logic
Unsafe action methods validate Origin against the request origin by default. Node-like requests also require a local or configured trusted host.
Do not set
validateOrigin: false for a browser form. Reserve it for endpoints with an independent authenticity mechanism, such as a verified webhook signature.contentType: "json" accepts application/json and +json media types.contentType: "form-data" accepts multipart/form-data and application/x-www-form-urlencoded.A declared content-type mismatch returns 415 before the handler runs.
Parsing or schema validation proves shape, not identity or permission. Authenticate, authorize, enforce CSRF or idempotency policy, and then commit the mutation.
Do not return secrets, private exception messages, or raw database failures in action bodies.
Configure delegated forms only for pure CSR boot
In a server-rendered document, a normal form posts to its matched route action. A pure CSR document can opt into delegated action transport through bootTavo and its csrActions option so the browser submits to an available backend endpoint.
tsximport { bootTavo } from "@tavojs/core";
void bootTavo({
csrActions: {
enabled: true,
baseUrl: "https://api.example.com",
credentials: "include",
headers: {
"X-Requested-With": "Tavo.js"
}
}
});tstype CsrActionContext = {
pathname: string;
search: string;
form?: HTMLFormElement;
};
type CsrActionsOptions = {
enabled?: boolean;
baseUrl?: string;
resolveUrl?: (context: CsrActionContext) => string;
credentials?: RequestCredentials;
headers?:
| HeadersInit
| ((context: {
pathname: string;
form: HTMLFormElement;
}) => HeadersInit);
};Delegation applies to non-GET, same-window forms that are not explicitly external.
The browser sends
FormDatawith the form method. credentials defaults to include when not configured.data-tavo-nativeopts one form out so the browser performs its native submission.A same-origin redirect becomes replace navigation. Other redirect destinations use a full browser navigation.
CSR action delegation is not enabled during SSR hydration. This prevents the hydrated application from installing a competing form transport.
A pure CSR form without
csrActionsemits a development warning unless it is markeddata-tavo-native.
Verify the complete mutation path
Submit valid multipart and URL-encoded forms, then test the wrong media type and expect 415.
Send a missing or cross-origin Origin header according to the deployment contract and verify 403 where required.
Verify no action returns 405, an oversized body returns 413, and internal failures return a generic 500.
Test redirect, JSON, body, empty, and direct Response results with their exact status and headers.
For pure CSR, verify endpoint mapping, credentials, native opt-out, same-origin replace navigation, and full external navigation.
bashnpx tavo inspect route /projects/new --json
npx tavo buildLook up exact public types
Follow linked API names to their canonical TypeScript declarations and package boundaries.