Skip to main content

createServerRpc

Creates a server-side RPC handler that lazily loads and executes a server function. This is used internally by the build system to create efficient server-side endpoints.
This is a low-level API typically used by the TanStack Start build plugin. Most users should use createServerFn instead.

Basic Usage

import { createServerRpc } from '@tanstack/start-server-core/createServerRpc'

const rpcHandler = createServerRpc(
  { id: 'my-function', name: 'myFunction', filename: '/src/functions.ts' },
  async () => {
    // Lazy load the actual server function implementation
    const { myFunction } = await import('./functions')
    return myFunction
  }
)

API Reference

serverFnMeta
ServerFnMeta
required
Metadata about the server function:
{
  id: string       // Unique function identifier
  name: string     // Function name
  filename: string // Source file path
}
splitImportFn
Function
required
An async function that dynamically imports and returns the server function implementation. This enables code splitting and lazy loading.
async (...args: any[]) => ServerFunction

Return Value

Returns a function with the following properties:
url
string
The URL endpoint for this server function, constructed from TSS_SERVER_FN_BASE environment variable and the function ID.
serverFnMeta
ServerFnMeta
The metadata object passed during creation.
[TSS_SERVER_FUNCTION]
true
Internal marker indicating this is a server function.

How It Works

  1. Build Time: The build plugin identifies server functions and generates RPC handlers
  2. Runtime: When called, the RPC handler lazy loads the actual implementation
  3. Execution: The loaded function is executed and its result returned

Environment Variables

TSS_SERVER_FN_BASE
string
Base URL path for server functions. Defaults to /api/fn/. The final URL is constructed as TSS_SERVER_FN_BASE + functionId.

Generated Code Example

When you define a server function:
// src/functions.ts
export const myFunction = createServerFn({ method: 'GET' }).handler(async () => {
  return { data: 'result' }
})
The build plugin generates:
// .tanstack/functions-manifest.ts (simplified)
import { createServerRpc } from '@tanstack/start-server-core/createServerRpc'

export const myFunction = createServerRpc(
  {
    id: 'my-function-hash',
    name: 'myFunction',
    filename: '/src/functions.ts'
  },
  async () => {
    const mod = await import('/src/functions.ts')
    return mod.myFunction
  }
)

Lazy Loading Benefits

Memory Efficiency

Server functions are only loaded when called, not at startup:
// Heavy server function
const processBigData = createServerFn().handler(async () => {
  const bigLib = await import('heavy-library') // Only loaded when needed
  return bigLib.process()
})

// RPC handler doesn't load the function until first call
const rpc = createServerRpc(meta, () => import('./functions'))

Code Splitting

Each server function can be in its own chunk:
const rpc1 = createServerRpc(meta1, () => import('./chunk1'))
const rpc2 = createServerRpc(meta2, () => import('./chunk2'))
// chunk1 and chunk2 are loaded independently

Internal Usage

The server handler uses this to look up and execute functions by ID:
import { getServerFnById } from '@tanstack/start-server-core'

// Request comes in for /api/fn/my-function-hash
const handler = async (request: Request) => {
  const functionId = extractIdFromUrl(request.url)
  const serverFn = await getServerFnById(functionId)
  
  // serverFn is the result of createServerRpc
  const result = await serverFn.__executeServer({
    method: request.method,
    data: await parseRequestData(request),
    headers: request.headers
  })
  
  return result
}

Comparison with createClientRpc

AspectcreateServerRpccreateClientRpc
EnvironmentServer-side onlyClient-side only
PurposeLazy load server functionsCall server by ID
ImportDynamic/lazyDirect
Code SplittingEnablesConsumes

Advanced Example

import { createServerRpc } from '@tanstack/start-server-core/createServerRpc'

// Create RPC with full metadata
const getUserRpc = createServerRpc(
  {
    id: 'get-user-abc123',
    name: 'getUser',
    filename: '/src/api/users.ts'
  },
  async () => {
    // Lazy load the implementation
    const mod = await import('/src/api/users.ts')
    return mod.getUser
  }
)

// Function properties
console.log(getUserRpc.url) // '/api/fn/get-user-abc123'
console.log(getUserRpc.serverFnMeta.name) // 'getUser'
console.log(getUserRpc.serverFnMeta.filename) // '/src/api/users.ts'

// The actual function is loaded on first call
const result = await getUserRpc('user-123')

Performance Considerations

Cold Starts

First call to a function will be slower due to import:
const rpc = createServerRpc(meta, () => import('./heavy-function'))

// First call: ~100ms (includes import time)
await rpc()

// Subsequent calls: ~10ms (cached)
await rpc()

Preloading

You can preload functions that you know will be needed:
const rpc = createServerRpc(meta, importFn)

// Preload the function
await importFn()

// Later calls use cached implementation
await rpc() // Fast

Bundle Size

RPC handlers add minimal overhead:
// Just the RPC handler: ~100 bytes
const rpc = createServerRpc(meta, importFn)

// vs full function with dependencies: ~50KB
import { actualFunction } from './big-module'

Type Safety

createServerRpc does not provide type safety for the function signature. The splitImportFn parameter accepts any arguments and returns any value. This is by design for maximum flexibility at the build level.
For type-safe server functions, use createServerFn which preserves full type information.

Build Plugin Integration

The TanStack Start build plugin automatically:
  1. Scans for createServerFn usage
  2. Extracts server function implementations
  3. Generates unique IDs and metadata
  4. Creates createServerRpc wrappers
  5. Generates a manifest for server-side lookups

Security

The function ID acts as a secure reference:
// ID is a hash, not predictable
const rpc = createServerRpc(
  { id: 'sha256-abc123...', name: 'secretFn', filename: '/src/api.ts' },
  importFn
)

// Client can't access server code, only call by ID

Error Handling

Errors during import or execution are propagated:
const rpc = createServerRpc(meta, async () => {
  throw new Error('Failed to load function')
})

try {
  await rpc()
} catch (error) {
  console.error(error) // 'Failed to load function'
}

Build docs developers (and LLMs) love