III The API

Call the API as Procedures

A partner’s back end, or a script, wants to make several calls in one request and does not care for paths and verbs: one endpoint, a method name, parameters, a result. Every route of the application is also a JSON-RPC procedure, without anything being declared per route.

§Turning it on

Add rpc to the exposition of the Context, beside the authorities that toa create wrote:

# context.toa.yaml
exposition:
  authorities:
    local: localhost:8000
  rpc: {}

Every method of the tree is now also a procedure, called at POST /.rpc. The examples call the routes of store.orders that Expose It over HTTP declares:

POST /.rpc HTTP/1.1
host: api.store.example
content-type: application/json
accept: application/json

{"jsonrpc": "2.0", "id": 1, "method": "store.orders._id.GET", "params": {"id": "4c4759e6f9c74da989d64511df42d6f4"}}
HTTP/1.1 200 OK
content-type: application/json
cache-control: no-store

{"jsonrpc": "2.0", "id": 1, "result": {"id": "4c4759e6f9c74da989d64511df42d6f4", "status": "pending", "total": 4200}}

This is the call GET /store/orders/4c47…d6f4/ makes, and it answers what that request answers.

$ npm run restart
$ curl http://localhost:8000/.rpc -H 'content-type: application/json' 
    -d '{"jsonrpc": "2.0", "id": 1, "method": "store.orders.GET", "params": {"query": {"limit": 2}}}'

Without the annotation nothing answers at /.rpc. The path is fixed and cannot be changed.

§Names

A procedure is a route and a verb, and its name is the two written as one word: the segments of the route joined with ., a variable marked with a leading _, and the verb last.

RouteVerbProcedure
/store/ordersGETstore.orders.GET
/store/ordersPOSTstore.orders.POST
/store/orders/:idGETstore.orders._id.GET
/store/orders/:idPUTstore.orders._id.PUT
/store/orders/of/:customerGETstore.orders.of._customer.GET
/pages/**GETpages.__.GET
/GETGET

The name is not chosen: it is the address. Moving a route renames its procedure.

§Routes without a name

A segment can be part of a name if it consists of letters, digits and - only. A route with any other segment — one containing a ., a _ or a *, or a variable whose name is not a single word — has no procedure. It is served over HTTP all the same; /.rpc cannot call it.

Each such route is reported once in the gateway’s log when it starts:

Route cannot be addressed as a procedure  route=/store/catalog/v1.0 segment=v1.0

If procedures matter to the application, name segments and variables with letters, digits and dashes only.

A route that declares map:buffer has no procedure either: it reads the raw request, and a call is not a request.

§Parameters

params is an object, and carries what a request would carry in three places:

  • a key named after a route variable is that variable;
  • query is the query string, as an object;
  • everything else is the body.
{ "jsonrpc": "2.0", "id": 1, "method": "store.orders._id.GET",
  "params": { "id": "4c4759e6f9c74da989d64511df42d6f4" } }
{ "jsonrpc": "2.0", "id": 2, "method": "store.orders.POST",
  "params": { "customer": "b70a7dbca6b14a2eaac8a9eb4b2ff4db", "items": [], "total": 4200 } }
{ "jsonrpc": "2.0", "id": 3, "method": "store.orders.GET",
  "params": { "query": { "criteria": "status==pending", "limit": 10 } } }
  • The query has a name of its own because an operation’s input may well have an id or a limit. An operation whose input has a property named query cannot be given it through a procedure.
  • The value of a variable is one path segment and may not contain /, ? or #.
  • The value of __ (the rest of a path) may contain /. It reaches directives under the name **, as over HTTP.

§Results

The result is the operation’s output, restricted by the same io:output as the route’s, in the format the request’s accept asks for.

A call that found nothing answers result: null: JSON-RPC has no 404, and nothing went wrong.

A call without an id is a notification. It runs and is not answered. A request that carries only notifications is answered 204 No Content.

§Several calls in one request

A request may carry an array of calls. The reply is an array of their answers, without entries for notifications:

[
  { "jsonrpc": "2.0", "id": 1, "method": "store.orders._id.GET",
    "params": { "id": "4c4759e6f9c74da989d64511df42d6f4" } },
  { "jsonrpc": "2.0", "id": 2, "method": "store.orders.GET",
    "params": { "query": { "limit": 2 } } },
  { "jsonrpc": "2.0", "method": "store.orders._id.PUT",
    "params": { "id": "4c4759e6f9c74da989d64511df42d6f4" } }
]

The calls run one after another, in the order given. They are separate calls: a failure of one does not undo or stop the others.

One request may carry up to 32 calls. Change the number with batch:

# context.toa.yaml
exposition:
  rpc:
    batch: 100

§Errors

A problem with the request as a whole is an HTTP status:

StatusWhen
405The verb is not POST.
400The body is unreadable, or is an empty array.
400The array is longer than batch; the body is an error with the code -32002.
401A procedure requires a credential and the request presents none.

A request has one credential for all its calls, so a missing credential refuses the request, not a call. The 401 carries the challenge that says where to authenticate.

A problem with one call is a JSON-RPC error, in a 200 reply:

{ "jsonrpc": "2.0", "id": 4,
  "error": { "code": -32001, "message": "…", "data": { "code": "NOT_PENDING" } } }
CodeMeaning
-32601No route of that name, or no such verb on it.
-32602A route variable is missing, or query is not a valid query.
-32000The identity is not authorized to make this call.
-32001The operation refused. Its own error code is in data.code.
-32603Anything else: an exception.

-32001 is where an application’s own errors arrive. A caller reads data.code, which is the code the operation declares in errors: — the same value an HTTP client reads in a 422 body.

A call that is not a valid call at all — no jsonrpc version, no method — is answered with id: null.

§The same rules as HTTP

A procedure is the route it names, reached another way. It runs through the same directives.

  • Authorization. A call is authorized against the path and the verb its name states. A credential that may GET /store/orders/:id/ may call store.orders._id.GET, and nothing more.
  • Read-only. A procedure named for a GET may only read, exactly as the GET may; one that reaches a write fails with -32603. See GET does not write.
  • Input and output. io:input, io:output and the map directives that do not need a request header apply unchanged.
  • Replies are not cached. Every reply is cache-control: no-store.

One difference: a route declared anonymous is reachable by a procedure call whether or not the request carries a credential. Over HTTP such a route refuses a request that presents one; the details are in Access.

§Finding the names

The answer to OPTIONS /.discovery lists every route the caller may reach, keyed by route template. A name follows from a key mechanically: drop the leading /, replace / with ., : with _, and append the verb.

Why this costs nothing is in Procedures and Tools.

§Summary

Turn onexposition.rpc: {} in the Context
EndpointPOST /.rpc
Nameroute segments joined by ., _variable, __ for **, verb last
paramsroute variables by name, query, the rest is the body
Batchan array of calls, 32 by default, rpc.batch
Application errors-32001, code in error.data.code
Search
Search the documentation