The Docs.

Prim+RPC is in prerelease mode and may be unstable until official release.

Basic Setup

Prim+RPC bridges two separated environments: the server, where your functions are located, and the client, where those functions are called. The server may be a literal HTTP server or could be some other environment like a Web Worker. Prim+RPC is able to work in all of these environments because of its plugin system.

More importantly, it makes communication between server and client easy. Functions are functions, on the server and client. You call the function on the client like you would on the server: Prim+RPC handles the dance between the two.

Prim+RPC communicates between the server and client over two types of plugins, each with a version for the server and client. Method plugins handle calls to methods. Callback plugins optionally handle callbacks on those methods. Plugins used on the server are referred to as handlers to easily differentiate server/client.

Today, we’ll set up a method handler to receive method calls over the Fetch API on the server. On the client, we’ll set up a method plugin to to call those methods using the Fetch API.

Starter Project

We’ll use a starter project to get up and running quickly. You may clone the starter project with the following command, or download directly.

npx giget@latest gh:doseofted/prim-rpc-examples/starter prim-rpc-examples/starter
cd prim-rpc-examples/starter

You may also open the starter project in StackBlitz:

Open in StackBlitz

See the project’s README.md file to learn how the project is configured. For this guide we’ll use the pnpm package manager but you may choose to use any runtime or package manager that you like.

Run pnpm install to install needed dependencies and pnpm dev to start the project. A browser window will open that says “Not implemented”.

Let’s implement it!

Installation

Prim+RPC is composed of multiple packages, in order: the core RPC functionality, plugins for it, and optional tooling. You may find install commands for your package manager or runtime below.

If you’re using the starter project, you can skip this step: Prim+RPC is already installed in both server and client parts of the project.

These packages should be installed on both server and client portions of your project.

npm add @doseofted/prim-rpc @doseofted/prim-rpc-plugins @doseofted/prim-rpc-tooling

You may also use Prim+RPC with Deno or from a CDN with the following import maps:

{
	"imports": {
		"@doseofted/prim-rpc": "npm:@doseofted/prim-rpc@latest",
		"@doseofted/prim-rpc-plugins/": "npm:@doseofted/prim-rpc-plugins@latest/",
		"@doseofted/prim-rpc-tooling/": "npm:@doseofted/prim-rpc-tooling@latest/"
	}
}

Server Setup

First, we need some function to call on the server. Let’s start with something simple:

export function sayHello(x = "Backend", y = "Frontend") {
	return `${x}, meet ${y}.`
}
sayHello.rpc = true

The rpc property here signals to Prim+RPC that it can safely call this function. Functions are objects in JavaScript so if this looks unfamiliar, this is perfectly valid code.

Now let’s set up the Prim+RPC server and pass our function to it (if using starter project, erase contents with the following):

import { createPrimServer } from "@doseofted/prim-rpc"
import * as module from "./module"
 
const prim = createPrimServer({ module })
 
export type Module = typeof module
 

Note that we exported the module’s type. This is optional but can make for an easier development experience when shared with the client (either locally or by uploading types only to a registry).

The Prim+RPC server is now configured however it doesn’t do much alone. Prim+RPC depends on plugins that transform requests into a form that it understands.

Let’s expose our function over HTTP using the Fetch API:

import { createPrimServer } from "@doseofted/prim-rpc"
import { primFetch } from "@doseofted/prim-rpc-plugins/server-fetch"
import * as module from "./module"
 
const prim = createPrimServer({ module })
const fetch = primFetch({ prim })
 
export type Module = typeof module
 

In Deno and Bun runtimes, this will work out of the box by utilizing the created fetch object. In Node, we’ll need to install an adapter called @whatwg-node/server (if using the starter project, this is already installed):

pnpm add @whatwg-node/server

Now we can set up the fetch handler in Node, just like other runtimes:

import { createPrimServer } from "@doseofted/prim-rpc"
import { primFetch } from "@doseofted/prim-rpc-plugins/server-fetch"
import * as module from "./module"
import { createServer } from "node:http"
import { createServerAdapter } from "@whatwg-node/server"
 
