Define a server function
Define a server function in the Server functions guide.
serverFn(options, handler) returns a callable. The handler receives the validated input and the
request event. The call must be the initializer of an exported
binding at the top level of a module, so that the build can find and register it.
The project starts with a note type, a Standard Schema validator and an in-memory data module. src/schema.ts is the
Standard Schema implementation; any Standard Schema object works in its place.
db.server.ts ends in .server, so the build treats it as server-only (see Keep secrets out of the
browser).
export interface Note {
id: string;
title: string;
owner: string;
}
import type { StandardSchemaV1, StandardSchemaV1Result } from "@shadow-js/dusk/server";
/** Wraps a check function as a Standard Schema: it returns `{ value }` or `{ issues }`. */
export function schema<T>(
check: (value: unknown) => StandardSchemaV1Result<T>,
): StandardSchemaV1<T, T> {
return { "~standard": { version: 1, vendor: "app", validate: check } };
}
import { schema } from "./schema";
function field(value: unknown, name: string): unknown {
return typeof value === "object" && value !== null && name in value
? Reflect.get(value, name)
: undefined;
}
export const noteId = schema<{ id: string }>((value) => {
const id = field(value, "id");
return typeof id === "string"
? { value: { id } }
: { issues: [{ message: "id must be a string", path: ["id"] }] };
});
export const newNote = schema<{ title: string }>((value) => {
const title = field(value, "title");
if (typeof title !== "string" || title.trim() === "") {
return { issues: [{ message: "title is required", path: ["title"] }] };
}
if (title.length > 80) {
return { issues: [{ message: "title is longer than 80 characters", path: ["title"] }] };
}
return { value: { title } };
});
import type { Note } from "../types";
/** A stand-in for a database client. */
export const notes = new Map<string, Note>([
["1", { id: "1", title: "First note", owner: "u1" }],
]);
import { HttpError, serverFn } from "@shadow-js/dusk/server";
import { newNote, noteId } from "../schemas";
import type { Note } from "../types";
import { notes } from "./db.server";
export const getNote = serverFn({ method: "GET", input: noteId }, async ({ id }): Promise<Note> => {
const note = notes.get(id);
if (note === undefined) throw new HttpError(404, "No such note");
return note;
});
export const addNote = serverFn({ input: newNote }, async ({ title }): Promise<Note> => {
for (const note of notes.values()) {
if (note.title === title) throw new HttpError(409, "That title exists", { data: { id: note.id } });
}
const note: Note = { id: String(notes.size + 1), title, owner: "anonymous" };
notes.set(note.id, note);
return note;
});
export const stats = serverFn({ method: "GET" }, async (): Promise<number> => {
throw new Error("connection to db.internal:5432 refused");
});
A loader may call a server function. On the server for the first request that is a direct call; in the browser, for later navigations, the same line calls the stub.
import { type LoaderArgs, useLoaderData } from "@shadow-js/router";
import type { JSX } from "@shadow-js/web";
import type { Note } from "../../types";
import { getNote } from "../../server/notes";
export const loader = ({ params }: LoaderArgs<"/notes/:id", unknown>): Promise<Note> =>
getNote({ id: params.id });
export default function NotePage(): JSX.Element {
const note = useLoaderData<Note>();
return <h1>{note()?.title}</h1>;
}
Use onError in the server entry to send unexpected errors to your logger.
import type { HandlerOptions } from "@shadow-js/dusk/server";
export const options: Omit<HandlerOptions, "manifest"> = {
security: { allowedHosts: ["app.example.com"] },
onError(error, info) {
console.error(info.phase, error);
},
};
The build finds each function, gives it an id, and registers it at /_dusk/fn/<id>. A server function is also a
normal function on the server: await addNote({ title }) inside a loader, an API route or another server
function validates the input and runs the handler in the current request. Outside a request there is no event;
use invoke(input, event) to pass one explicitly (see Middleware).
The build also gives the server-side function the id and url its browser stub has, so markup rendered on the
server can write <form action={fn.url}> for a function defined with form: true (see Form posts).
Reading id or url on a function the build did not process, for example in a unit test that imports the module on
its own, throws SERVER_FN_NOT_BUILT.
What the build accepts is strict, because it reads the source rather than running it:
- The options must be an object literal.
methodmust be the literal"GET"or"POST"andformthe literaltrueorfalse; other keys (input,middleware) may be any expression. export const name = serverFn(...),export default serverFn(...)andconst f = serverFn(...); export { f }are found. AserverFn(...)call anywhere else (inside a function, as an argument, not exported) is an error.- The name must be a plain identifier; destructuring is rejected.