2026.10.17 · 4 min · react · typescript · javascript · backend
TanStack Start: a server function is just a function.
createServerFn code runs only on the server, but you call it from the browser like a typed local async function. The build swaps the body for an RPC stub, so server code never ships and the call becomes a fetch. Which also makes it a public endpoint. How it works, the code, and what people get wrong. As of 2026.10.
Calling your own server usually means four pieces of glue: an API route, a fetch call, JSON parsing, and the same types written twice, once on each side. TanStack Start's server functions cut that down to one: you write a function that runs on the server and await it from the browser as if it were local. The build tool writes the network layer, and the types come along for free.
Watch the 60-second version on Instagram.
TanStack Start 1.0 was announced as stable and production-ready on 2026.10.13, framed as a commitment to no breaking API changes (the post gives no patch version). Start grew out of TanStack Router: react-router-server was renamed to start in May 2024. It builds on Vite and Nitro, with Rsbuild also supported. Server functions are its headline full-stack feature.
The whole API
import { createServerFn } from '@tanstack/react-start'
export const getUser = createServerFn({ method: 'GET' })
.validator((data: { id: string }) => data)
.handler(async ({ data }) => {
return db.user.find(data.id) // server only
})
// in a component / loader
const user = await getUser({ data: { id: '42' } })Three parts:
- A method. The default is
GET. UsecreateServerFn({ method: 'POST' })for mutations. .validator(fnOrSchema), a plain function or a Zod schema. Older (2025) tutorials call it.inputValidator(...); the current docs and the source expose only.validator(...)..handler(async ({ data }) => …), which gets the validateddata.
Callers always pass one object, { data }. That works from a loader, an event handler, or a queryFn. Inside components, useServerFn(fn) wraps it so redirects and errors are handled for you.
What the build does
The build replaces server function implementations with RPC stubs in the client bundles. The handler body stays in the server bundle, next to your database; the browser gets a function with the same name whose body only sends a request. So server code never reaches the browser, and a client call becomes a fetch.
One call, across the wire
From the call site it looks like a local async function. Underneath, it's a network round trip: the stub sends data, the server runs the validator and then the handler, and the result comes back. The return type you see in the browser is inferred from the server handler, so there's no shared DTO file to keep in sync.
Two rules come with that round trip. Inputs and outputs must be serializable (strict: true by default), so a class instance or a function in the return value fails. And by default, requests must be same-origin: Start checks Sec-Fetch-Site, Origin and Referer. For a public or cross-origin API, use server routes instead.
What people get wrong
- "It's local, so it's safe." Every call is a network request to a public endpoint. Anyone can call it with any
data, so validate the input and check auth inside the function. AbeforeLoadroute guard is not a data boundary. - Calling it positionally.
getUser('42')is wrong; it's alwaysgetUser({ data: { id: '42' } }). - Importing it dynamically. A dynamic
import()of a server function module can cause bundler issues. Use static imports. - Returning things that don't serialize. Class instances and functions fail the strict checks.
- Caching per-user responses as
public. Apubliccache header on a response that belongs to one user can leak it to others.
On a client project, I'd put auth and validation inside every server function from day one and treat each one as an API route I happen not to have written, because that's exactly what it is.
The short version
createServerFncode runs only on the server; you call it withawait fn({ data }).GETby default,POSTfor mutations; chain.validator()then.handler().- The build swaps the body for an RPC stub in client bundles: server code never ships, the call becomes a
fetch. - It's a public endpoint: validate input and check auth inside it. Route guards don't protect it.
- Same-origin and serializable by default; for a public or cross-origin API, use server routes.
Sources: TanStack Start 1.0 (blog) · TanStack Start docs, Server Functions · TanStack source, createServerFn.ts