Menu

JSON-RPC Anywhere

Build end-to-end typed RPC services. This package contains implementations for all JS runtimes, from workers, to edge, to backend. Interop with any language via JSON Schema.

Example

typescript
import rpc from "@json-rpc-anywhere/core";
 
// An RPC service definition with a single method.
interface AddService {
add(params: {x: number, y: number}): number;
}
 
async function example() {
const implementation = rpc.createImplementation<AddService>({
add(params) {
return Promise.resolve(params.x + params.y);
},
});
 
// Transports connect different participants.
// In this example, both client and server are in the same process.
const transport = rpc.createInMemoryTransport({implementation});
 
const client = rpc.createStub({transport});
 
// Call `.value()` on a pending request to wait for the result to come back.
const result = await client.request("add", {x: 1, y: 2}).value();
 
//assert.ok(result === 3);
 
// End-to-end type safety - this is a type error!
client.request("add", {x: 1, wrong: 2});
Object literal may only specify known properties, and 'wrong' does not exist in type '{ x: number; y: number; }'.2353Object literal may only specify known properties, and 'wrong' does not exist in type '{ x: number; y: number; }'.
}

About

json-rpc-anywhere is a set of libraries that allow you to build strongly typed services on top of the JSON-RPC protocol which can run in any JS environment. It can work code-first like tRPC, or in a schema-first fashion like gRPC to enable stronger interop with clients in other languages.

It was born from the desire for a simple, stable and robust core to build RPC services on, and the need to use RPC-like interactions in many parts of the stack. This might be between a browser and a server, between two Web Workers, on an edge platform, or via a WebRRTC channel.

What is RPC?

Remote Prodecure Call (RPC) is an API design paradigm for interprocess communication (IPC). It is one of the simplest IPC approaches. Calling a remote procedure is analogous to calling a function which accepts basic data types, executes in some other process, and returns basic data types.

For more detail about the RPC paradigm, see the following articles:

Request–response protocols date to early distributed computing in the late 1960s, theoretical proposals of remote procedure calls as the model of network operations date to the 1970s, and practical implementations date to the early 1980s.

"Remote procedure call" https://en.wikipedia.org/wiki/Remote_procedure_call

RPC is the earliest, simplest form of API interaction. It is about executing a block of code on another server, and when implemented in HTTP or AMQP it can become a Web API. There is a method and some arguments, and that is pretty much it. Think of it like calling a function, taking a method name and arguments.

"Understanding RPC, REST and GraphQL" https://apisyouwonthate.com/blog/understanding-rpc-rest-and-graphql/

Remote Procedure Call (RPC) is typically used to call remote functions on a server that require an action result. You can use it when you require complex calculations or want to trigger a remote procedure on the server, with the process hidden from the client.

"RPC vs REST - Difference Between API Architectures" https://aws.amazon.com/compare/the-difference-between-rpc-and-rest/

To really separate the parts [of an application] from each other, you probably want to run the parts in several processes (with different users/rights). But this means that the processes have to communicate with each other in some way. This is called inter-process communication (IPC), and one way of doing this is by using remote-procedure calls (RPC).

"simple is better - JSON-RPC" https://www.simple-is-better.org/rpc/

Some examples of RPC systems are:

What is JSON-RPC?

JSON-RPC is a specification for writing RPC services which communicate by passing JSON documents back and forth.

Read the spec: https://www.jsonrpc.org/specification

It can use any network or IPC transport mechanism, as long as the request and response objects are JSON. For example, you could use HTTP requests, WebSockets, Node Streams, or any other mechanism in order to exchange JSON-RPC messages.

JSON-RPC specifies three important things:

  1. The shape of the "envelope" which wraps the request and response data,
  2. How protocol and application errors are represented, and
  3. How to match a response to a specific request by using id properties. (This is important for transports other than HTTP, which may not already have "request-response" semantics built in.)

It's a very small specification, what you might consider the "minimum viable specification" for communicating using JSON.

Users of JSON-RPC include:

For more JSON-RPC ecosystem projects, check out https://github.com/shanejonas/awesome-json-rpc

