The zero-server package provides APIs for processing mutation requests from Zero clients. Use these functions to handle client mutations in your API server.
handleMutateRequest
Processes a mutation request from a Zero client.
import {handleMutateRequest} from '@rocicorp/zero/server';
import {zeroNodePg} from '@rocicorp/zero/server/adapters/pg';
import {Pool} from 'pg';
const pool = new Pool({connectionString: process.env.ZERO_UPSTREAM_DB});
const zql = zeroNodePg(schema, pool);
app.post('/api/zero/mutate', async (req, res) => {
const response = await handleMutateRequest(
zql,
async (transact, mutation) => {
const mutator = getMutation(serverMutators, mutation.name);
return transact(async (tx, name, args) => {
await mutator(tx, args, {});
});
},
req,
'info'
);
res.json(response);
});
Parameters
Database provider implementing the Database interface. Use one of the database adapters.
Callback function that processes each mutation. Receives:
transact: Function to execute the mutation in a transaction
mutation: The mutation object with name, args, id, and clientID
Must return a MutationResponse.
request
Request | URLSearchParams | Record<string, string>
required
The HTTP request or query parameters containing mutation data.
Request body (when using URLSearchParams overload).
Logging level: 'debug', 'info', 'warn', or 'error'.
Returns
Returns a Promise<PushResponse> containing mutation results or error information.
Overloads
// Request overload
function handleMutateRequest<D extends Database<ExtractTransactionType<D>>>(
dbProvider: D,
callback: (transact: TransactFn<D>, mutation: CustomMutation) => Promise<MutationResponse>,
request: Request,
logLevel?: LogLevel,
): Promise<PushResponse>;
// Query string overload
function handleMutateRequest<D extends Database<ExtractTransactionType<D>>>(
dbProvider: D,
callback: (transact: TransactFn<D>, mutation: CustomMutation) => Promise<MutationResponse>,
queryString: URLSearchParams | Record<string, string>,
body: ReadonlyJSONValue,
logLevel?: LogLevel,
): Promise<PushResponse>;
getMutation
Retrieves a mutator function by name from a mutator registry.
import {getMutation} from '@rocicorp/zero/server';
const mutator = getMutation(serverMutators, 'todo.create');
Parameters
mutators
AnyMutatorRegistry | CustomMutatorDefs<any>
required
Mutator registry or custom mutator definitions.
Mutator name. Supports dot notation (e.g., 'todo.create') and pipe notation (e.g., 'todo|create').
Returns
Returns a CustomMutatorImpl function that can be called with (tx, args, ctx).
Transaction Types
Database
Abstract interface for databases that can execute transactions.
interface Database<T> {
transaction: <R>(
callback: (tx: T, transactionHooks: TransactionProviderHooks) => MaybePromise<R>,
transactionInput?: TransactionProviderInput,
) => Promise<R>;
}
TransactionProviderHooks
Hooks provided by the database adapter to manage client mutation tracking.
interface TransactionProviderHooks {
updateClientMutationID: () => Promise<{lastMutationID: number | bigint}>;
writeMutationResult: (result: MutationResponse) => Promise<void>;
deleteMutationResults: (args: CleanupResultsArg) => Promise<void>;
}
Input provided to transactions for mutation tracking.
interface TransactionProviderInput {
upstreamSchema: string;
clientGroupID: string;
clientID: string;
mutationID: number;
}
Error Handling
OutOfOrderMutation
Thrown when a client sends a mutation with an unexpected ID.
class OutOfOrderMutation extends Error {
constructor(
clientID: string,
receivedMutationID: number,
lastMutationID: number | bigint,
);
}
ApplicationError
Wrap errors to provide structured error information to clients.
import {ApplicationError} from '@rocicorp/zero/server';
throw new ApplicationError('User not found', {userId: args.id});
Error message displayed to the client.
Additional error details sent to the client.
Complete Example
import {handleMutateRequest, getMutation, ApplicationError} from '@rocicorp/zero/server';
import {zeroPostgresJS} from '@rocicorp/zero/server/adapters/postgresjs';
import {defineMutator, defineMutators} from '@rocicorp/zero';
import postgres from 'postgres';
import {z} from 'zod/mini';
import express from 'express';
const sql = postgres(process.env.ZERO_UPSTREAM_DB!);
const zql = zeroPostgresJS(schema, sql);
const serverMutators = defineMutators({
todo: {
create: defineMutator(
z.object({id: z.string(), text: z.string()}),
async ({tx, args}) => {
if (tx.location !== 'server') {
throw new Error('Server-only mutator');
}
// Validate text length
if (args.text.length > 1000) {
throw new ApplicationError('Text too long', {
maxLength: 1000,
actualLength: args.text.length,
});
}
await tx.dbTransaction.wrappedTransaction`
INSERT INTO todo (id, text, complete)
VALUES (${args.id}, ${args.text}, false)
`;
},
),
},
});
const app = express();
app.use(express.json());
app.post('/api/zero/mutate', async (req, res) => {
const response = await handleMutateRequest(
zql,
async (transact, mutation) => {
const mutator = getMutation(serverMutators, mutation.name);
return transact(async (tx, name, args) => {
await mutator(tx, args, {});
});
},
req,
'info'
);
res.json(response);
});
app.listen(3000);
Best Practices
- Validate inputs: Always validate mutation arguments before executing
- Use ApplicationError: Throw
ApplicationError for user-facing errors
- Check transaction location: Verify
tx.location === 'server' for server-only mutations
- Handle retries:
ApplicationError instances are retried automatically
- Set appropriate log levels: Use
'debug' in development, 'info' in production