{JSON}.xRPC
Bringing back the pure similicity of modern decoupled multi-tier APIs, MicroServices, Mobile and Web Apps.
Coming soonFeatures
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 HowFully 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 HowShort 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 optionalstringthat defaults to"error"but can be set"warning"in case of user-errors and non-code problems like validation errors."trace": an optionalstringwhich 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-extractscan 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)
form2jsonserialize the HTML form into a JSON and submit itjson2jsonwith jQ-like functional programming and data flow.json2domandjson2str(for SSR) with mustache-like templatejson.dom_opstemplate (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 thebook.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;
}