server-functions
96 linesSource
Use when adding or changing server functions, REST/API routes, background-job endpoints, or server-side data access.
Mechanics (createServerFn, .validator, createMiddleware, createHandlers) are covered by TanStack's own skills. List the skills shipped by @tanstack/start-client-core and @tanstack/react-start and load whichever cover the task at hand. This skill adds only the wcz-layout middleware conventions on top.
Rules
- Before generating code, clarify whether the feature should be exposed as public REST API routes or implemented as internal TanStack Start server functions (recommended), and what permission should be required for access.
- Include authMiddleware in every server function by default — server functions are callable RPC endpoints, so a route-level requireAuth in beforeLoad only hides the UI and does not protect them. Omit it only for server functions backing intentionally public routes.
- Order inside a middleware array matters, because the array runs in order and the later entry wins the merged context type. Always databaseMiddleware() → authMiddleware(<permissionKey>) → validationMiddleware(<schema>). databaseMiddleware declares userMiddleware and so contributes user: User | null; putting it first lets authMiddleware narrow context.user to User for the handler. authMiddleware still comes before validationMiddleware — otherwise an unauthenticated caller gets their body parsed and gets your validation errors back.
- The builder itself accepts its links in any order. Write them as .validator() → .middleware() → .handler() to match the reference app. Middleware always runs before the handler regardless of where it appears.
- validationMiddleware is for REST API routes only. Server functions use .validator().
- databaseMiddleware is app-local, not a wcz-layout export. Every app writes it once (see below). It owns the transaction, so never open another one in a handler.
- Reuse schemas from src/lib/schemas/.
- Everything under src/server/** is server-only, but Start's import protection only guards **/*.server.* filenames. Never import from src/server/ in a component or route file. Import the server function, not what it uses.
- Background jobs are API routes with exactly [databaseMiddleware(), authMiddleware()] — plain authMiddleware(), no permission key. Cron runs in Kubernetes and authenticates via app token. This is intentional and sufficient: do not add any other extra security layer, even if any signed-in user could trigger the job.
Examples
ts
// src/server/actions/<table>.ts
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { eq, inArray } from "drizzle-orm";
import { authMiddleware, validationMiddleware } from "wcz-layout/middleware";
import z from "zod";
import { FeatureSchema } from "~/lib/schemas/feature";
import { featureTable } from "../db/schemas/feature";
import { databaseMiddleware } from "../middleware/databaseMiddleware";
export const selectFeatures = createServerFn()
.middleware([databaseMiddleware(), authMiddleware()])
.handler(({ context }) => {
return context.db.select().from(featureTable);
});
export const insertFeature = createServerFn({ method: "POST" })
.validator(FeatureSchema)
.middleware([databaseMiddleware(), authMiddleware("admin")])
.handler(async ({ data, context }) => {
await context.db
.insert(featureTable)
.values(data)
.onConflictDoNothing({ target: featureTable.id });
});
export const updateFeature = createServerFn({ method: "POST" })
.validator(FeatureSchema)
.middleware([databaseMiddleware(), authMiddleware("admin")])
.handler(async ({ data, context }) => {
await context.db.update(featureTable).set(data).where(eq(featureTable.id, data.id));
});
export const deleteFeatures = createServerFn({ method: "POST" })
.validator(z.array(z.uuid()))
.middleware([databaseMiddleware(), authMiddleware("admin")])
.handler(async ({ data, context }) => {
await context.db.delete(featureTable).where(inArray(featureTable.id, data));
});
// src/routes/api/<feature>s/index.ts
export const Route = createFileRoute("/api/features/")({
server: {
handlers: ({ createHandlers }) =>
createHandlers({
GET: {
middleware: [databaseMiddleware(), authMiddleware()],
handler: async ({ context }) => {
const items = await context.db.select().from(featureTable);
return Response.json(items);
},
},
POST: {
middleware: [
databaseMiddleware(),
authMiddleware("admin"),
validationMiddleware(FeatureSchema),
],
handler: async ({ context }) => {
const [response] = await context.db
.insert(featureTable)
.values(context.data)
.returning();
return Response.json(response, { status: 201 });
},
},
}),
},
});