Server routes
Expose deliberate HTTP endpoints through the plugin server boundary using standard web requests and responses.
Choose a server route for an HTTP endpoint
Use a page action when a mutation belongs to one application route. Use a plugin server route for a reusable API, webhook, health check, or integration endpoint that should respond before page rendering.
Register methods in a plugin
Declare every endpoint in the plugin manifest, then implement the same endpoint keys in a lazy server phase. Handlers receive a standard Request and return a terminal Response.
Create project-api.ts
tsimport { definePlugin, definePluginPhase } from "@tavojs/core/plugin";
export const projectApi = definePlugin({
id: "@project/api",
version: "1.0.0",
apiVersion: 1,
manifest: {
endpoints: [
{
id: "summary",
methods: ["GET"],
match: { kind: "exact", path: "/api/projects/summary" },
},
{
id: "collection",
methods: ["GET", "POST"],
match: { kind: "exact", path: "/api/projects" },
},
{
id: "project",
methods: ["GET"],
match: { kind: "subtree", path: "/api/projects" },
},
],
exposure: [
{
target: "server",
from: "/api/projects",
to: "/api/projects",
reason: "Expose the documented project API.",
},
],
},
server: () =>
definePluginPhase({
endpoints: {
summary: async () => {
const { projectSummary } = await import("./project-store.server");
return Response.json(await projectSummary());
},
collection: async ({ request }) => {
const store = await import("./project-store.server");
if (request.method === "POST") {
await store.requireProjectPermission(request);
const input = store.parseProjectInput(await request.json());
return Response.json(await store.createProject(input), {
status: 201,
});
}
const status =
new URL(request.url).searchParams.get("status") ?? "all";
return Response.json(await store.listProjects(status));
},
project: async ({ request }) => {
const { getProject } = await import("./project-store.server");
const id = decodeURIComponent(
new URL(request.url).pathname.split("/").at(-1)!,
);
const project = await getProject(id);
return project
? Response.json(project)
: Response.json({ error: "Project not found" }, { status: 404 });
},
},
}),
});
Enable the plugin
Defining a plugin does not expose it by itself. Add it to the top-level plugins array in tavo.config.ts; the manifest exposure maps its declared server path onto the application's public URL tree.
Merge into tavo.config.ts
tsimport { defineConfig } from "@tavojs/core/config";
import { projectApi } from "./project-api";
export default defineConfig({
pagesDir: "src/pages",
plugins: [projectApi],
});
Keep route matching explicit
Use exact matchers for individual URLs and subtree matchers for a deliberate route tree. Read query values and dynamic path pieces from request.url, validate them before use, and keep response formats stable for clients.
Return 405 when an endpoint does not support the incoming method.
Return deliberate content-type and cache headers.
Keep database and secret-bearing clients in server-only modules.
Bound request bodies and remote work with deployment-appropriate limits.
Preserve the request security boundary
Unsafe methods validate their origin by default. Disable that check only for independently authenticated endpoints such as verified webhooks, and authenticate and authorize every protected operation inside the handler.