Skip to content

Handlers

Build local handlers that run actions in the current process.

A local handler runs actions in the current process. Build one, register it on the runtime, and it answers incoming requests. There are three ways to build one — pick whichever reads best.

server.ts
import { createLocalHandler } from "@nice-code/action";
const userHandler = createLocalHandler().forDomainActionCases(userDomain, {
getUser: async (action) => {
const user = await db.users.find(action.input.userId);
if (!user) throw err_user.fromId("not_found", { userId: action.input.userId });
return user;
},
updateName: async (action) => {
await db.users.update(action.input.userId, { name: action.input.name });
return { success: true };
},
});

Each handler receives the full actionaction.input is typed, and action.context carries routing info like originClient (see Bi-directional).

const userHandler = createLocalHandler()
.forAction(userDomain.action.getUser, async ({ input }) => db.users.find(input.userId));
const userHandler = userDomain.wrapAsLocalHandler({
getUser: async ({ userId }) => { /* ... */ },
updateName: async ({ userId, name }) => { /* ... */ },
});

Here each handler receives the destructured input directly rather than the action wrapper — the most concise form when you don’t need action.context.

wrapAsPartialLocalHandler is the same as wrapAsLocalHandler but lets you implement only some of a domain’s actions — useful for local-first clients that resolve a few actions themselves and forward the rest.

const partial = userDomain.wrapAsPartialLocalHandler({
getUser: async ({ userId }) => cache.get(userId), // others forwarded over the carrier
});

Once built, handlers are passed to serveChannel (acceptor) or registered for toConnector pushes via connectChannel’s onPush (connector).

Next: Serving & connecting →