The Docs.

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


Prim+RPC is a low-level library and keeps a narrow scope: handling RPC and providing the tools to easily integrate with other libraries. There are a few things to keep in mind when using Prim+RPC to keep your functions secure.

Validate Arguments

By default, Prim+PRC validates that messages passed to it are RPC but it does not validate what’s in that RPC. Validation is especially important if your RPC is sent over a network like an HTTP server. Prim+RPC provides several options to easily set up validation.

The first option is the most flexible: choose any validation library and use it directly in your function to overwrite arguments. Here is an example using ArkType:

import { type } from "arktype"
export function sayHello(x = "Backend", y = "Frontend") {
	;[x, y] = type(["string", "string"]).assert([x, y])
	return `${x}, meet ${y}.`
sayHello.rpc = true

The second option is to wrap your function in another library that validates arguments for you. This example using Zod may look familiar if you are coming from tRPC:

import { z } from "zod"
export const sayHello = z
	.args(z.string().optional(), z.string().optional())
	.implement(function (x = "Backend", y = "Frontend") {
		return `${x}, meet ${y}.`
Object.defineProperty(sayHello, "rpc", { value: true })

The third option is to have Prim+RPC enforce validation through preCall and/or postCall hooks, instead of validating within the function. First, we must set up a preCall hook. This example will use Valibot:

import { createPrimServer } from "@doseofted/prim-rpc"
import * as module from "./module"
import { parse } from "valibot"
const prim = createPrimServer({
	preCall(args, func) {
		if ("params" in func) return parse(func.params, args)
		throw new Error("Function validation is required")

Notice that we parse arguments according to the params property defined on the given function. Let’s add that to our example:

import { tuple, string, fallback, type Input } from "valibot"
export function sayHello(x?: SayHelloParams[0], y?: SayHelloParams[1]) {
	return `${x}, meet ${y}.`
sayHello.rpc = true
sayHello.params = tuple([fallback(string(), "Backend"), fallback(string(), "Frontend")])
type SayHelloParams = Input<typeof sayHello.params>

Now we can be certain that not only will this function’s arguments be validated but functions added in the future will require validation through the params property.

Limit RPC Access

By default, when Prim+RPC is used over a network, it only accepts requests over a POST request. However, you can still make an RPC with a GET request using the special keyword "idempotent".

However, you should take caution when doing so. We can demonstrate with an example. This is an idempotent function:

export function add(x: number, y: number) {
	return x + y
add.rpc = "idempotent"

No matter how many times that we call this function, we will always get the same result. This can be exposed over a URL (with a GET request) because simply visiting that URL won’t change the state of the server. Here’s a bad example:

const x = 0
export function add(y: number) {
	return x + y
// NOTE: the property below should be set to `true` instead
add.rpc = "idempotent"

If we expose this function over a URL, then visiting that URL will change the state of the server which may not be intended.

It’s also important to note that GET requests are often logged so functions that use the "idempotent" keyword should never have sensitive arguments passed to them. All functions where .rpc = true will be POST requests and cannot be accessed with a GET request. All functions where .rpc = "idempotent" may be accessed from either a GET or POST request.

Selectively Import

Prim+RPC is very selective about what gets exposed from the server. Functions (and functions only) must be passed to the server, must explicitly be marked as RPC, must not be defined on another function, and cannot access built-in functions.

While this provides a lot of protection against accidental imports, you should still be cautious about what is passed to the Prim+PRC server. If Prim+RPC doesn’t need access to a function or variable then don’t pass it to the server.

Secure the Transport

Prim+RPC does not transport your RPC messages. It creates RPC messages and passes them to your server and client frameworks. Securing the transport means something different for every environment and falls outside of Prim+RPC’s scope. When sending RPC over a network for instance, you will want to use a security certificate and implement relevant security headers. These may be provided by or be options of your server framework.

Safely Serialize

Prim+RPC uses the default JSON “stringify” method to serialize RPC and the destr library to deserialize RPC by default. You may override this behavior by providing your own alternative JSON handler. However you should be aware of any possible security implications of doing so.

Particularly, consider whether the library has protection for prototype pollution or has any unpatched security vulnerabilities. If your custom JSON handler allows additional types or supports circular references, consider how this may impact your code.

Report an Issue