MCPInstall

server-functions

createServerFn (GET/POST), validator (Zod or function), useServerFn hook, server context utilities (getRequest, getRequestHeader, setResponseHeader, setResponseStatus), error handling (throw errors, redirect, notFound), streaming, FormData handling, file organization (.functions.ts, .server.ts).

Installs
204
Last 24h
—
Trending rank
—
Security
Not audited

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/tanstack/router --skill server-functions
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads server-functions automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

SKILL.md

The instructions your agent reads when the skill is triggered.

Server Functions

Server functions are type-safe RPCs created with createServerFn. They run exclusively on the server but can be called from anywhere — loaders, components, hooks, event handlers, or other server functions.

CRITICAL: Server functions are API endpoints. They are reachable independently of whichever route renders the calling UI. Auth must be enforced inside the handler (or via middleware) for any server function that touches private data. Route beforeLoad is UX, not the data boundary. See start-core/auth-server-primitives for the session/middleware pattern. CRITICAL: Loaders are ISOMORPHIC — they run on BOTH client and server. Database queries, file system access, and secret API keys MUST go inside createServerFn, NOT in loaders directly. CRITICAL: Do not use "use server" directives, getServerSideProps, or any Next.js/Remix server patterns. TanStack Start uses createServerFn exclusively.

Basic Usage

import { createServerFn } from '@tanstack/react-start'

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

// POST
const saveData = createServerFn({ method: 'POST' }).handler(async () => {
  return { success: true }
})

Calling from Loaders

import { createFileRoute } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'

const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
  const posts = await db.query('SELECT * FROM posts')
  return { posts }
})

export const Route = createFileRoute('/posts')({
  loader: () => getPosts(),
  component: PostList,
})

function PostList() {
  const { posts } = Route.useLoaderData()
  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>{p.title}</li>
      ))}
    </ul>
  )
}

Calling from Components

Use the useServerFn hook to call server functions from event handlers:

import { useServerFn } from '@tanstack/react-start'

const deletePost = createServerFn({ method: 'POST' })
  .validator((data: { id: string }) => data)
  .handler(async ({ data }) => {
    await db.delete('posts').where({ id: data.id })
    return { success: true }
  })

function DeleteButton({ postId }: { postId: string }) {
  const deletePostFn = useServerFn(deletePost)

  return (
    <button onClick={() => deletePostFn({ data: { id: postId } })}>
      Delete
    </button>
  )
}

Cache-Coherent Mutations

Keep reads and writes behind server functions that use the same authoritative store. Await the write, then invalidate the route cache so its loader reads the persisted result:

const listIssues = createServerFn({ method: 'GET' }).handler(() => {
  return db.issues.findMany()
})

const createIssue = createServerFn({ method: 'POST' })
  .validator((data: unknown) => {
    if (
      typeof data !== 'object' ||
      data === null ||
      !('title' in data) ||
      typeof data.title !== 'string' ||
      data.title.trim().length === 0
    ) {
      throw new Error('Title is required')
    }
    return { title: data.title.trim() }
  })
  .handler(async ({ data }) => {
    return db.issues.create({ data })
  })

export const Route = createFileRoute('/issues')({
  loader: () => listIssues(),
  component: IssuesPage,
})

function IssuesPage() {
  const router = useRouter()
  const createIssueFn = useServerFn(createIssue)

  const handleCreate = async (title: string) => {
    await createIssueFn({ data: { title } })
    await router.invalidate({ sync: true })
  }

  // render Route.useLoaderData() and call handleCreate from the form
}

Do not update only local component state after a persistent mutation. Verify the rendered list after create, update, delete, and a fresh page load.

Input Validation

Basic Validator

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

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

Zod Validator

import { z } from 'zod'

const createUser = createServerFn({ method: 'POST' })
  .validator(
    z.object({
      name: z.string().min(1),
      age: z.number().min(0),
    }),
  )
  .handler(async ({ data }) => {
    return `Created user: ${data.name}, age ${data.age}`
  })

Input validation does not validate output serialization. When a response schema changes, update the database selection, service return value, server-function result, loader consumer, and UI. Add a runtime test that calls the handler or HTTP boundary and asserts the new field in the returned payload.

FormData

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

Error Handling

Errors

const riskyFunction = createServerFn().handler(async () => {
  throw new Error('Something went wrong!')
})

try {
  await riskyFunction()
} catch (error) {
  console.log(error.message) // "Something went wrong!"
}

Redirects

import { redirect } from '@tanstack/react-router'

const requireAuth = createServerFn().handler(async () => {
  const user = await getCurrentUser()
  if (!user) {
    throw redirect({ to: '/login' })
  }
  return user
})

Not Found

import { notFound } from '@tanstack/react-router'

