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
typescriptimportrpc from "@json-rpc-anywhere/core";// An RPC service definition with a single method.interfaceAddService {add (params : {x : number,y : number}): number;}async functionexample () {constimplementation =rpc .createImplementation <AddService >({add (params ) {returnPromise .resolve (params .x +params .y );},});// Transports connect different participants.// In this example, both client and server are in the same process.consttransport =rpc .createInMemoryTransport ({implementation });constclient =rpc .createStub ({transport });// Call `.value()` on a pending request to wait for the result to come back.constresult = awaitclient .request ("add", {x : 1,y : 2}).value ();//assert.ok(result === 3);// End-to-end type safety - this is a type error!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; }'.client .request ("add", {x : 1,: 2}); wrong }
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:
- The shape of the "envelope" which wraps the request and response data,
- How protocol and application errors are represented, and
- How to match a response to a specific request by using
idproperties. (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:
- Stable
- Minimal
- Robust
- Type-safe
- Extensible
It aims to support the following runtime environments:
- Nodejs
- Deno
- Bun
- Modern* web browsers
*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:
- tRPC does not strictly follow the JSON-RPC spec for transport. This means it cannot reliably be used with other clients/servers. json-rpc-anywhere intends to follow the spec, and enable contract-first workflows to interoperate with other languages and libraries.
- tRPC has 11 major versions and counting. It has a large surface area and a lot of features. json-rpc-anywhere is aiming to have one major version.
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:
- typed-rpc does not implement JSON-RPC batches.
- typed-rpc does not build in reliability of RPC transport channels.
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.
-
WeakMap MDN · caniuse
WeakMap is used for extension data storage and configuration. -
AbortController MDN · caniuse
AbortController and AbortSignal are used for request cancellation. -
Array methods:
- Array.prototype.flatMap MDN
Getting started
Installation
bashnpm 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
typescriptimport {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
typescriptimport {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 backgroundtsx server.ts &# Run the client, which will print 7 and exittsx client.ts# Stop the serverkill %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.
bashnpx @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
typescriptimport {implement, server} from "@json-rpc-anywhere/node";import {AdditionService} from "./addition.validation";const service = implement({// TODO: validation APIadd: 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
@json-rpc-anywhere/coreis runtime-agnostic and implements the main feature of JSON-RPC: matching requests with responses via theirids.@json-rpc-anywhere/cliis an executable that provides code generation for interoperability with other languages.