{JSON}.xRPC

Bringing back the pure similicity of modern decoupled multi-tier APIs, MicroServices, Mobile and Web Apps.

Coming soon

Features

Transport-Agnostic Protocol

The JSON of the request and the response can be carried over a normal HTTP request/response or a websocket or even A UDP packet.

See How

Fully typed

You will get validators for free with minimal coding. JSON Schema or JSON Type Definition(RFC 8927) can be used as IDL, which will give us request validation. If you are using a typed language or language with type hints you might generate those validations from the code.

See How
{*}

{any}.xRPC

{any}.xRPC or {*}.xRPC means that JSON can be replaced with any JSON like serializer like msgpack without the need to rewrite any of your APIs.

See How

Fast

A multi-transport client using both WebSocket was faster than gRPC.

See the benchmark

Keep it simple

A DIY browser client can be written from scratch in ~30 lines without any dependency

See How
🖇️

Server-to-Server communications

It can be used for server-to-server microservices communications

See How

Short description of this extension

  • "jsonrpc": "2.0" is optional and ignored. No Protocol versioning, no negotiation.
  • "method"` in HTTP-based Transport is optional, if missing it will be read from HTTP URL like this /rpc/<METHOD>. For developers this help using Swagger/OpenAPI/Postman. For OPs this provide visibility and routing. In WS method is manadatory.
  • "id" according to spec if null or missing run background, if present but empty string `""` auto create id and wait for it.
  • first level "params" must not start with "_" (any param starting with "_" should not reach the RPC).
  • params starting with "_" are optional server-internal params, namely: _ctx (per-session context) and _req (per request context) provide state.
  • "error" has the following properties beside "code" and "message"
    • "level": an optional string that defaults to "error" but can be set "warning" in case of user-errors and non-code problems like validation errors.
    • "trace": an optional string which is the stack trace in non-production environments.
    • "data": an object with details about the error ex. the user_id. one of those properties:
      • "validations": Object<string, Array<string>> in case of validation errors it contains a mapping of field names (properties) and their corresponding array of problems.
    • TBD: bi-directional events and server push (for real-time applications like chat).

Tooling

  • code-extract scan code signature to
    • generate IDL.
    • validate requests with json schema, ajv, python dataclass (or pydantic, beartype, ...)
    • generate swagger/openAPI/postman
  • web client
    • multi-transport client (tries to establish websocket, and can use normal HTTP req/res mean while)
    • form2json serialize the HTML form into a JSON and submit it
    • json2json with jQ-like functional programming and data flow.
    • json2dom and json2str (for SSR) with mustache-like template
    • json.dom_ops template (replace, appent, prepend), add_class, remove_class, toggle_class, set_attr

Example Requests


POST /rpc/ HTTP/1.1
Host: example.org
Content-Type: application/json; charset=utf-8

{
   "jsonrpc": "2.0",
   "id": "7cc337d3-77fb-4fa8-a9e6-f909f5d7600b",
   "method": "book.list",
   "params": {
       "page":1,
       "per_page":10
   }
}

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
   "jsonrpc": "2.0",
   "id": "7cc337d3-77fb-4fa8-a9e6-f909f5d7600b",
   "result": {
       "count": 35,
       "items": [
           {"id": 1, "title": "Alice in Wonderland"}
       ]
   }
}

HTTP/1.1 400 OK
Content-Type: application/json; charset=utf-8

{
   "jsonrpc": "2.0",
   "id": "7cc337d3-77fb-4fa8-a9e6-f909f5d7600b",
   "error": {
       "code": -32000,
       "message": "per-page can't be 0",
   }
}

POST /rpc/book.list HTTP/1.1
Host: example.org
Content-Type: application/json; charset=utf-8

{
   "params": {
       "page":1,
       "per_page":10
   }
}

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
   "result": {
       "count": 35,
       "items": [
           {"id": 1, "title": "Alice in Wonderland"}
       ]
   }
}

POST /rpc/book.list HTTP/1.1
Host: example.org
Content-Type: application/json; charset=utf-8

{
   "params": {
       "page":"abc",
       "per_page":10
   }
}

HTTP/1.1 400 OK
Content-Type: application/json; charset=utf-8

{
   "error": {
       "level": "error", // or warning
       "errorcode": "integer-parse-error", // ENUM
       "message": "could not parse integer [abc]", // human
       "data": {
           "validations": {
               "page": ["must be integer"]
           }
       },
       "trace": "" // only in development mode / debug mode
   }
}

Server Sample Codes


// book.ts
import {validate, validate_int, validate_min, validate_max, CodedError} from "xrpc_utils"

@validate({
    page: validate_int,
    per_page: [validate_int, validate_min(1), validate_max(100)],
})
export function list({page: number, per_page: number}) {
    throw new CodedError("not-imp", "not implemented");
    return {
      "count": 35,
      "items": [
        {"id": 1, "title": "Alice in Wonderland"},
      ],
    };
}


  // book.js
  import {add_validator, validate_int, validate_min, validate_max, CodedError} from "xrpc_utils"
  
  export function list({page: number, per_page: number}) {
      throw new CodedError("not-imp", "not implemented");
      return {
        "count": 35,
        "items": [
           {"id": 1, "title": "Alice in Wonderland"},
        ],
      };
  }
  add_validator(list, "page", validate_int)
  add_validator(list, "per_page", [validate_int, validate_min(1), validate_max(100)])
  

# book.py
from xrpc_utils import (
    CodedError,
    validate,
    validate_int,
    validate_min, validate_max, 
)

@validate({
    "per_page": [validate_min(1), validate_max(100)],
})
def get_list(page: int=1, per_page: int=10):
    raise CodedError("not-imp", "not implemented")
    return {
        "count": 35,
        "items": [
           {"id": 1, "title": "Alice in Wonderland"},
        ]
    }

<?php
// Book.php
class Book {
  public static function list(int $page=1, int $per_page=10) {
    throw new CodedError("not-imp", "not implemented");
    return [
        "count"=> 35,
        "items"=> [
           ["id"=> 1, "title"=> "Alice in Wonderland"],
        ]
    ];
  }
}

DIY Browser Client

Code that called the book.get_list RPC defined above can be used like this:

  async function on_clicked() {
    let res = await rpc_call("book.get_list", {page:1});
  }
  
A fully functional client with error handling can be written in ~30 lines like this:

      function throw_parsed(parsed) {
        const err = new Error(parsed.error.message);
        err.code = parsed.error.codename;
        err.validations = parsed.error.validations;
        throw err;
      }
      async function rpc_call(method, params) {
        const body = {"jsonrpc": "2.0", "params": params || {}};
        const res = await fetch(`/rpc/${method}`, {
            method: "POST",
            headers: {
                "Accept": "application/json",
                "Content-Type": "application/json",
            },
            body: JSON.stringify(body),
        });
        let parsed;
        if (!res.ok) {
            if (res.headers.get("content-type")=="application/json") {
                const text = await res.text();
                parsed = JSON.parse(text);
                throw_parsed(parsed)
            }
            throw new Error("req failed");
        }
        try {
            const text = await res.text();
            parsed = JSON.parse(text);
        } catch (e) {
            throw new Error("req failed");
        }
        if (parsed.error) {
            throw_parsed(parsed);
        }
        return parsed.result;
    }