Project goals

json-rpc-anywhere aims to be:

It aims to support the following runtime environments:

*See compatibility section for specifics.

Comparison with other libraries

tRPC

tRPC is a fully-featured framework for quickly developing tightly coupled services (e.g. a web frontend and backend). It has end-to-end typesafety, runtime validation, and a large and healthy ecosystem. It is compatible with major backend frameworks, runtimes, and modern browsers.

Differences between tRPC and json-rpc-anywhere:

fgnass/typed-rpc

https://github.com/fgnass/typed-rpc

A simple and lightweight RPC implementation in the spirit of this project.

Differences between typed-rpc and json-rpc-anywhere:

lorefnon/ts-json-rpc

https://github.com/lorefnon/ts-json-rpc

A fork of fgnass/typed-rpc with the addition of Zod for runtime data validation.

ptol/typed-json-rpc

https://github.com/ptol/typed-json-rpc

Compatibility

Web platform

json-rpc-anywhere uses the following web platform features. If your runtime environment does not yet support them, you may need to ensure appropriate polyfills are installed.

When developing new features, we consider any new usage of web API platform capabilities seriously.

Getting started

Installation

bash
npm i --save @json-rpc-anywhere/node

Your first RPC server

Let's deploy a basic RPC service that adds numbers. For simplicity, we'll use a Node backend server and a Node script as the client.

Let's first define a contract for our service. This is known as a contract-first workflow, and is common when using other RPC tooling like gRPC or Capn' Proto.

addition.service.ts

typescript
/**
* A service definition that provides a single method to add two numbers;
*/
export interface AdditionService {
add(params: {x: number, y: number}): number;
}

By convention, service definitions are named your_service_name.service.ts.

Note that our add method takes a single argument, the parameters of the RPC. The params type can be an object or an array.

Next, let's implement this on the backend.

server.ts

typescript
import {implement, server} from "@json-rpc-anywhere/node";
import type {AdditionService} from "./addition.service";
const service = implement<AdditionService>({
add: async (params) => params.x + params.y,
});
const running = server(service).listen({port: 3000});

TODO: explain this.

server is a helper to make this example shorter, but there are also ways to integrate an RPC service endpoint with an existing Node server, or with backend frameworks like Express or Fastify.

Now we have a server, let's implement a client:

client.ts

typescript
import {stub, fetchTransport} from "@json-rpc-anywhere/node";
import type {AdditionService} from "./addition.service";
const transport = fetchTransport("localhost:3000");
const addition = stub<AdditionService>({transport});
(async () => {
const result = await addition.add({x: 3, y: 4});
console.log(result);
})();

Note we first create a transport using fetchTransport which uses modern Node's built-in fetch global. We can then create a typed stub which sends requests using that transport.

Let's run the server first, then use the client to connect to it:

bash
# Run the server in the background
tsx server.ts &
# Run the client, which will print 7 and exit
tsx client.ts
# Stop the server
kill %1

Generating validation code

Our service currently has no runtime validation. This might be OK if we control both the server and client in a monorepo and can be sure that both ends are updated together. However if we accept data from browsers or other untrusted clients, we may want to use runtime validation to ensure all payloads are what we expect them to be.

One approach to doing this is to generate validation code from our TypeScript schema definition file.

bash
npx @json-rpc-anywhere/cli generate validators-from-schema ./addition.schema.ts

The CLI will create a file called addition.validation.ts next to our addition.schema.ts.

Let's modify our server:

server.ts

typescript
import {implement, server} from "@json-rpc-anywhere/node";
import {AdditionService} from "./addition.validation";
const service = implement({
// TODO: validation API
add: async (params) => params.x + params.y,
});
const running = server(service).listen({port: 3000});

Usage

With HTTP

With web workers

Contributing

Governance

json-rpc-anywhere is currently in the BDFN phase: crabmusket is the Benevolent Dictator For Now.

Changes are proposed via an RFC process conducted on Github Discussions. When an RFC is accepted, it will be canonised into the codebase in the rfcs directory.

Overview of packages