Server Functions

What are Server Functions?

Server functions let you define server-only logic that can be called from anywhere in your application - loaders, components, hooks, or other server functions. They run on the server but can be invoked from client code seamlessly.

tsx
import { createServerFn } from '@tanstack/solid-start'

export const getServerTime = createServerFn().handler(async () => {
  // This runs only on the server
  return new Date().toISOString()
})

// Call from anywhere - components, loaders, hooks, etc.
const time = await getServerTime()
import { createServerFn } from '@tanstack/solid-start'

export const getServerTime = createServerFn().handler(async () => {
  // This runs only on the server
  return new Date().toISOString()
})

// Call from anywhere - components, loaders, hooks, etc.
const time = await getServerTime()

Server functions provide server capabilities (database access, environment variables, file system) while maintaining type safety across the network boundary.

Basic Usage

Server functions are created with createServerFn() and can specify HTTP method:

tsx
import { createServerFn } from '@tanstack/solid-start'

// GET request (default)
export const getData = createServerFn().handler(async () => {
  return { message: 'Hello from server!' }
})

// POST request
export const saveData = createServerFn({ method: 'POST' }).handler(async () => {
  // Server-only logic
  return { success: true }
})
import { createServerFn } from '@tanstack/solid-start'

// GET request (default)
export const getData = createServerFn().handler(async () => {
  return { message: 'Hello from server!' }
})

// POST request
export const saveData = createServerFn({ method: 'POST' }).handler(async () => {
  // Server-only logic
  return { success: true }
})

Where to Call Server Functions

Call server functions from:

  • Route loaders - Perfect for data fetching
  • Components - Use with useServerFn() hook
  • Other server functions - Compose server logic
  • Event handlers - Handle form submissions, clicks, etc.
tsx
// In a route loader
export const Route = createFileRoute('/posts')({
  loader: () => getPosts(),
})

// In a component
function PostList() {
  const getPosts = useServerFn(getServerPosts)

  const { data } = useQuery({
    queryKey: ['posts'],
    queryFn: () => getPosts(),
  })
}
// In a route loader
export const Route = createFileRoute('/posts')({
  loader: () => getPosts(),
})

// In a component
function PostList() {
  const getPosts = useServerFn(getServerPosts)

  const { data } = useQuery({
    queryKey: ['posts'],
    queryFn: () => getPosts(),
  })
}

Parameters & Validation

Server functions accept a single data parameter. Since they cross the network boundary, validation ensures type safety and runtime correctness.

Basic Parameters

tsx
import { createServerFn } from '@tanstack/solid-start'

export const greetUser = createServerFn({ method: 'GET' })
  .inputValidator((data: { name: string }) => data)
  .handler(async ({ data }) => {
    return `Hello, ${data.name}!`
  })

await greetUser({ data: { name: 'John' } })
import { createServerFn } from '@tanstack/solid-start'

export const greetUser = createServerFn({ method: 'GET' })
  .inputValidator((data: { name: string }) => data)
  .handler(async ({ data }) => {
    return `Hello, ${data.name}!`
  })

await greetUser({ data: { name: 'John' } })

Validation with Zod

For robust validation, use schema libraries like Zod:

tsx
import { createServerFn } from '@tanstack/solid-start'
import { z } from 'zod'

const UserSchema = z.object({
  name: z.string().min(1),
  age: z.number().min(0),
})

export const createUser = createServerFn({ method: 'POST' })
  .inputValidator(UserSchema)
  .handler(async ({ data }) => {
    // data is fully typed and validated
    return `Created user: ${data.name}, age ${data.age}`
  })
import { createServerFn } from '@tanstack/solid-start'
import { z } from 'zod'

const UserSchema = z.object({
  name: z.string().min(1),
  age: z.number().min(0),
})

export const createUser = createServerFn({ method: 'POST' })
  .inputValidator(UserSchema)
  .handler(async ({ data }) => {
    // data is fully typed and validated
    return `Created user: ${data.name}, age ${data.age}`
  })

Form Data

Handle form submissions with FormData:

tsx
export const submitForm = createServerFn({ method: 'POST' })
  .inputValidator((data) => {
    if (!(data instanceof FormData)) {
      throw new Error('Expected FormData')
    }

    return {
      name: data.get('name')?.toString() || '',
      email: data.get('email')?.toString() || '',
    }
  })
  .handler(async ({ data }) => {
    // Process form data
    return { success: true }
  })
export const submitForm = createServerFn({ method: 'POST' })
  .inputValidator((data) => {
    if (!(data instanceof FormData)) {
      throw new Error('Expected FormData')
    }

    return {
      name: data.get('name')?.toString() || '',
      email: data.get('email')?.toString() || '',
    }
  })
  .handler(async ({ data }) => {
    // Process form data
    return { success: true }
  })

Error Handling & Redirects

Server functions can throw errors, redirects, and not-found responses that are handled automatically when called from route lifecycles or components using useServerFn().

