# sf-create-server-fn: Use createServerFn for Server-Side Logic ## Priority: CRITICAL ## Explanation `createServerFn()` creates type-safe server functions that can be called from anywhere - loaders, components, or other server functions. The code inside the handler runs only on the server, with automatic RPC for client calls. ## Bad Example ```tsx // Using fetch directly - no type safety, manual serialization async function createPost(data: CreatePostInput) { const response = await fetch('/api/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!response.ok) throw new Error('Failed to create post') return response.json() } // Or using API routes - more boilerplate // api/posts.ts export async function POST(request: Request) { const data = await request.json() // No type safety from client const post = await db.posts.create({ data }) return new Response(JSON.stringify(post)) } ``` ## Good Example ```tsx // lib/posts.functions.ts import { createServerFn } from '@tanstack/react-start' import { z } from 'zod' import { db } from './db.server' const createPostSchema = z.object({ title: z.string().min(1).max(200), content: z.string().min(1), published: z.boolean().default(false), }) export const createPost = createServerFn({ method: 'POST' }) .validator(createPostSchema) .handler(async ({ data }) => { // This code only runs on the server const post = await db.posts.create({ data: { title: data.title, content: data.content, published: data.published, }, }) return post }) // Usage in component function CreatePostForm() { const createPostMutation = useServerFn(createPost) const handleSubmit = async (formData: FormData) => { try { const post = await createPostMutation({ data: { title: formData.get('title') as string, content: formData.get('content') as string, published: false, }, }) // post is fully typed console.log('Created post:', post.id) } catch (error) { console.error('Failed to create post:', error) } } } ``` ## Good Example: GET Function for Data Fetching ```tsx // lib/posts.functions.ts export const getPosts = createServerFn() // GET is default .handler(async () => { const posts = await db.posts.findMany({ orderBy: { createdAt: 'desc' }, take: 20, }) return posts }) export const getPost = createServerFn() .validator(z.object({ id: z.string() })) .handler(async ({ data }) => { const post = await db.posts.findUnique({ where: { id: data.id }, }) if (!post) { throw notFound() } return post }) // Usage in route loader export const Route = createFileRoute('/posts/$postId')({ loader: async ({ params }) => { return await getPost({ data: { id: params.postId } }) }, }) ``` ## Good Example: With Context and Dependencies ```tsx // Compose server functions export const getPostWithComments = createServerFn() .validator(z.object({ postId: z.string() })) .handler(async ({ data }) => { const [post, comments] = await Promise.all([ getPost({ data: { id: data.postId } }), getComments({ data: { postId: data.postId } }), ]) return { post, comments } }) ``` ## Key Benefits - **Type safety**: Input/output types flow through client and server - **Automatic serialization**: No manual JSON parsing - **Code splitting**: Server code never reaches client bundle - **Composable**: Call from loaders, components, or other server functions - **Validation**: Built-in input validation with schema libraries ## Context - Default method is GET (idempotent, cacheable) - Use POST for mutations that change data - Server functions are RPC calls under the hood - Validation errors are properly typed and serialized - Import is safe on client - build process replaces with RPC stub