Introducing the grubba-rpc package

Gabriel Grubba
Meteor Blog
Published in
3 min readNov 8, 2022

--

RPC Tutorial

In this tutorial, I will be teaching how to gradually add to your code base our new package grubba-rpc for more details on what this package ships with, you can check out here.

If you want to follow along, you can use this template or get the final source code here.

Where to start?

Start installing the packages:

meteor npm i grubba-rpc
meteor npm i zod

Then choose a module that you want to create. You can use the old methods that you already have.

Follow along in api/tasks/tasks.methods.js

You can move from something like this:

// in server.js
/**
* Insert a task for the logged user.
* @param {{ description: String }}
* @throws Will throw an error if user is not logged in.
*/
const insertTask = ({ description }) => {
checkLoggedIn();
TasksCollection.insert({
description,
userId: Meteor.userId(),
createdAt: new Date(),
});
};
Meteor.methods({
insertTask
})

It has the same effect as this:

/**
* Insert a task for the logged user.
* @param {{ description: String }}
* @throws Will throw an error if user is not logged in.
*/
const insertTask = ({ description }) => {
checkLoggedIn();
TasksCollection.insert({
description,
userId: Meteor.userId(),
createdAt: new Date(),
});
};
const insert = createMethod('insertTask', z.object({ description: z.string() }), insertTask);

The key difference is on the client:

Before:

Meteor.call('insertTask', { description }, err => {
if (err) {
const errorMessage = err?.reason || 'Sorry, please try again.';
actions.setStatus(errorMessage);
} else {
actions.resetForm();
}
actions.setSubmitting(false);
});

With RPC:

Tasks.insert({ description })
.then(() => {
actions.resetForm();
})
.catch(err => {
const errorMessage = err?.reason || 'Sorry, please try again.';
actions.setStatus(errorMessage);
})
.finally(() => {
actions.setSubmitting(false);
})

What are the advantages?

As using a JavaScript object has some perks, the lib was written in TypeScript, and because of that, in addition to Zod helping make those validations, we have fully typed methods.

Hooks

You can have hooks. For example, you can throw an email every time someone adds a new task, when someone removes a task or when something goes wrong toggling as done.

import { insert, remove, setChecked } from "./mutations";remove.addBeforeResolveHook((raw, parsed) => {
console.log(parsed, 'email')
})
insert.addAfterResolveHook((raw, parsed, result) => {
console.log(parsed, 'email')
})
setChecked.addErrorResolveHook((raw, parsed, error) => {
console.log(error, 'email')
})

This is on examples/simpletasks/common/tasks/mutations.hooks.ts

This package having the hook pattern can help you scale many apps well because you can add as many side effects in a controlled manner as you want.

Different bundles

Importing your methods directly from your common folder may ship to the client and backend code, which is not desirable for most developers. A great solution is the Method.setResolver function that can be seen in api/tasks/tasks.methods.js

// tasks/mutations.ts
const insert = createMethod('tasks.insert', TaskValidator).expect<void>()
const remove = createMethod('tasks.remove', IDValidator).expect<void>()
const setChecked = createMethod('tasks.setChecked', IDValidator).expect<void>()
// ....
Tasks.insert.setResolver(insertTask);
Tasks.remove.setResolver(removeTask);
Tasks.setChecked.setResolver(toggleTaskDone);

This file only is shipped on the server, while the UI and common folder are shipped to the client. As there is no typescript on the client, there are no traces left of the backend code, and you have this awesome Developer Experience.

Subscriptions

Another great question that people may have is about subscriptions. In almost the same way as methods, we also have subscriptions. There is an example in examples/simpletasks/common/tasks/subscriptions.ts where we create the publication as follows below:

const tasksByUser = createPublication('tasks.byUser', z.any()).expect<Task>()

You can use the same way that you are used to making subscriptions:

import { TasksCollection } from './tasks.collection';
import { tasksByUser } from "../../common/tasks/subscriptions";
tasksByUser.setResolver(function () {
return TasksCollection.find({ userId: this.userId });
})

For the front end using React, we have this model:

const isLoading = useSubscribe(tasksByUser.config.name);

The subscriptionName.config.nameis to get the subscription name, as the useSubscribe uses the subscription name to bind the values

Curious to learn more?

Having seen what this package is capable of, are you interested in trying it out? If yes, you can check the repo to start making magic happen.

--

--