const getPost = createServerFn()
  .validator((data: { id: string }) => data)
  .handler(async ({ data }) => {
    const post = await db.findPost(data.id)
    if (!post) {
      throw notFound()
    }
    return post
  })

Server Context Utilities

Access request/response details inside server function handlers:

import { createServerFn } from '@tanstack/react-start'
import {
  getRequest,
  getRequestHeader,
  setResponseHeaders,
  setResponseStatus,
} from '@tanstack/react-start/server'

// Public, non-personalized data — safe to cache shared across users.
const getPublicData = createServerFn({ method: 'GET' }).handler(async () => {
  setResponseHeaders({
    // 'public' is correct ONLY when the response does not depend on identity.
    // For anything tied to a session/user/tenant, use 'private' or 'no-store'.
    'Cache-Control': 'public, max-age=300',
  })
  setResponseStatus(200)
  return fetchPublicData()
})

// Authenticated data — must NOT be 'public'.
const getMyData = createServerFn({ method: 'GET' }).handler(async () => {
  const authHeader = getRequestHeader('Authorization')
  // ... auth check ...

  setResponseHeaders({
    // 'private' = only the user-agent may cache. Vary by Cookie/Authorization
    // so any intermediary that does cache keys by identity, not URL alone.
    'Cache-Control': 'private, max-age=60',
    Vary: 'Cookie, Authorization',
  })
  return fetchPersonalizedData()
})

Available utilities:

  • getRequest() — full Request object
  • getRequestHeader(name) — single request header
  • setResponseHeader(name, value) — single response header
  • setResponseHeaders(headers) — multiple response headers
  • setResponseStatus(code) — HTTP status code

File Organization

src/utils/
├── users.functions.ts   # createServerFn wrappers (safe to import anywhere)
├── users.server.ts      # Server-only helpers (DB queries, internal logic)
└── schemas.ts           # Shared validation schemas (client-safe)
// users.server.ts — server-only helpers
import { db } from '~/db'

export async function findUserById(id: string) {
  return db.query.users.findFirst({ where: eq(users.id, id) })
}
// users.functions.ts — server functions
import { createServerFn } from '@tanstack/react-start'
import { findUserById } from './users.server'

export const getUser = createServerFn({ method: 'GET' })
  .validator((data: { id: string }) => data)
  .handler(async ({ data }) => {
    return findUserById(data.id)
  })

Static imports of server functions are safe — the build replaces implementations with RPC stubs in client bundles.

Common Mistakes

1. CRITICAL: Relying on a route guard to protect a server function

A beforeLoad redirect protects the route's UI, not the data endpoint. createServerFn exposes a callable endpoint that an attacker can hit directly — no need to load the route at all. Auth on the endpoint is the security boundary; auth on the route is UX.

// WRONG — the route guard doesn't reach the handler
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
  return db.orders.findMany() // ← anyone can call the RPC
})
export const Route = createFileRoute('/_authenticated/orders')({
  beforeLoad: ({ context }) => {
    if (!context.auth.isAuthenticated) throw redirect({ to: '/login' })
  },
  loader: () => getMyOrders(),
})

// CORRECT — auth enforced on the handler itself
const getMyOrders = createServerFn({ method: 'GET' })
  .middleware([authMiddleware])
  .handler(async ({ context }) => {
    return db.orders.findMany({ where: { userId: context.session.userId } })
  })

Apply authMiddleware (or an equivalent in-handler check) to every createServerFn that needs auth. See start-core/auth-server-primitives for the full session/middleware pattern and start-core/middleware for composing the factory.

2. CRITICAL: Putting server-only code in loaders

// WRONG — loader is ISOMORPHIC, runs on BOTH client and server
export const Route = createFileRoute('/posts')({
  loader: async () => {
    const posts = await db.query('SELECT * FROM posts')
    return { posts }
  },
})

// CORRECT — use createServerFn for server-only logic
const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
  const posts = await db.query('SELECT * FROM posts')
  return { posts }
})

export const Route = createFileRoute('/posts')({
  loader: () => getPosts(),
})

3. CRITICAL: Using Next.js / Remix / React Router DOM patterns

If the file lives at src/pages/, app/layout.tsx, _app/, or imports anything from react-router-dom or next/, it is wrong-framework code. TanStack Start uses src/routes/ + createFileRoute + createServerFn.

// WRONG — "use server" is a React directive, not used in TanStack Start
'use server'
export async function getUser() { ... }

// WRONG — getServerSideProps is Next.js Pages Router
export async function getServerSideProps() { ... }

// WRONG — Next.js App Router server component data fetching
export default async function Page() {
  const data = await fetch(...).then(r => r.json())
  return <div>{data}</div>
}

