Meteor-RPC
What is this package?
Inspired on zodern:relay and on tRPC
This package provides functions for building E2E type-safe RPCs.
How to download it?
meteor npm i grubba-rpc @tanstack/react-query zod
install react query into your project, following their quick start guide
How to use it?
Firstly, you need to create a module, then you can add methods, publications, and subscriptions to it.
Then you need to build the module and use it in the client as a type.
createModule
subModule
without a namespace: createModule()
is used to create the main
server module, the one that will be exported to be used in the client.
subModule
with a namespace: createModule("namespace")
is used to create a submodule that will be added to the main module.
Remember to use build
at the end of module creation to ensure that the module is going to be created.
for example:
import { createModule } from "grubba-rpc";
import { ChatCollection } from "/imports/api/chat";
import { z } from "zod";
const Chat = createModule("chat")
.addMethod("createChat", z.void(), async () => {
return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] });
})
.buildSubmodule();
const server = createModule()
.addMethod("bar", z.string(), (arg) => "bar" as const)
.addSubmodule(Chat)
.build();
export type Server = typeof server;
import { createClient } from "grubba-rpc";
const api = createClient<Server>();
const bar: "bar" = await api.bar("some string");
const newChatId = await api.chat.createChat();
module.addMethod
addMethod(name: string, schema: ZodSchema, handler: (args: ZodTypeInput<ZodSchema>) => T, config?: Config<ZodTypeInput<ZodSchema>, T>)
This is the equivalent of Meteor.methods
but with types and runtime validation.
import { createModule } from "grubba-rpc";
import { z } from "zod";
const server = createModule();
server.addMethod("foo", z.string(), (arg) => "foo" as const);
server.build();
import { Meteor } from "meteor/meteor";
import { z } from "zod";
Meteor.methods({
foo(arg: string) {
z.string().parse(arg);
return "foo";
},
});
module.addPublication
addPublication(name: string, schema: ZodSchema, handler: (args: ZodTypeInput<ZodSchema>) => Cursor<any, any>)
This is the equivalent of Meteor.publish
but with types and runtime validation.
import { createModule } from "grubba-rpc";
import { ChatCollection } from "/imports/api/chat";
import { z } from "zod";
const server = createModule();
server.addPublication("chatRooms", z.void(), () => {
return ChatCollection.find();
});
server.build();
import { Meteor } from "meteor/meteor";
import { ChatCollection } from "/imports/api/chat";
Meteor.publish("chatRooms", function () {
return ChatCollection.find();
});
module.addSubmodule
This is used to add a submodule to the main module, adding namespaces for your methods and publications and also making it easier to organize your code.
Remember to use submodule.buildSubmodule
when creating a submodule
import { ChatCollection } from "/imports/api/chat";
import { createModule } from "grubba-rpc";
export const chatModule = createModule("chat")
.addMethod("createChat", z.void(), async () => {
return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] });
})
.buildSubmodule();
import { createModule } from "grubba-rpc";
import { chatModule } from "./module/chat";
const server = createModule()
.addMethod("bar", z.string(), (arg) => "bar" as const)
.addSubmodule(chatModule)
.build();
server.chat;
module.addMiddlewares
addMiddlewares(middlewares: Middleware[])
Type Middleware = (raw: unknown, parsed: unknown) => void;
This is used to add middlewares to the module, it can be used to add side effects logic to the methods and publications, ideal for logging, rate limiting, etc.
The middleware ordering is last in first out. Check the example below:
import { ChatCollection } from "/imports/api/chat";
import { createModule } from "grubba-rpc";
export const chatModule = createModule("chat")
.addMiddlewares([
(raw, parsed) => {
console.log("run first");
},
])
.addMethod("createChat", z.void(), async () => {
return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] });
})
.buildSubmodule();
import { createModule } from "grubba-rpc";
import { chatModule } from "./module/chat";
const server = createModule()
.addMiddlewares([
(raw, parsed) => {
console.log("run second");
},
])
.addMethod("bar", z.string(), (arg) => "bar" as const)
.addSubmodule(chatModule)
.build();
React focused API
Using in the client
When using in the client you have to use the createModule
and build
methods to create a module that will be used in the client
and be sure that you are exporting the type of the module
You should only create one client in your application
You can have something like api.ts
that will export the client and the type of the client
import { createModule } from "grubba-rpc";
const server = createModule()
.addMethod("bar", z.string(), (arg) => "bar" as const)
.build();
export type Server = typeof server;
import type { Server } from "/imports/api/server";
const app = createClient<Server>();
await app.bar("str");
method.useMutation
It uses the useMutation
from react-query to create a mutation that will call the method
import { createModule } from "grubba-rpc";
const server = createModule()
.addMethod("bar", z.string(), (arg) => "bar" as const)
.build();
export type Server = typeof server;
import type { Server } from "/imports/api/server";
const app = createClient<Server>();
export const Component = () => {
const { mutate, isLoading, isError, error, data } = app.bar.useMutation();
return (
<button
onClick={() => {
mutation.mutate("str"); // it has intellisense
}}
>
Click me
</button>
);
};
method.useQuery
It uses the useQuery
from react-query to create a query that will call the method, it uses suspense
to handle loading states
import { createModule } from "grubba-rpc";
const server = createModule()
.addMethod("bar", z.string(), (arg) => "bar" as const)
.build();
export type Server = typeof server;
import type { Server } from "/imports/api/server";
const app = createClient<Server>();
export const Component = () => {
const { data } = app.bar.useQuery("str");
return <div>{data}</div>;
};
publication.useSubscription
Subscriptions on the client have useSubscription
method that can be used as a hook to subscribe to a publication. It uses suspense
to handle loading states
import { createModule } from "grubba-rpc";
import { ChatCollection } from "/imports/api/chat";
import { z } from "zod";
const server = createModule()
.addPublication("chatRooms", z.void(), () => {
return ChatCollection.find();
})
.build();
export type Server = typeof server;
import type { Server } from "/imports/api/server";
const app = createClient<Server>();
export const Component = () => {
const { data: rooms, collection: chatCollection } =
api.chatRooms.usePublication();
return <div>{data}</div>;
};
Examples
Currently we have this chat-app that uses this package to create a chat app
it includes: methods, publications, and subscriptions
Advanced usage
you can take advantage of the hooks to add custom logic to your methods, checking the raw and parsed data, and the result of the method,
you can add more complex validations.
server.addMethod("name", z.any(), () => "str", {
hooks: {
onBeforeResolve: [
(raw, parsed) => {
console.log("before resolve", raw, parsed);
},
],
onAfterResolve: [
(raw, parsed, result) => {
console.log("after resolve", raw, parsed, result);
},
],
onErrorResolve: [
(err, raw, parsed) => {
console.log("error resolve", err, raw, parsed);
},
],
},
});
or
server.name.addBeforeResolveHook((raw, parsed) => {
console.log("before resolve", raw, parsed);
});
server.name.addAfterResolveHook((raw, parsed, result) => {
console.log("after resolve", raw, parsed, result);
});
server.name.addErrorResolveHook((err, raw, parsed) => {
console.log("error resolve", err, raw, parsed);
});
server = server.build();