const prim = createPrimServer({ module })
const fetch = primFetch({ prim })
 
const fetchAdapter = createServerAdapter(fetch)
const server = createServer(fetchAdapter).listen(3001)
console.log("Prim+RPC is available at http://localhost:3001/prim")
 
export type Module = typeof module
 

We can now make requests to the server! We haven’t set up the client yet but we can still try it out from the command line:

curl \
	--request POST \
	--header "Content-Type: application/json" \
	--data '{ "method": "sayHello", "args": ["Backend", "Terminal"] }' \
	"http://localhost:3001/prim"
# {"result":"Backend, meet Terminal."}

In fact, we could even call this function over a GET request. We just need to modify out .rpc property to use a special keyword: "idempotent"

export function sayHello(x = "Backend", y = "Frontend") {
	return `${x}, meet ${y}.`
}
sayHello.rpc = "idempotent"

This will signal to Prim+RPC that it’s safe to call from a URL. Now we can make a GET request to this function or paste the link into a web browser:

curl "http://localhost:3001/prim/sayHello?0=Backend&1=Terminal"
# {"result":"Backend, meet Terminal."}

Of course, this is not the primary way we’ll be using Prim+RPC: we’ll be setting up the client next. This means that we’ll need to implement CORS so that the client is allowed to call the server. We can configure this in the Fetch handler’s options:

import { createPrimServer } from "@doseofted/prim-rpc"
import { primFetch } from "@doseofted/prim-rpc-plugins/server-fetch"
import * as module from "./module"
import { createServer } from "node:http"
import { createServerAdapter } from "@whatwg-node/server"
 
const prim = createPrimServer({ module })
function postprocess(res: Response) {
	res.headers.set("access-control-allow-origin", "http://localhost:3000")
	res.headers.set("access-control-allow-headers", "content-type")
}
const fetch = primFetch({ prim, postprocess })
 
const fetchAdapter = createServerAdapter(fetch)
const server = createServer(fetchAdapter).listen(3001)
console.log("Prim+RPC is available at http://localhost:3001/prim")
 
export type Module = typeof module
 

If you are running the starter project in a hosted environment like Stackblitz, you may need to adjust CORS rules to use https://localhost:3000 (using HTTPS instead of HTTP).

This will allow our client, which will run on http://localhost:3000, to call the server. Our server is fully configured and we can now set up the client!

Client Setup

Now that the server can receive function calls, we’ll need a client that knows how to interact with the server. Let’s set that up now. We’ll start off with the following:

import { createPrimClient } from "@doseofted/prim-rpc"
import type { Module } from "../server"
 
const prim = createPrimClient<Module>()

We have set up the client and passed the type parameter from the server to get type definitions. Like the server, the client doesn’t do anything by itself. It needs compatible plugins to work with the server.

Let’s set up our method plugin next. This plugin will utilize the browser’s Fetch API under the hood:

import { createPrimClient } from "@doseofted/prim-rpc"
import { createMethodPlugin } from "@doseofted/prim-rpc-plugins/browser-fetch"
import type { Module } from "../server"
 
export const client = createPrimClient<Module>({
	endpoint: "http://localhost:3001/prim",
	methodPlugin: createMethodPlugin(),
})
export default client

We have passed the server’s address, from the last step, to the client and told the Prim+RPC client to send an HTTP request containing the function call to the server.

Prim+RPC is now set up and ready to use! Try making a function call:

import { client } from "./prim"
 
const greeting = await client.sayHello()
console.log(greeting) // "Frontend, meet Backend."

Now we can see our message logged to the developer’s console. Let’s replace the “Not implemented” message on the page with our greeting:

import { client } from "./prim"
 
const greeting = await client.sayHello()
console.log(greeting) // "Frontend, meet Backend."
 
const app = document.getElementById("app")
if (app) app.innerText = greeting

Now we can easily add more functions on the server and simply call those functions from the client.

You’re all set up! Find next steps below to learn more about what Prim+RPC can do.

Next Steps

While we could stop right here, Prim+RPC can do much more. See the advanced guide to continue on and send and receive Files, work with callbacks, and work with more types.

Report an Issue