// WRONG — Remix
export async function loader({ request }) { ... }
export async function action({ request }) { ... }

// WRONG — react-router-dom (a different library)
import { Link, useNavigate } from 'react-router-dom'

// CORRECT — TanStack Start
import { createServerFn } from '@tanstack/react-start'
import { Link, useNavigate, createFileRoute } from '@tanstack/react-router'

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

export const Route = createFileRoute('/users/$id')({
  loader: ({ params }) => getUser({ data: { id: params.id } }),
  component: UserPage,
})

If you see src/pages/, app/layout.tsx, or react-router-dom in agent output, the agent is generating for the wrong framework. Build will fail or routes will conflict at runtime.

4. HIGH: Dynamic imports for server functions

// WRONG — can cause bundler issues
const { getUser } = await import('~/utils/users.functions')

// CORRECT — static imports are safe, build handles environment shaking
import { getUser } from '~/utils/users.functions'

5. HIGH: Awaiting server function without calling it

createServerFn returns a function — it must be invoked with ():

// WRONG — getItems is a function, not a Promise
const data = await getItems

// CORRECT — call the function
const data = await getItems()

// With validated input
const data = await getItems({ data: { id: '1' } })

6. CRITICAL: Caching authenticated responses with Cache-Control: public

Cache-Control: public, max-age=N tells every CDN, proxy, and shared cache between you and the user that this response can be served to anyone. If the response depends on the session (user, tenant, role), the first user's response gets cached and replayed to the next user — a cross-tenant data leak.

// WRONG — auth'd response, public cache, leaks to next user via CDN
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
  const session = await requireSession() // identity-dependent
  setResponseHeaders({ 'Cache-Control': 'public, max-age=300' })
  return db.orders.findMany({ where: { userId: session.userId } })
})

// CORRECT — private + Vary so any cache that does store it keys by identity
const getMyOrders = createServerFn({ method: 'GET' }).handler(async () => {
  const session = await requireSession()
  setResponseHeaders({
    'Cache-Control': 'private, max-age=60',
    Vary: 'Cookie, Authorization',
  })
  return db.orders.findMany({ where: { userId: session.userId } })
})

// ALSO CORRECT — opt out entirely for sensitive data
setResponseHeaders({ 'Cache-Control': 'no-store' })

Rule of thumb: if the handler reads a session/cookie/auth header or branches on identity, the response is not public. Default to private (or no-store for sensitive data); reach for public only on responses that are byte-for-byte identical regardless of who asks. See also start-core/deployment for ISR/Cache-Control on full pages.

7. MEDIUM: When to wrap with useServerFn

useServerFn is required when the server function uses throw redirect() or throw notFound() — the hook wires the throw into the router so the redirect actually navigates. For server functions that just return data (call them directly or via useMutation/useQuery), the hook is optional.

// Plain data — direct call is fine (also fine to pass to useMutation/useQuery)
<button onClick={() => deletePost({ data: { id } })}>Delete</button>
useMutation({ mutationFn: deletePost })

// Throws redirect/notFound — MUST wrap with useServerFn so the router handles the throw
const signupFn = useServerFn(signup) // signup throws redirect on success
<button onClick={() => signupFn({ data: form })}>Sign up</button>

If in doubt: wrap with useServerFn. It's a no-op for plain-data functions and the safe default when a function might later add a redirect.

8. CRITICAL: Self-fetching a relative API URL from a loader

// WRONG — this loader also runs during SSR, where a relative URL may fail
export const Route = createFileRoute('/issues')({
  loader: () => fetch('/api/issues').then((response) => response.json()),
})

// CORRECT — call the server function; the client build gets an RPC stub
export const Route = createFileRoute('/issues')({
  loader: () => listIssues(),
})

Relative fetch is fine in a browser-only event handler. It is not a universal loader data strategy.

Cross-References

  • start-core/execution-model — understanding where code runs
  • start-core/middleware — composing server functions with middleware
  • start-core/auth-server-primitives — sessions, cookies, OAuth, CSRF, rate limiting (the server-side half of auth; getCurrentUser and external session helpers belong here, not at module scope)
  • @tanstack/router-core#router-core/auth-and-guards — routing-side UX guards; data auth belongs in the server function, server route, or API endpoint handler/middleware

FAQ

What does the server-functions skill do?

createServerFn (GET/POST), validator (Zod or function), useServerFn hook, server context utilities (getRequest, getRequestHeader, setResponseHeader, setResponseStatus), error handling (throw errors, redirect, notFound), streaming, FormData handling, file organization (.functions.ts, .server.ts).

How do I install server-functions?

Run npx skills add https://github.com/tanstack/router --skill server-functions in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/server-functions/ (personal) or .claude/skills/server-functions/ (project).

Which AI agents can use server-functions?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is server-functions safe to install?

