---
name: server-functions
description: "Use when adding or changing server functions, REST/API routes, background-job endpoints, or server-side data access."
metadata:
  type: convention
  library: wcz-layout
---

> 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 using only `databaseMiddleware()` + `authMiddleware()` — cron runs in Kubernetes and authenticates via app token.

## Examples

```ts
// src/server/actions/<table>.ts
import { createFileRoute } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { eq } 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);
  });

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 deleteFeature = createServerFn({ method: "POST" })
  .validator(z.uuid())
  .middleware([databaseMiddleware(), authMiddleware("admin")])
  .handler(async ({ data, context }) => {
    await context.db.delete(featureTable).where(eq(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 });
          },
        },
      }),
  },
});
```