Basic Errors

tsx
import { createServerFn } from '@tanstack/solid-start'

export const riskyFunction = createServerFn().handler(async () => {
  if (Math.random() > 0.5) {
    throw new Error('Something went wrong!')
  }
  return { success: true }
})

// Errors are serialized to the client
try {
  await riskyFunction()
} catch (error) {
  console.log(error.message) // "Something went wrong!"
}
import { createServerFn } from '@tanstack/solid-start'

export const riskyFunction = createServerFn().handler(async () => {
  if (Math.random() > 0.5) {
    throw new Error('Something went wrong!')
  }
  return { success: true }
})

// Errors are serialized to the client
try {
  await riskyFunction()
} catch (error) {
  console.log(error.message) // "Something went wrong!"
}

Redirects

Use redirects for authentication, navigation, etc:

tsx
import { createServerFn } from '@tanstack/solid-start'
import { redirect } from '@tanstack/solid-router'

export const requireAuth = createServerFn().handler(async () => {
  const user = await getCurrentUser()

  if (!user) {
    throw redirect({ to: '/login' })
  }

  return user
})
import { createServerFn } from '@tanstack/solid-start'
import { redirect } from '@tanstack/solid-router'

export const requireAuth = createServerFn().handler(async () => {
  const user = await getCurrentUser()

  if (!user) {
    throw redirect({ to: '/login' })
  }

  return user
})

Not Found

Throw not-found errors for missing resources:

tsx
import { createServerFn } from '@tanstack/solid-start'
import { notFound } from '@tanstack/solid-router'

export const getPost = createServerFn()
  .inputValidator((data: { id: string }) => data)
  .handler(async ({ data }) => {
    const post = await db.findPost(data.id)

    if (!post) {
      throw notFound()
    }

    return post
  })
import { createServerFn } from '@tanstack/solid-start'
import { notFound } from '@tanstack/solid-router'

export const getPost = createServerFn()
  .inputValidator((data: { id: string }) => data)
  .handler(async ({ data }) => {
    const post = await db.findPost(data.id)

    if (!post) {
      throw notFound()
    }

    return post
  })

Advanced Topics

For more advanced server function patterns and features, see these dedicated guides:

Server Context & Request Handling

Access request headers, cookies, and response customization:

  • getRequest() - Access the full request object
  • getRequestHeader() - Read specific headers
  • setResponseHeader() - Set custom response headers
  • setResponseStatus() - Custom status codes

Streaming & Raw Responses

Return Response objects for streaming, binary data, or custom content types.

Progressive Enhancement

Use server functions without JavaScript by leveraging the .url property with HTML forms.

Middleware

Compose server functions with middleware for authentication, logging, and shared logic. See the Middleware guide.

Static Server Functions

Cache server function results at build time for static generation. See Static Server Functions.

Request Cancellation

Handle request cancellation with AbortSignal for long-running operations.

Function ID generation

Server functions are addressed by a generated, stable function ID under the hood. These IDs are embedded into the client/SSR builds and used by the server to locate and import the correct module at runtime.

Defaults:

  • In development, IDs are URL-safe strings derived from ${filename}--${functionName} to aid debugging.
  • In production, IDs are SHA256 hashes of the same seed to keep bundles compact and avoid leaking file paths.
  • If two server functions end up with the same ID (including when using a custom generator), the system de-duplicates by appending an incrementing suffix like _1, _2, etc.
  • IDs are stable for a given file/function tuple for the lifetime of the process (hot updates keep the same mapping).

Customization:

You can customize function ID generation by providing a generateFunctionId function when configuring the TanStack Start Vite plugin.

Please note that this customization is experimental und subject to change.

Example:

ts
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackStart } from '@tanstack/solid-start/plugin/vite'

export default defineConfig({
  plugins: [
    tanstackStart({
      serverFns: {
        generateFunctionId: ({ filename, functionName }) => {
          // Return a custom ID string. If you return undefined, the default is used.
          // For example, always hash (even in dev):
          // return createHash('sha256').update(`${filename}--${functionName}`).digest('hex')
          return undefined
        },
      },
    }),
    react(),
  ],
})
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackStart } from '@tanstack/solid-start/plugin/vite'

export default defineConfig({
  plugins: [
    tanstackStart({
      serverFns: {
        generateFunctionId: ({ filename, functionName }) => {
          // Return a custom ID string. If you return undefined, the default is used.
          // For example, always hash (even in dev):
          // return createHash('sha256').update(`${filename}--${functionName}`).digest('hex')
          return undefined
        },
      },
    }),
    react(),
  ],
})

Tips:

  • Prefer deterministic inputs (filename + functionName) so IDs remain stable between builds.
  • If you don’t want file paths in dev IDs, return a hash in all environments.
  • Ensure the returned ID is URL-safe.

Note: Server functions use a compilation process that extracts server code from client bundles while maintaining seamless calling patterns. On the client, calls become fetch requests to the server.

Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.