grubba:rpc

v1.0.0Published yesterday

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

ConcernConvention
File locationCo-locate with your collection: imports/api/todos/methods.ts
Namingexport const verbNoun = RPC.method(...) — the export name is your handle
SchemaPrefer Standard Schema libraries for full TypeScript inference
Server secretsWrap 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 / Match support
  • server() helper
  • TypeScript inference for args
  • RPC.publish() — phase 2
  • HTTP endpoint — phase 2
  • Build plugin / tree-shaking — phase 2