claude-start-cf

Server Functions

Server functions run on the server but can be called from anywhere — loaders, components, event handlers. On the client they become RPC calls automatically.

Basic GET server function

createServerFn({ method: 'GET' }).handler(async () => { ... })

The simplest pattern. Define a handler, call it from anywhere. During SSR it runs directly. From the client it becomes a fetch request.

Server time2026-02-20T10:01:43.136Z
Worker PID9401

First load came from the loader (SSR). Clicking calls it client-side via RPC.

POST with input validation

.inputValidator((data) => { ... }).handler(async ({ data }) => { ... })

Input validators run before the handler. They validate and transform the data that crosses the network boundary. Use plain functions or Zod schemas.

+
const addNumbers = createServerFn({ method: 'POST' })
  .inputValidator((data: { a: number; b: number }) => {
    if (typeof data.a !== 'number') throw new Error('...')
    return data
  })
  .handler(async ({ data }) => {
    return { result: data.a + data.b }
  })

Request context

getRequest(), getRequestHeader(), setResponseHeaders()

Inside a handler you can access the incoming request, read headers, and set response headers. Useful for auth checks, caching, and logging.

MethodGET
Path/server-fns
Hostclaude-start-cf.jmorrison.workers.dev
User-AgentMozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko;...

On SSR the request comes from the initial page load. On re-fetch it comes from the client RPC call — notice the path changes.

Error handling

throw new Error() / throw redirect() / throw notFound()

Errors thrown in server functions are serialized to the client. You can also throw redirect() for navigation or notFound() for 404s.

// Errors
throw new Error('Something went wrong')

// Redirects (auth, navigation)
throw redirect({ to: '/login' })

// 404s (missing resources)
throw notFound()

FormData handling

.inputValidator((data) => { /* parse FormData */ })

Server functions can accept FormData directly. The input validator parses it into a typed object before the handler runs. Works with progressive enhancement — the form can submit without JS too.

useServerFn hook

const fn = useServerFn(myServerFn)

The useServerFn hook wraps a server function for use in components. It handles pending states and integrates with React's transition model.

This calls getGreeting from ~/utils/greetings.functions.ts, which imports server-only logic from greetings.server.ts.

File organization

*.functions.ts / *.server.ts / *.ts

Recommended pattern for organizing server-side code. Separates RPC boundaries from internal server logic.

src/utils/
├── greetings.functions.ts   # createServerFn wrappers (import anywhere)
├── greetings.server.ts      # Server-only helpers (DB, internal logic)
└── schemas.ts               # Shared types & validation (client-safe)
greetings.server.tsServer-only helper
// Only import inside server function handlers
export function buildGreeting(name: string) {
  const greeting = greetings[Math.floor(Math.random() * greetings.length)]
  return `${greeting}, ${name}!`
}
greetings.functions.tsServer function wrapper
import { createServerFn } from '@tanstack/react-start'
import { buildGreeting } from './greetings.server'

export const getGreeting = createServerFn({ method: 'GET' })
  .inputValidator((data: { name: string }) => data)
  .handler(async ({ data }) => {
    return { message: buildGreeting(data.name) }
  })
component.tsxClient component (safe import)
// ✅ Static import — build replaces with RPC stub
import { getGreeting } from '~/utils/greetings.functions'

const result = await getGreeting({ data: { name: 'World' } })

Key rule: *.functions.ts files can be imported anywhere. *.server.ts files should only be imported inside server function handlers — the build process tree-shakes them from the client bundle.