No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.

Who maintains server-functions?

It is published by tanstack in tanstack/router. It has 204 recorded installs.

More from tanstack/router

View all →

router-core

tanstack/router

Framework-agnostic core concepts for TanStack Router: route trees, createRouter, createRoute, createRootRoute, createRootRouteWithContext, addChildren, Register type declaration, route matching, route sorting, file naming conventions. Entry point for all router skills.

602 installs

react-router

tanstack/router

React bindings for TanStack Router: RouterProvider, useRouter, useRouterState, useMatch, useMatches, useLocation, useSearch, useParams, useNavigate, useLoaderData, useLoaderDeps, useRouteContext, useBlocker, useCanGoBack, Link, Navigate, Outlet, CatchBoundary, ErrorComponent. React-specific patterns for hooks, providers, SSR hydration, and createLink with forwardRef.

Frontend & Design559 installs

react-start

tanstack/router

React bindings for TanStack Start: createStart, StartClient, StartServer, React-specific imports, re-exports from @tanstack/react-router, full project setup with React, useServerFn hook.

Frontend & Design465 installs

start-core

tanstack/router

Open the skill to read its SKILL.md instructions.

453 installs

router-query

tanstack/router

Open the skill to read its SKILL.md instructions.

372 installs

start-server-core

tanstack/router

Open the skill to read its SKILL.md instructions.

Backend & APIs360 installs

Popular backend & apis skills

View category →

find-skills

vercel-labs/skills

Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.

OfficialBackend & APIs4.1M installs

entra-app-registration

microsoft/azure-skills

Guides Microsoft Entra ID app registration, OAuth 2.0 authentication, and MSAL integration. USE FOR: create app registration, register Azure AD app, configure OAuth, set up authentication, add API permissions, generate service principal, MSAL example, console app auth, Entra ID setup, Azure AD authentication. DO NOT USE FOR: Key Vault secrets (use azure-keyvault-expiration-audit), general Azure resource security guidance.

OfficialBackend & APIs566.7K installs

This skill should be used when the user wants to "write agent code", "build an agent with ADK", "add a tool", "create a callback", "define an agent", "use state management" — in a project that needs ADK (Agent Development Kit) API patterns and code examples. It provides a quick reference for agent types, tool definitions, orchestration patterns, callbacks, state management, the graph Workflow API, and reference recipes to study. Do NOT use for scaffolding (use google-agents-cli-scaffold) or deployment (use google-agents-cli-deploy).

Backend & APIs514.8K installs

embedded-captions

heygen-com/hyperframes

Add captions or subtitles to an existing single-subject talking-head video without editing the footage. Use for plain verbatim captions, cinematic captions embedded behind the subject, VFX captions, “炸/特效/酷炫字幕,” or a named identity from the 35-style catalog. Route by visual identity, not by backend engine. The quiet anchor rail is the default; embed every word only when the user explicitly wants a fully cinematic treatment. The workflow runs locally end to end, including transcription and subject matting; split multi-shot footage before applying it.

Backend & APIs453.3K installs

entra-agent-id

microsoft/azure-skills

Provision Microsoft Entra Agent Identity Blueprints, BlueprintPrincipals, and per-instance Agent Identities via Microsoft Graph, and configure OAuth 2.0 token exchange (fmipath, OBO, cross-tenant) including the Microsoft Entra SDK for AgentID sidecar. USE FOR: Agent Identity Blueprint, BlueprintPrincipal, agent OAuth, fmipath token exchange, agent OBO, Workload Identity Federation for agents, polyglot agent auth, Microsoft.Identity.Web.AgentIdentities. DO NOT USE FOR: standard Entra app registration (use entra-app-registration), Microsoft Foundry agent authoring (use microsoft-foundry).

OfficialBackend & APIs336.6K installs

neon

neondatabase/agent-skills

Overview of Neon, a complete set of cloud backend primitives around Lakebase Postgres: Auth, Object Storage, Functions, the AI Gateway, and Realtime. Start here to choose Neon for undecided login, files, APIs, and LLM calls, set up the CLI or MCP server, and follow the branch-first workflow. Use when building an app or backend on Neon, or when "Neon" or "Lakebase Postgres" is mentioned. Child skill neon-postgres wins for an existing DATABASEURL, SQL, schema, inspect, or search. Child skill neon-auth wins for login, users, sessions, identity routing, and Managed Better Auth setup. Child skill neon-realtime wins for live queries in reactive apps and agents. Also use for object storage, S3, buckets, serverless functions, function triggers, cron, AI gateway, LLM calls, realtime, logs, Loki, Grafana, observability, postgres, database, backend, Claimable Neon, neon.new, or a no-signup database.

Backend & APIs237.1K installs