wcz-layout
Version:
113 lines (100 loc) • 4.82 kB
Markdown
---
name: server-functions
description: "Use when: creating or modifying TanStack Start server functions, API routes or middleware."
---
> Mechanics (`createServerFn`, `.validator`, `createMiddleware`, `createHandlers`) are covered by TanStack's own skills — load `/start-client-core#start-core/server-functions`, `#start-core/middleware`, and `#start-core/server-routes` for the API details. 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.
- Every server function MUST include `authorizationMiddleware` — server functions are callable RPC endpoints, so a route-level `requirePermission` in `beforeLoad` only hides the UI and does not protect them.
- Follow the chain order for server functions: `middleware` → `validator` → `handler`.
- Follow the order for middleware arrays: `authorizationMiddleware(<permissionKey>)` → `databaseMiddleware()`.
- Use `validationMiddleware` only in REST API routes. Server functions use `.validator()`.
- Never create a transaction in the handler because `databaseMiddleware` already owns it.
- Reuse schemas from `src/lib/schemas/`.
- Background jobs are API routes using only `authenticationMiddleware()` + `databaseMiddleware()` — cron runs in Kubernetes and authenticates via app token.
## File Placement
```
src/server/actions/ — server functions
src/routes/api/ — REST API routes
src/server/middleware/ — databaseMiddleware
wcz-layout/middleware/ — authorizationMiddleware, validationMiddleware
src/server/db/schemas/ — tables, enums, relations
src/lib/schemas/ — Zod schemas (shared between client and server)
src/lib/auth/permissions.ts — permission keys
```
## Examples
```ts
// src/server/actions/<feature>.ts
export const selectFeatures = createServerFn()
.middleware([authorizationMiddleware("all"), databaseMiddleware()])
.handler(async ({ context }) => {
return await context.db.select().from(featureTable);
});
export const insertFeature = createServerFn({ method: "POST" })
.middleware([authorizationMiddleware("admin"), databaseMiddleware()])
.validator(FeatureSchema)
.handler(async ({ data, context }) => {
await context.db.insert(featureTable).values(data);
});
export const updateFeature = createServerFn({ method: "POST" })
.middleware([authorizationMiddleware("admin"), databaseMiddleware()])
.validator(FeatureSchema)
.handler(async ({ data, context }) => {
await context.db.update(featureTable).set(data).where(eq(featureTable.id, data.id));
});
export const deleteFeature = createServerFn({ method: "POST" })
.middleware([authorizationMiddleware("admin"), databaseMiddleware()])
.validator(FeatureSchema.pick({ id: true }))
.handler(async ({ data, context }) => {
await context.db.delete(featureTable).where(eq(featureTable.id, data.id));
});
// src/routes/api/<feature>s/index.ts
export const Route = createFileRoute("/api/features/")({
server: {
middleware: [databaseMiddleware()],
handlers: ({ createHandlers }) =>
createHandlers({
GET: {
middleware: [authorizationMiddleware("all")],
handler: async ({ context }) => {
const items = await context.db.select().from(featureTable);
return Response.json(items);
},
},
POST: {
middleware: [validationMiddleware(FeatureSchema), authorizationMiddleware("admin")],
handler: async ({ context }) => {
const [response] = await context.db
.insert(featureTable)
.values(context.data)
.returning();
return Response.json(response, { status: 201 });
},
},
}),
},
});
// src/routes/api/<feature>s/$id.ts
export const Route = createFileRoute("/api/features/$id")({
server: {
middleware: [authorizationMiddleware("admin"), databaseMiddleware()],
handlers: ({ createHandlers }) =>
createHandlers({
PUT: {
middleware: [validationMiddleware(FeatureSchema)],
handler: async ({ params, context }) => {
await context.db
.update(featureTable)
.set(context.data)
.where(eq(featureTable.id, params.id));
return new Response(null, { status: 204 });
},
},
DELETE: async ({ params, context }) => {
await context.db.delete(featureTable).where(eq(featureTable.id, params.id));
return new Response(null, { status: 204 });
},
}),
},
});
```