Skip to main content

Server Functions

Server functions let you write backend logic alongside your frontend code and call them from React components as if they were local functions. While not strictly required, we recommend suffixing server function files with .server.ts. The build system transforms them into RPC calls automatically.

Basic Usage

// src/api/users.server.ts
"use server";

export async function getUsers() {
return await db.users.findMany();
}

export async function createUser(name: string, email: string) {
return await db.users.create({ data: { name, email } });
}

Rules

  • File must start with "use server"; directive
  • Only named async function exports are transformed
  • Recommendation: Use the .server.ts extension (e.g. users.server.ts) or place them in a src/api/ directory to help differentiate them from client code.
  • No default exports — only named exports

Query Patterns

evjs provides type-safe useQuery and useSuspenseQuery that accept server functions directly. Server function stubs also carry .queryKey(), .fnId, and .fnName properties for cache invalidation and introspection.

import {
useQuery,
useSuspenseQuery,
useMutation,
useQueryClient,
getFnQueryKey,
getFnQueryOptions,
} from "@evjs/client";
import { getUsers, getUser, createUser } from "../api/users.server";

// Queries — pass server functions directly, types are inferred
const { data: users } = useQuery(getUsers); // data: User[]
const { data: user } = useQuery(getUser, userId); // data: User
const { data } = useSuspenseQuery(getUsers); // data: User[] (guaranteed)

// Mutations — pass server functions directly, just like useQuery
const queryClient = useQueryClient();
const { mutate } = useMutation(createUser, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: getFnQueryKey(getUsers) });
},
});

// Route loaders / prefetching — use getFnQueryOptions()
loader: ({ context }) =>
context.queryClient.ensureQueryData(getFnQueryOptions(getUsers));

Server Function Metadata

Every registered server function stub carries these properties at runtime:

getFnQueryKey(getUsers)         // → ["<fnId>"]
getFnQueryKey(getUsers, someArg)// → ["<fnId>", someArg]
getUsers.fnId // → "<hash>" (stable SHA-256)
getUsers.fnName // → "getUsers"
  • getFnQueryKey(fn, ...args) — Build a TanStack Query key. Use for invalidateQueries, setQueryData, etc.
  • .fnId — The stable internal function ID (read-only).
  • .fnName — The human-readable export name (read-only).
  • getFnQueryOptions(fn, ...args) — Returns { queryKey, queryFn } for loaders, prefetch, and useInfiniteQuery.

Mutation Arguments

// Single argument: pass object directly
mutate({ name: "Alice", email: "alice@example.com" });

// Multiple arguments: pass as array
mutate(["Alice", "alice@example.com"]);

Raw fetch / Non-Server Functions

For non-server functions, use the standard TanStack Query API directly:

const { data } = useQuery({
queryKey: ["github-user", username],
queryFn: () =>
fetch(`https://api.github.com/users/${username}`).then((r) => r.json()),
});

Transport Configuration

HTTP (Default)

import { initTransport } from "@evjs/client";

initTransport({ endpoint: "/api/fn" });

Custom Transport (e.g., WebSocket)

Implement the ServerTransport interface for custom protocols:

import { initTransport } from "@evjs/client";
import type { ServerTransport } from "@evjs/client";

const wsTransport: ServerTransport = {
call: async (fnId, args) => {
// Implement your WebSocket or custom protocol here
},
};

initTransport({ transport: wsTransport });

Server Config

// ev.config.ts
import { defineConfig } from "@evjs/ev";

export default defineConfig({
server: {
endpoint: "/api/fn", // default
},
});

Error Handling

Server Side

Throw structured errors with status codes and data:

import { ServerError } from "@evjs/server";

export async function getUser(id: string) {
const user = await db.users.findById(id);
if (!user) {
throw new ServerError("User not found", {
status: 404,
data: { id },
});
}
return user;
}

Client Side

Catch typed errors:

import { ServerFunctionError } from "@evjs/client";

try {
const user = await getUser("123");
} catch (e) {
if (e instanceof ServerFunctionError) {
console.log(e.message); // "User not found"
console.log(e.status); // 404
console.log(e.data); // { id: "123" }
}
}

Build Pipeline

At build time, the "use server" directive triggers two separate transforms:

  • Client build: function bodies → __fn_call(fnId, args) stubs
  • Server build: original bodies preserved + registerServerFn(fnId, fn) injected
  • Function IDs are stable SHA-256 hashes from filePath + exportName

Key Points

PatternUsage
QueryuseQuery(fn, ...args)
Suspense queryuseSuspenseQuery(fn, ...args)
MutationuseMutation(fn) or useMutation(fn, { onSuccess })
Cache invalidationgetFnQueryKey(...args)
Loader / prefetchgetFnQueryOptions(...args){ queryKey, queryFn }
Function metadatafn.fnId, fn.fnName
ArgumentsSpread: useQuery(getUser, id) not useQuery(getUser, [id])
Server errorsServerError on server → ServerFunctionError on client