grubba:rpc
Status: experimental — API may change without notice
A modern, type-safe RPC layer for Meteor methods. Thin syntactic sugar over
Meteor.methods + Meteor.callAsync with runtime schema validation and full
TypeScript inference.
Install
meteor add grubba:rpc
Quick start
1import { RPC } from "meteor/grubba:rpc"; 2import { z } from "zod"; 3 4// Define a schema and a method in one expression 5export const addTodo = RPC.method( 6 z.object({ title: z.string().min(1) }), 7 async ({ title }) => { 8 return Todos.insertAsync({ title, createdAt: new Date() }); 9 } 10); 11 12// Call it — works identically on client and server 13const id = await addTodo({ title: "Buy milk" });
The method is auto-registered with Meteor.methods(). The variable name is
your import handle; no name: field needed.
Schemas
Zod (Standard Schema)
1import { z } from "zod"; 2const schema = z.object({ _id: z.string(), isPrivate: z.boolean() });
Valibot (Standard Schema)
1import * as v from "valibot"; 2const schema = v.object({ _id: v.string(), isPrivate: v.boolean() });
ArkType (Standard Schema)
1import { type } from "arktype"; 2const schema = type({ _id: "string", isPrivate: "boolean" });
Meteor check / Match patterns
1import { Match } from "meteor/check"; 2 3// Match pattern 4const schema = Match.ObjectIncluding({ _id: String, isPrivate: Boolean }); 5 6// Plain constructor / object shape (shorthand check syntax) 7const schema = { _id: String, isPrivate: Boolean };
server() — inline server guard
Use server() to scope expensive or sensitive code to the server only inside
an isomorphic method handler:
1import { RPC, server } from "meteor/grubba:rpc"; 2 3export const setPrivate = RPC.method( 4 { _id: String, isPrivate: Boolean }, 5 async ({ _id, isPrivate }) => { 6 // Isomorphic code (optimistic updates, etc.) can live here … 7 8 return server(async () => { 9 // … sensitive/heavy server-only work lives here 10 return Todos.updateAsync({ _id }, { $set: { isPrivate } }); 11 }); 12 } 13);
Note (phase 1):
server()is a runtime guard (Meteor.isServer). The code inside the callback still ships to the client bundle — it just never executes there. Use a server-only import path if you need to keep secrets out of the client bundle.
Options
1RPC.method(schema, handler, { 2 name: "todos.setPrivate", // explicit method name (default: auto-derived) 3 serverOnly: true, // skip client stub; no optimistic UI 4});
Conventions
| Concern | Convention |
|---|---|
| File location | Co-locate with your collection: imports/api/todos/methods.ts |
| Naming | export const verbNoun = RPC.method(...) — the export name is your handle |
| Schema | Prefer Standard Schema libraries for full TypeScript inference |
| Server secrets | Wrap in server() or use a .server.ts file |
How it works
RPC.method(schema, fn, opts) │ ├─ getCallSiteId() generates a stable opaque ID from Error.stack ├─ Meteor.methods({ id }) registers validated handler for DDP └─ returns callable ├─ server → fn(args) directly └─ client → Meteor.callAsync(id, args)
Validation runs server-side (and client-side for stubs). On failure a
Meteor.Error('validation-error', …) is thrown so the client receives a
structured DDP error.
Phase 1 scope
-
RPC.method()with auto-naming - Standard Schema support (Zod, Valibot, ArkType)
-
check/Matchsupport -
server()helper - TypeScript inference for args
-
RPC.publish()— phase 2 - HTTP endpoint — phase 2
- Build plugin / tree-shaking — phase 2