Skip to content

Grubba27/meteor-rpc

Repository files navigation

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 meteor-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:

// server/main.ts
import { createModule } from "meteor-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() // server has no namespace
  .addMethod("bar", z.string(), (arg) => "bar" as const)
  .addSubmodule(Chat)
  .build();

export type Server = typeof server;

// client.ts
import { createClient } from "meteor-rpc";

const api = createClient<Server>();
const bar: "bar" = await api.bar("some string");
//   ?^ 'bar'
const newChatId = await api.chat.createChat(); // with intellisense

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.

// server/main.ts
import { createModule } from "meteor-rpc";
import { z } from "zod";

const server = createModule();

server.addMethod("foo", z.string(), (arg) => "foo" as const);

server.build();

// is the same as

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.

// server/main.ts
import { createModule } from "meteor-rpc";
import { ChatCollection } from "/imports/api/chat";
import { z } from "zod";

const server = createModule();

server.addPublication("chatRooms", z.void(), () => {
  return ChatCollection.find();
});

server.build();

// is the same as
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

// module/chat.ts
import { ChatCollection } from "/imports/api/chat";
import { createModule } from "meteor-rpc";

export const chatModule = createModule("chat")
  .addMethod("createChat", z.void(), async () => {
    return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] });
  })
  .buildSubmodule(); // <-- this is important so that this module can be added as a submodule

// server/main.ts
import { createModule } from "meteor-rpc";
import { chatModule } from "./module/chat";

const server = createModule()
  .addMethod("bar", z.string(), (arg) => "bar" as const)
  .addSubmodule(chatModule)
  .build();

server.chat; // <-- this is the namespace for the chat module

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:

// module/chat.ts
import { ChatCollection } from "/imports/api/chat";
import { createModule } from "meteor-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();

// server/main.ts
import { createModule } from "meteor-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

// server/main.ts
import { createModule } from "meteor-rpc";

const server = createModule()
  .addMethod("bar", z.string(), (arg) => "bar" as const)
  .build();

export type Server = typeof server;

// client.ts
import type { Server } from "/imports/api/server"; // you must import the type
const app = createClient<Server>();

await app.bar("str"); // it will return "bar"

method.useMutation

It uses the useMutation from react-query to create a mutation that will call the method

// server/main.ts
import { createModule } from "meteor-rpc";

const server = createModule()
  .addMethod("bar", z.string(), (arg) => "bar" as const)
  .build();

export type Server = typeof server;

// client.ts
import type { Server } from "/imports/api/server"; // you must import the type
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

// server/main.ts
import { createModule } from "meteor-rpc";

const server = createModule()
  .addMethod("bar", z.string(), (arg) => "bar" as const)
  .build();

export type Server = typeof server;

// client.ts
import type { Server } from "/imports/api/server"; // you must import the type
const app = createClient<Server>();

export const Component = () => {
  const { data } = app.bar.useQuery("str"); // will trigger suspense

  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

// server/main.ts
import { createModule } from "meteor-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;

// client.ts
import type { Server } from "/imports/api/server"; // you must import the type
const app = createClient<Server>();

export const Component = () => {
  const { data: rooms, collection: chatCollection } =
    api.chatRooms.usePublication(); // it will trigger suspense and rooms is reactive

  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.ts
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();

About

Meteor Methods Evolved

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •