Server functions allow you to specify logic that can be invoked anywhere (even the client), but run only on the server. In fact, they are not so different from an API route, but with a few key differences:
However, they are similar to regular API routes in that:
How are server functions different from "React Server Functions"?
- TanStack Server Functions are not tied to a specific front-end framework, and can be used with any front-end framework or none at all.
- TanStack Server Functions are backed by standard HTTP requests and can be called as often as you like without suffering from serial-execution bottlenecks.
Server functions can be defined anywhere in your application, but must be defined at the top level of a file. They can be called from anywhere in your application, including loaders, hooks, etc. Traditionally, this pattern is known as a Remote Procedure Call (RPC), but due to the isomorphic nature of these functions, we refer to them as server functions.
fetch
request to the server instructing it to execute the server function in the server bundle and then send the response back to the client.Server functions can use middleware to share logic, context, common operations, prerequisites, and much more. To learn more about server function middleware, be sure to read about them in the Middleware guide.
We'd like to thank the tRPC team for both the inspiration of TanStack Start's server function design and guidance while implementing it. We love (and recommend) using tRPC for API routes so much that we insisted on server functions getting the same 1st class treatment and developer experience. Thank you!
Server functions are defined using the createServerFn
function, exported from the @tanstack/start
package. This function must be called with an HTTP verb, and an async function that will be executed on the server. Here's an example:
// getServerTime.tsimport { createServerFn } from '@tanstack/start'
export const getServerTime = createServerFn().handler(async () => { // Wait for 1 second await new Promise((resolve) => setTimeout(resolve, 1000)) // Return the current time return new Date().toISOString()})
Server functions accept a single parameter, which can be a variety of types:
string
number
boolean
null
Array
Object
Here's an example of a server function that accepts a simple string parameter:
import { createServerFn } from '@tanstack/start'
export const greet = createServerFn({ method: 'GET',}) .validator((data: string) => data) .handler(async (ctx) => { return `Hello, ${ctx.data}!` })
greet({ data: 'John',})
Server functions can be configured to validate their input data at runtime. This is useful for ensuring that the input is of the correct type before executing the server function, can provide more friendly error messages, and even power type-safety if configured correctly.
To enable validation, call the validator
method on the server function. You can pass a variety of things:
Here's a simple example of a server function that validates the input parameter:
import { createServerFn } from '@tanstack/start'
type Person = { name: string}
export const greet = createServerFn({ method: 'GET' }) .validator((person: unknown): Person => { if (typeof person !== 'object' || person === null) { throw new Error('Person must be an object') }
if ('name' in person && typeof person.name !== 'string') { throw new Error('Person.name must be a string') }
return person as Person }) .handler(async ({ data }) => { return `Hello, ${data.name}!` })
You can also use a validation library like Zod to validate the input parameter:
import { createServerFn } from '@tanstack/start'
import { z } from 'zod'
const Person = z.object({ name: z.string(),})
export const greet = createServerFn({ method: 'GET' }) .validator((person: unknown) => { return Person.parse(person) }) .handler(async (ctx) => { return `Hello, ${ctx.data.name}!` })
greet({ data: { name: 'John', },})
Since server-functions cross the network boundary, it's important to ensure that the data being passed to them is not only just the right type, but also validated at runtime. This is especially important wheen dealing with user input, as it can be unpredictable. To help ensure developers validate their I/O data, types are reliant on using the input
validation, which is then used to infer the input and output types of the server function.
import { createServerFn } from '@tanstack/start'
type Person = { name: string}
export const greet = createServerFn({ method: 'GET' }) .validator((person: unknown): Person => { if (typeof person !== 'object' || person === null) { throw new Error('Person must be an object') }
if ('name' in person && typeof person.name !== 'string') { throw new Error('Person.name must be a string') }
return person as Person }) .handler( async ({ data, // Person }) => { return `Hello, ${data.name}!` }, )
function test() { greet({ data: { name: 'John' } }) // OK greet({ data: { name: 123 } }) // Error: Argument of type '{ name: number; }' is not assignable to parameter of type 'Person'.}
Server functions infer their input and output types based on the validator
handler and the return type of the handler
function. In fact, the validator
you pass can even have it's own separate input/output types, which can be useful if your validator performs some kind of transformation on the input data.
To illustrate this, let's take a look at an example using the zod
validation library:
import { createServerFn } from '@tanstack/start'import { z } from 'zod'
const transactionSchema = z.object({ amount: z.string().transform((val) => parseInt(val, 10)),})
const createTransaction = createServerFn() .validator(transactionSchema) .handler(({ data }) => { return data.amount // Returns a number })
createTransaction({ data: { amount: '123', // Accepts a string },})
While we highly recommend using a validation library to validate your network I/O data, you may for whatever reason not want to validate your data, but still have the type-safety. To do this, you can still provide type information to the server function using an identity function as the validator
handler that casts the input and or output to the correct type:
import { createServerFn } from '@tanstack/start'
type Person = { name: string}
export const greet = createServerFn({ method: 'GET' }) .validator((d: Person) => d) .handler(async (ctx) => { return `Hello, ${ctx.data.name}!` })
greet({ data: { name: 'John', },})
Server functions can accept JSON-serializable objects as parameters. This is useful for passing complex data structures to the server:
import { createServerFn } from '@tanstack/start'
type Person = { name: string age: number}
export const greet = createServerFn({ method: 'GET' }) .validator((data) => data) .handler(async ({ data }) => { return `Hello, ${data.name}! You are ${data.age} years old.` })
greet({ data: { name: 'John', age: 34, },})
Server functions can accept FormData
objects as parameters
import { createServerFn } from '@tanstack/start'
export const greetUser = createServerFn() .validator((data) => { if (!(data instanceof FormData)) { throw new Error('Invalid form data') } const name = data.get('name') const age = data.get('age')
if (!name || !age) { throw new Error('Name and age are required') }
return { name: name.toString(), age: parseInt(age.toString(), 10), } }) .handler(async ({ data: { name, age } }) => { return `Hello, ${name}! You are ${age} years old.` })
// Usagefunction Test() { return ( <form onSubmit={async (event) => { event.preventDefault() const formData = new FormData(event.target) const response = await greetUser({ data: formData }) console.log(response) }} > <input name="name" /> <input name="age" /> <button type="submit">Submit</button> </form> )}
In addition to the single parameter that server functions accept, you can also access server request context from within any server function using many utilities from vinxi/http
. Under the hood, Vinxi uses unjs
's h3
package to perform cross-platform HTTP requests.
There are many context functions available to you for things like:
For a full list of available context functions, see all of the available h3 Methods or inspect the Vinxi Exports Source Code.
For starters, here are a few examples:
Let's use Vinxi's getWebRequest
function to access the request itself from within a server function:
import { createServerFn } from '@tanstack/start'import { getWebRequest } from 'vinxi/http'
export const getServerTime = createServerFn({ method: 'GET' }).handler( async () => { const request = getWebRequest()
console.log(request.method) // GET
console.log(request.headers.get('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3 },)
Use Vinxi's getHeaders
function to access all headers from within a server function:
import { createServerFn } from '@tanstack/start'import { getHeaders } from 'vinxi/http'
export const getServerTime = createServerFn({ method: 'GET' }).handler( async () => { console.log(getHeaders()) // { // "accept": "*/*", // "accept-encoding": "gzip, deflate, br", // "accept-language": "en-US,en;q=0.9", // "connection": "keep-alive", // "host": "localhost:3000", // ... // } },)
You can also access individual headers using the getHeader
function:
import { createServerFn } from '@tanstack/start'import { getHeader } from 'vinxi/http'
export const getServerTime = createServerFn({ method: 'GET' }).handler( async () => { console.log(getHeader('User-Agent')) // Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3 },)
Server functions can return a few different types of values:
redirect
errors (can also be thrown)notFound
errors (can also be thrown)To return any primitive or JSON-serializable object, simply return the value from the server function:
import { createServerFn } from '@tanstack/start'
export const getServerTime = createServerFn({ method: 'GET' }).handler( async () => { return new Date().toISOString() },)
export const getServerData = createServerFn({ method: 'GET' }).handler( async () => { return { message: 'Hello, World!', } },)
By default, server functions assume that any non-Response object returned is either a primitive or JSON-serializable object.
To respond with custom headers, you can use Vinxi's setHeader
function:
import { createServerFn } from '@tanstack/start'import { setHeader } from 'vinxi/http'
export const getServerTime = createServerFn({ method: 'GET' }).handler( async () => { setHeader('X-Custom-Header', 'value') return new Date().toISOString() },)
To respond with a custom status code, you can use Vinxi's setResponseStatus
function:
import { createServerFn } from '@tanstack/start'import { setResponseStatus } from 'vinxi/http'
export const getServerTime = createServerFn({ method: 'GET' }).handler( async () => { setResponseStatus(201) return new Date().toISOString() },)
To return a raw Response object, simply return a Response object from the server function:
import { createServerFn } from '@tanstack/start'
export const getServerTime = createServerFn({ method: 'GET' }).handler( async () => { // Read a file from s3 return fetch('https://example.com/time.txt') },)
Aside from special redirect
and notFound
errors, server functions can throw any custom error. These errors will be serialized and sent to the client as a JSON response along with a 500 status code.
import { createServerFn } from '@tanstack/start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => { throw new Error('Something went wrong!')})
// Usagefunction Test() { try { await doStuff() } catch (error) { console.error(error) // { // message: "Something went wrong!", // stack: "Error: Something went wrong!\n at doStuff (file:///path/to/file.ts:3:3)" // } }}
Server functions can be called normally from route loader
s, beforeLoad
s, or any other router-controlled APIs. These APIs are equipped to handle errors, redirects, and notFounds thrown by server functions automatically.
import { getServerTime } from './getServerTime'
export const Route = createFileRoute('/time')({ loader: async () => { const time = await getServerTime()
return { time, } },})
Server functions can throw redirect
s or notFound
s and while not required, it is recommended to catch these errors and handle them appropriately. To make this easier, the @tanstack/start
package exports a useServerFn
hook that can be used to bind server functions to components and hooks:
import { useServerFn } from '@tanstack/start'import { useQuery } from '@tanstack/react-query'import { getServerTime } from './getServerTime'
export function Time() { const getTime = useServerFn(getServerTime)
const timeQuery = useQuery({ queryKey: 'time', queryFn: () => getTime(), })}
Server functions are just async functions, so they can ultimately be called from anywhere in your application. However, be aware that any redirects or notFounds thrown by server functions will not be handled automatically unless called from a route lifecycle or a component that uses the useServerFn
hook.
Server functions can throw a redirect
error to redirect the user to a different URL. This is useful for handling authentication, authorization, or other scenarios where you need to redirect the user to a different page.
useServerFn
hook. If you call a server function from anywhere else, redirects will not be handled automatically.To throw a redirect, you can use the redirect
function exported from the @tanstack/react-router
package:
import { redirect } from '@tanstack/react-router'import { createServerFn } from '@tanstack/start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => { // Redirect the user to the home page throw redirect({ to: '/', })})
Redirects can utilize all of the same options as router.navigate
, useNavigate()
and <Link>
components. So feel free to also pass:
Redirects can also set the status code of the response by passing a status
option:
import { redirect } from '@tanstack/react-router'import { createServerFn } from '@tanstack/start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => { // Redirect the user to the home page with a 301 status code throw redirect({ to: '/', status: 301, })})
⚠️ Do not use Vinxi's
sendRedirect
function to send soft redirects from within server functions. This will send the redirect using theLocation
header and will force a full page hard navigation on the client.
You can also set custom headers on a redirect by passing a headers
option:
import { redirect } from '@tanstack/react-router'import { createServerFn } from '@tanstack/start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => { // Redirect the user to the home page with a custom header throw redirect({ to: '/', headers: { 'X-Custom-Header': 'value', }, })})
While calling a server function from a loader
or beforeLoad
route lifecycle, a special notFound
error can be thrown to indicate to the router that the requested resource was not found. This is more useful than a simple 404 status code, as it allows you to render a custom 404 page, or handle the error in a custom way. If notFound is thrown from a server function used outside of a route lifecycle, it will not be handled automatically.
To throw a notFound, you can use the notFound
function exported from the @tanstack/start
package:
import { createServerFn, notFound } from '@tanstack/start'
const getStuff = createServerFn({ method: 'GET' }).handler(async () => { // Randomly return a not found error if (Math.random() < 0.5) { throw notFound() }
// Or return some stuff return { stuff: 'stuff', }})
export const Route = createFileRoute('/stuff')({ loader: async () => { const stuff = await getStuff()
return { stuff, } },})
Not found errors are a core feature of TanStack Router,
If a server function throws a (non-redirect/non-notFound) error, it will be serialized and sent to the client as a JSON response along with a 500 status code. This is useful for debugging, but you may want to handle these errors in a more user-friendly way. You can do this by catching the error and handling it in your route lifecycle, component, or hook as you normally would.
import { createServerFn } from '@tanstack/start'
export const doStuff = createServerFn({ method: 'GET' }).handler(async () => { undefined.foo()})
export const Route = createFileRoute('/stuff')({ loader: async () => { try { await doStuff() } catch (error) { // Handle the error: // error === { // message: "Cannot read property 'foo' of undefined", // stack: "TypeError: Cannot read property 'foo' of undefined\n at doStuff (file:///path/to/file.ts:3:3)" } },})
Without JavaScript enabled, there's only one way to execute server functions: by submitting a form.
This is done by adding a form
element to the page
with the HTML attribute action
.
Notice that we mentioned the HTML attribute
action
. This attribute only accepts a string in HTML, just like all other attributes.While React 19 added support for passing a function to
action
, it's a React-specific feature and not part of the HTML standard.
The action
attribute tells the browser where to send the form data when the form is submitted. In this case, we want
to send the form data to the server function.
To do this, we can utilize the url
property of the server function:
const yourFn = createServerFn() .validator((formData) => { const name = formData.get('name')
if (!name) { throw new Error('Name is required') }
return name }) .handler(async ({ data: name }) => { console.log(name) // 'John' })
console.info(yourFn.url)
And pass this to the action
attribute of the form:
function Component() { return ( <form action={yourFn.url} method="POST"> <input name="name" defaultValue="John" /> <button type="submit">Click me!</button> </form> )}
When the form is submitted, the server function will be executed.
To pass arguments to a server function when submitting a form, you can use the input
element with the name
attribute
to attach the argument to the FormData
passed to your
server function:
const yourFn = createServerFn() .validator((formData) => { if (!(formData instanceof FormData)) { throw new Error('Invalid form data') }
const age = formData.get('age')
if (!age) { throw new Error('age is required') }
return age.toString() }) .handler(async ({ data: formData }) => { // `age` will be '123' const age = formData.get('age') // ... })
function Component() { return ( // We need to tell the server that our data type is `multipart/form-data` by setting the `encType` attribute on the form. <form action={yourFn.url} method="POST" encType="multipart/form-data"> <input name="age" defaultValue="34" /> <button type="submit">Click me!</button> </form> )}
When the form is submitted, the server function will be executed with the form's data as an argument.
Regardless of whether JavaScript is enabled, the server function will return a response to the HTTP request made from the client.
When JavaScript is enabled, this response can be accessed as the return value of the server function in the client's JavaScript code.
const yourFn = createServerFn().handler(async () => { return 'Hello, world!'})
// `.then` is not available when JavaScript is disabledyourFn().then(console.log)
However, when JavaScript is disabled, there is no way to access the return value of the server function in the client's JavaScript code.
Instead, the server function can provide a response to the client, telling the browser to navigate in a certain way.
When combined with a loader
from TanStack Router, we're able to provide an experience similar to a single-page application, even when
JavaScript is disabled;
all by telling the browser to reload the current page with new data piped through the loader
:
import * as fs from 'fs'import { createFileRoute } from '@tanstack/react-router'import { createServerFn } from '@tanstack/start'
const filePath = 'count.txt'
async function readCount() { return parseInt( await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'), )}
const getCount = createServerFn({ method: 'GET',}).handler(() => { return readCount()})
const updateCount = createServerFn({ method: 'POST' }) .validator((formData) => { if (!(formData instanceof FormData)) { throw new Error('Invalid form data') }
const addBy = formData.get('addBy')
if (!addBy) { throw new Error('addBy is required') }
return parseInt(addBy.toString()) }) .handler(async ({ data: addByAmount }) => { const count = await readCount() await fs.promises.writeFile(filePath, `${count + addByAmount}`) // Reload the page to trigger the loader again return new Response('ok', { status: 301, headers: { Location: '/' } }) })
export const Route = createFileRoute('/')({ component: Home, loader: async () => await getCount(),})
function Home() { const state = Route.useLoaderData()
return ( <div> <form action={updateCount.url} method="POST" encType="multipart/form-data" > <input type="number" name="addBy" defaultValue="1" /> <button type="submit">Add</button> </form> <pre>{state}</pre> </div> )}
Under the hood, server functions are extracted out of the client bundle and into a separate server bundle. On the server, they are executed as-is, and the result is sent back to the client. On the client, server functions proxy the request to the server, which executes the function and sends the result back to the client, all via fetch
.
The process looks like this:
createServerFn
is found in a file, the inner function is checked for a use server
directiveuse server
directive is missing, it is added to the top of the functionYour weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.