# json-rpc-2.0 Let your client and server talk over function calls under [JSON-RPC 2.0 spec](https://www.jsonrpc.org/specification). - Protocol agnostic - Use over HTTP, WebSocket, TCP, UDP, inter-process, whatever else - Easy migration from HTTP to WebSocket, for example - No external dependencies - Keep your package small - Stay away from dependency hell - Works in both browser and Node.js - First-class TypeScript support - Written in TypeScript - [Strongly typed client and server calls](#typed-client-and-server) ## Install `npm install --save json-rpc-2.0` ## Example The example uses HTTP for communication protocol, but it can be anything. ### Server ```javascript const express = require("express"); const bodyParser = require("body-parser"); const { JSONRPCServer } = require("json-rpc-2.0"); const server = new JSONRPCServer(); // First parameter is a method name. // Second parameter is a method itself. // A method takes JSON-RPC params and returns a result. // It can also return a promise of the result. server.addMethod("echo", ({ text }) => text); server.addMethod("log", ({ message }) => console.log(message)); const app = express(); app.use(bodyParser.json()); app.post("/json-rpc", (req, res) => { const jsonRPCRequest = req.body; // server.receive takes a JSON-RPC request and returns a promise of a JSON-RPC response. // It can also receive an array of requests, in which case it may return an array of responses. // Alternatively, you can use server.receiveJSON, which takes JSON string as is (in this case req.body). server.receive(jsonRPCRequest).then((jsonRPCResponse) => { if (jsonRPCResponse) { res.json(jsonRPCResponse); } else { // If response is absent, it was a JSON-RPC notification method. // Respond with no content status (204). res.sendStatus(204); } }); }); app.listen(80); ``` #### With authentication To hook authentication into the API, inject custom params: ```javascript const server = new JSONRPCServer(); // The method can also take a custom parameter as the second parameter. // Use this to inject whatever information that method needs outside the regular JSON-RPC request. server.addMethod("echo", ({ text }, { userID }) => `${userID} said ${text}`); app.post("/json-rpc", (req, res) => { const jsonRPCRequest = req.body; const userID = getUserID(req); // server.receive takes an optional second parameter. // The parameter will be injected to the JSON-RPC method as the second parameter. server.receive(jsonRPCRequest, { userID }).then((jsonRPCResponse) => { if (jsonRPCResponse) { res.json(jsonRPCResponse); } else { res.sendStatus(204); } }); }); const getUserID = (req) => { // Do whatever to get user ID out of the request }; ``` #### Middleware Use middleware to intercept request and response: ```javascript const server = new JSONRPCServer(); // next will call the next middleware const logMiddleware = (next, request, serverParams) => { console.log(`Received ${JSON.stringify(request)}`); return next(request, serverParams).then((response) => { console.log(`Responding ${JSON.stringify(response)}`); return response; }); }; const exceptionMiddleware = async (next, request, serverParams) => { try { return await next(request, serverParams); } catch (error) { if (error.code) { return createJSONRPCErrorResponse(request.id, error.code, error.message); } else { throw error; } } }; // Middleware will be called in the same order they are applied server.applyMiddleware(logMiddleware, exceptionMiddleware); ``` #### Constructor Options Optionally, you can pass options to `JSONRPCServer` constructor: ```typescript new JSONRPCServer({ errorListener: (message: string, data: unknown): void => { // Listen to error here. By default, it will use console.warn to log errors. }, }); ``` ### Client ```javascript import { JSONRPCClient } from "json-rpc-2.0"; // JSONRPCClient needs to know how to send a JSON-RPC request. // Tell it by passing a function to its constructor. The function must take a JSON-RPC request and send it. const client = new JSONRPCClient((jsonRPCRequest) => fetch("http://localhost/json-rpc", { method: "POST", headers: { "content-type": "application/json", }, body: JSON.stringify(jsonRPCRequest), }).then((response) => { if (response.status === 200) { // Use client.receive when you received a JSON-RPC response. return response .json() .then((jsonRPCResponse) => client.receive(jsonRPCResponse)); } else if (jsonRPCRequest.id !== undefined) { return Promise.reject(new Error(response.statusText)); } }) ); // Use client.request to make a JSON-RPC request call. // The function returns a promise of the result. client .request("echo", { text: "Hello, World!" }) .then((result) => console.log(result)); // Use client.notify to make a JSON-RPC notification call. // By definition, JSON-RPC notification does not respond. client.notify("log", { message: "Hello, World!" }); ``` #### With authentication Just like `JSONRPCServer`, you can inject custom params to `JSONRPCClient` too: ```javascript const client = new JSONRPCClient( // It can also take a custom parameter as the second parameter. (jsonRPCRequest, { token }) => fetch("http://localhost/json-rpc", { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}`, // Use the passed token }, body: JSON.stringify(jsonRPCRequest), }).then((response) => { // ... }) ); // Pass the custom params as the third argument. client.request("echo", { text: "Hello, World!" }, { token: "foo's token" }); client.notify("log", { message: "Hello, World!" }, { token: "foo's token" }); ``` #### With timeout Sometimes you don't want to wait for the response indefinitely. You can use `timeout` to automatically fail the request after certain delay: ```typescript const client = new JSONRPCClient(/* ... */); client .timeout(10 * 1000) // Automatically fails if it didn't get a response within 10 sec .request("echo", { text: "Hello, World!" }); // Create a custom error response const createTimeoutJSONRPCErrorResponse = ( id: JSONRPCID ): JSONRPCErrorResponse => createJSONRPCErrorResponse(id, 123, "Custom error message"); client .timeout(10 * 1000, createTimeoutJSONRPCErrorResponse) .request("echo", { text: "Hello, World!" }); ``` ### Bi-directional JSON-RPC For bi-directional JSON-RPC, use `JSONRPCServerAndClient`. ```javascript const webSocket = new WebSocket("ws://localhost"); const serverAndClient = new JSONRPCServerAndClient( new JSONRPCServer(), new JSONRPCClient((request) => { try { webSocket.send(JSON.stringify(request)); return Promise.resolve(); } catch (error) { return Promise.reject(error); } }) ); webSocket.onmessage = (event) => { serverAndClient.receiveAndSend(JSON.parse(event.data.toString())); }; // On close, make sure to reject all the pending requests to prevent hanging. webSocket.onclose = (event) => { serverAndClient.rejectAllPendingRequests( `Connection is closed (${event.reason}).` ); }; serverAndClient.addMethod("echo", ({ text }) => text); serverAndClient .request("add", { x: 1, y: 2 }) .then((result) => console.log(`1 + 2 = ${result}`)); ``` #### Constructor Options Optionally, you can pass options to `JSONRPCServerAndClient` constructor: ```typescript new JSONRPCServerAndClient(server, client, { errorListener: (message: string, data: unknown): void => { // Listen to error here. By default, it will use console.warn to log errors. }, }); ``` ### Error handling To respond an error, reject with an `Error`. On the client side, the promise will be rejected with an `Error` object with the same message. ```javascript server.addMethod("fail", () => Promise.reject(new Error("This is an error message.")) ); client.request("fail").then( () => console.log("This does not get called"), (error) => console.error(error.message) // Outputs "This is an error message." ); ``` If you want to return a custom error response, use `JSONRPCErrorException`: ```typescript import { JSONRPCErrorException } from "json-rpc-2.0"; const server = new JSONRPCServer(); server.addMethod("throws", () => { const errorCode = 123; const errorData = { foo: "bar", }; throw new JSONRPCErrorException( "A human readable error message", errorCode, errorData ); }); ``` Alternatively, use [advanced APIs](#advanced-apis) or implement `mapErrorToJSONRPCErrorResponse`: ```typescript import { createJSONRPCErrorResponse, JSONRPCErrorResponse, JSONRPCID, JSONRPCServer, } from "json-rpc-2.0"; const server = new JSONRPCServer(); server.mapErrorToJSONRPCErrorResponse = ( id: JSONRPCID, error: any ): JSONRPCErrorResponse => { return createJSONRPCErrorResponse( id, error?.code || 0, error?.message || "An unexpected error occurred", // Optional 4th argument. It maps to error.data of the response. { foo: "bar" } ); }; ``` ### Advanced APIs Use the advanced APIs to handle raw JSON-RPC messages. #### Server ```typescript import { JSONRPC, JSONRPCResponse, JSONRPCServer } from "json-rpc-2.0"; const server = new JSONRPCServer(); // Advanced method takes a raw JSON-RPC request and returns a raw JSON-RPC response server.addMethodAdvanced( "doSomething", (jsonRPCRequest: JSONRPCRequest): PromiseLike => { if (isValid(jsonRPCRequest.params)) { return { jsonrpc: JSONRPC, id: jsonRPCRequest.id, result: "Params are valid", }; } else { return { jsonrpc: JSONRPC, id: jsonRPCRequest.id, error: { code: -100, message: "Params are invalid", data: jsonRPCRequest.params, }, }; } } ); // Remove the added method server.removeMethod("doSomething"); ``` #### Client ```typescript import { JSONRPC, JSONRPCClient, JSONRPCRequest, JSONRPCResponse, } from "json-rpc-2.0"; const send = () => { // ... }; let nextID: number = 0; const createID = () => nextID++; // To avoid conflict ID between basic and advanced method request, inject a custom ID factory function. const client = new JSONRPCClient(send, createID); const jsonRPCRequest: JSONRPCRequest = { jsonrpc: JSONRPC, id: createID(), method: "doSomething", params: { foo: "foo", bar: "bar", }, }; // Advanced method takes a raw JSON-RPC request and returns a raw JSON-RPC response // It can also send an array of requests, in which case it returns an array of responses. client .requestAdvanced(jsonRPCRequest) .then((jsonRPCResponse: JSONRPCResponse) => { if (jsonRPCResponse.error) { console.log( `Received an error with code ${jsonRPCResponse.error.code} and message ${jsonRPCResponse.error.message}` ); } else { doSomethingWithResult(jsonRPCResponse.result); } }); ``` ### Typed client and server To strongly type `request` and `addMethod` methods, use `TypedJSONRPCClient`, `TypedJSONRPCServer` and `TypedJSONRPCServerAndClient` interfaces. ```typescript import { JSONRPCClient, JSONRPCServer, JSONRPCServerAndClient, TypedJSONRPCClient, TypedJSONRPCServer, TypedJSONRPCServerAndClient, } from "json-rpc-2.0"; type Methods = { echo(params: { message: string }): string; sum(params: { x: number; y: number }): number; }; const server: TypedJSONRPCServer = new JSONRPCServer(/* ... */); const client: TypedJSONRPCClient = new JSONRPCClient(/* ... */); // Types are infered from the Methods type server.addMethod("echo", ({ message }) => message); server.addMethod("sum", ({ x, y }) => x + y); // These result in type error // server.addMethod("ech0", ({ message }) => message); // typo in method name // server.addMethod("echo", ({ messagE }) => messagE); // typo in param name // server.addMethod("echo", ({ message }) => 123); // return type must be string client .request("echo", { message: "hello" }) .then((result) => console.log(result)); client.request("sum", { x: 1, y: 2 }).then((result) => console.log(result)); // These result in type error // client.request("ech0", { message: "hello" }); // typo in method name // client.request("echo", { messagE: "hello" }); // typo in param name // client.request("echo", { message: 123 }); // message param must be string // client // .request("echo", { message: "hello" }) // .then((result: number) => console.log(result)); // return type must be string // The same rule applies to TypedJSONRPCServerAndClient type ServerAMethods = { echo(params: { message: string }): string; }; type ServerBMethods = { sum(params: { x: number; y: number }): number; }; const serverAndClientA: TypedJSONRPCServerAndClient< ServerAMethods, ServerBMethods > = new JSONRPCServerAndClient(/* ... */); const serverAndClientB: TypedJSONRPCServerAndClient< ServerBMethods, ServerAMethods > = new JSONRPCServerAndClient(/* ... */); serverAndClientA.addMethod("echo", ({ message }) => message); serverAndClientB.addMethod("sum", ({ x, y }) => x + y); serverAndClientA .request("sum", { x: 1, y: 2 }) .then((result) => console.log(result)); serverAndClientB .request("echo", { message: "hello" }) .then((result) => console.log(result)); ``` ## Build `npm run build` ## Test `npm test`