# z.string().min(1), > TanStack Start createServerFn patterns for type-safe server-side operations - Author: dev-yetibooks - Repository: YetiBooks/yeti-books - Version: 20260127042856 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-06 - Source: https://github.com/YetiBooks/yeti-books - Web: https://mule.run/skillshub/@@YetiBooks/yeti-books~z.string().min(1),:20260127042856 --- --- description: TanStack Start createServerFn patterns for type-safe server-side operations globs: src/server-functions/**/*.ts, src/server-functions/**/*.tsx alwaysApply: false --- # Guidelines for Server Functions ## Purpose and Overview Server functions in TanStack Start provide type-safe, RPC-style communication between client and server. They run only on the server but can be called from client components with full TypeScript inference. In this architecture, server functions act as the API layer that: 1. Handles request validation and authentication 2. Calls Convex functions directly for simple CRUD operations 3. Delegates to services for complex business logic ``` UI (Components/Routes) | Server Functions (src/server-functions/) | +-- (simple CRUD) --> Convex Functions | +-- (complex logic) --> Services --> Convex Functions ``` ## File Organization Place server functions in `src/server-functions/` organized by domain: ``` src/server-functions/ ├── organizations/ │ ├── create-organization.ts │ ├── get-organization.ts │ └── update-organization.ts ├── users/ │ ├── get-user-profile.ts │ └── update-user-profile.ts └── invitations/ ├── create-invitation.ts └── accept-invitation.ts ``` ## Basic Structure ### Simple Server Function (No Input) ```tsx import { createServerFn } from "@tanstack/react-start"; export const fetchPosts = createServerFn().handler(async () => { console.info("Fetching posts..."); const res = await fetch("https://api.example.com/posts"); if (!res.ok) { throw new Error("Failed to fetch posts"); } const posts = await res.json(); return posts as Array; }); ``` ### Server Function with Input Validation ```tsx import { createServerFn } from "@tanstack/react-start"; import { notFound } from "@tanstack/react-router"; export type PostType = { id: number; title: string; body: string; }; export const fetchPost = createServerFn({ method: "POST" }) .inputValidator((d: string) => d) .handler(async ({ data }) => { console.info(`Fetching post with id ${data}...`); const res = await fetch(`https://api.example.com/posts/${data}`); if (!res.ok) { if (res.status === 404) { throw notFound(); } throw new Error("Failed to fetch post"); } const post = await res.json(); return post as PostType; }); ``` ### Server Function with Zod Validation ```tsx import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; const createPostSchema = z.object({ title: z.string().min(1), body: z.string().min(10), authorId: z.number(), }); export const createPost = createServerFn({ method: "POST" }) .inputValidator((input: z.infer) => { return createPostSchema.parse(input); }) .handler(async ({ data }) => { const res = await fetch("https://api.example.com/posts", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) { throw new Error("Failed to create post"); } return res.json(); }); ``` ## Integrating with Convex ### Convex Server Utilities Use the utilities in `src/lib/convex/server.ts` for server-side Convex calls: ```typescript // src/lib/convex/server.ts import { ConvexHttpClient } from "convex/browser"; import type { FunctionReference, FunctionArgs, FunctionReturnType, } from "convex/server"; const convexUrl = process.env.CONVEX_URL; if (!convexUrl) { throw new Error("CONVEX_URL environment variable is required"); } const client = new ConvexHttpClient(convexUrl); export async function fetchQuery>( query: Query, args: FunctionArgs, ): Promise> { return await client.query(query, args); } export async function fetchMutation< Mutation extends FunctionReference<"mutation">, >( mutation: Mutation, args: FunctionArgs, ): Promise> { return await client.mutation(mutation, args); } ``` ### Pattern 1: Simple CRUD (Direct Convex Call) For straightforward operations without business logic, call Convex directly: ```typescript // src/server-functions/organizations/get-organization.ts import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { fetchQuery } from "~/lib/convex/server"; import { api } from "@repo/convex/_generated/api"; const getOrganizationSchema = z.object({ id: z.string(), }); export const getOrganization = createServerFn({ method: "POST" }) .inputValidator((input: z.infer) => { return getOrganizationSchema.parse(input); }) .handler(async ({ data }) => { const organization = await fetchQuery( api.organizations.getOrganizationById, { id: data.id, }, ); return organization; }); ``` ### Pattern 2: With Service Layer (Complex Business Logic) For operations requiring orchestration, multiple steps, or business rules, delegate to a service: ```typescript // src/server-functions/organizations/create-organization.ts import { createServerFn } from "@tanstack/react-start"; import { z } from "zod"; import { getAuthSession } from "~/lib/auth/server"; import { createOrganizationWithMembershipService } from "~/services/organizations/create-organization-with-membership-service"; const createOrganizationSchema = z.object({ name: z.string().min(1), slug: z.string().min(1), }); export const createOrganization = createServerFn({ method: "POST" }) .inputValidator((input: z.infer) => { return createOrganizationSchema.parse(input); }) .handler(async ({ data }) => { // Auth resolved in server function const session = await getAuthSession(); if (!session) { throw new Error("Not authenticated"); } // Delegate to service for business logic return await createOrganizationWithMembershipService({ payload: data, userId: session.userId, }); }); ``` ## When to Use Service Layer | Scenario | Use Service? | Example | | ---------------------------------- | ------------ | -------------------------------------- | | Single Convex query/mutation | No | `getOrganizationById` | | Multiple Convex calls needed | Yes | Create org + add member | | Data transformation required | Yes | Aggregate data from multiple sources | | Business rules to enforce | Yes | Validate invite limits before creating | | Authorization beyond simple checks | Yes | Complex permission logic | **Use services when:** - Multiple Convex operations must happen together - Business logic needs to be applied before/after data access - Data from multiple sources needs to be combined or transformed - Complex validation or authorization is required **Call Convex directly when:** - Simple single query or mutation - No business logic beyond what Convex function handles - Straightforward CRUD operations ## Method Types ### GET Method (Default) Used for data fetching without side effects: ```tsx export const getData = createServerFn().handler(async () => { // Fetch-only operations }); ``` ### POST Method Used for mutations or when sending data: ```tsx export const submitData = createServerFn({ method: "POST" }) .inputValidator((d: FormData) => d) .handler(async ({ data }) => { // Create/update operations }); ``` ## Usage in Routes ### In Route Loaders ```tsx import { createFileRoute } from "@tanstack/react-router"; import { getOrganization } from "~/server-functions/organizations/get-organization"; export const Route = createFileRoute("/organizations/$orgId")({ loader: async ({ params }) => { const organization = await getOrganization({ data: { id: params.orgId } }); return { organization }; }, component: OrganizationComponent, }); function OrganizationComponent() { const { organization } = Route.useLoaderData(); return
{organization.name}
; } ``` ### In Components (Client-Side) ```tsx import { getOrganizations } from "~/server-functions/organizations/get-organizations"; import { useState, useEffect } from "react"; function OrganizationsList() { const [organizations, setOrganizations] = useState([]); useEffect(() => { getOrganizations().then(setOrganizations); }, []); return (
    {organizations.map((org) => (
  • {org.name}
  • ))}
); } ``` ## Error Handling ### Using notFound() ```tsx import { notFound } from "@tanstack/react-router"; export const fetchItem = createServerFn({ method: "POST" }) .inputValidator((id: string) => id) .handler(async ({ data }) => { const res = await fetch(`/api/items/${data}`); if (res.status === 404) { throw notFound(); } if (!res.ok) { throw new Error("Failed to fetch item"); } return res.json(); }); ``` ### Custom Error Types ```tsx class ApiError extends Error { constructor( public statusCode: number, message: string, ) { super(message); } } export const fetchData = createServerFn().handler(async () => { const res = await fetch("/api/data"); if (!res.ok) { throw new ApiError(res.status, "API request failed"); } return res.json(); }); ``` ## Best Practices 1. **Type Safety**: Always define return types for server functions 2. **Input Validation**: Use `.inputValidator()` for all functions that accept input 3. **Error Handling**: Handle errors appropriately, use `notFound()` for 404 cases 4. **Logging**: Add server-side logging with `console.info()` for debugging 5. **Single Responsibility**: Each server function should do one thing well 6. **Naming**: Use verb prefixes (`fetch`, `create`, `update`, `delete`) 7. **Auth in Server Functions**: Resolve authentication context in server functions, pass user info to services 8. **Direct vs Service**: Use direct Convex calls for simple CRUD, services for complex logic