yaar
v3.0.2
Published
Yet Another API Router
Downloads
12
Readme
yaar
An API router to be used by an express application.
Basic usage
In this section, we will walk through basic usage for the library.
Instantiate an api router;
const { APIRouter, HTTPRPCInterface } = require('yaar');
const router = new APIRouter();
yaar
does not listen for or accept connections itself---it is merely a routing layer.
It must be used within an application that accepts express routers, like so:
const express = require('express');
const app = express();
app.use(router.getExpressRouter());
After making a router, create a version and add an interface to it:
router.version(1).addInterface(new HTTPRPCInterface());
Now you can create some methods!
router.register({
method: 'some-method'
}, () => {
return 'some response';
});
The APIRouter class is a CrispHooks emitter, and emits several events during the course of a request:
router.on('request-begin', function(ctx) {
console.log('Request placed to ' + ctx.method);
});
router.on('request-end', function(ctx) {
console.log('Request to ' + ctx.method + ' completed');
});
router.on('request-error', function(ctx, error) {
console.log('Request to ' + ctx.method + ' failed unexpectedly:');
console.log(error);
});
Versions
yaar
allows for routes to be namespaced by version.
To create a version, run:
router.version(1);
const versionTwo = router.version(2);
This returns a VersionRouter
---a child router on which version-specific methods may be defined.
Methods
Methods are functions defined on routers, accessed via interfaces (as explained below).
They are registered on the router, with the method name defined in an options object.
Method names may be semantically sectioned by .
, which can be utilized or ignored by specific interfaces.
See the documentation on APICallRegistrar#register for an in-depth explanation of all accepted options.
router.register({
method: 'foo.bar'
}, () => {
return 'yay!';
});
// => { response: 'yay!' }
As shown above, a return value in a method will be used as a response.
Errors are sent in a similar fashion.
router.register({
method: 'foo'
}, () => {
throw new Error('no!');
});
// => { error: { code: 'internal_error', message: 'no!' } }
XError
instances are explicitly supported, for convenience:
router.register({
method: 'not_modified'
}, () => {
throw new XError(XError.NOT_MODIFIED, 'I\'m afraid I can\'t do that.');
});
// => { error: { code: 'not_modified', message: 'I\'m afraid I can\'t do that.' } }
Methods may also be defined on specific VersionRouter
instances, rather than the main router.
Such methods will only be available under the relevant version namespace.
VersionRouter#register
functions the same as APIRouter#register
.
Parameters
As stated above, parameters are exposed as ctx.params
.
If a method is registered with a schema, these params will be normalized to it.
For example:
router.register({
method: 'everything.ever',
schema: createSchema({ foo: Number })
}, (ctx) => {
return ctx.params;
});
If the above method were called with parameters { foo: '64' }
, the response would be { result: { foo: 64 } }
.
Versions
Methods may be defined to adhere to specific versions. The supported syntax is as follows:
router.register({
method: 'everything.ever',
versions: [ '-1', 3, '4-5', '7', '9-' ]
}, () => {
return true;
});
This versions
option accepts an array with any combination of the following valid items:
- a number, referencing a specific version
- a string, referencing a specific version
- a range terminated on both ends, referencing a range of versions
- a range terminated on only one end, referencing a range of versions
Promises
These methods support promises as well.
Returning a Promise
from a method will result in a success response if the promise resolves,
and an error response if the promise rejects.
router.register({
method: 'resolve'
}, () => {
return new Promise((resolve) => {
setImmediate(() => resolve({ foo: 'bar' }));
});
});
// => { result: { foo: 'bar' } }
router.register({
method: 'limit_exceeded'
}, () => {
return new Promise((resolve, reject) => {
setImmediate(() => reject(new XError(XError.LIMIT_EXCEEDED, 'STOP DOING THAT!')));
});
});
// => { error: { code: 'limit_exceeded', message: 'STOP DOING THAT!' } }
Middleware
APIMiddleware
When you register a method on a router, you are really just defining a single middleware function. Multiple such middleware methods may be defined as follows:
router.register(
{ method: 'foo' },
(ctx) => { console.log('This route was just hit.'); },
(ctx) => { ctx.someArray = []; },
someOtherMiddleware,
(ctx) => { ctx.someArray.push('foo'); },
(ctx) => { ctx.someArray.push('bar'); },
yetAnotherMiddleware,
...
);
The ctx
argument is made up of the following:
ctx.req
: The express request.ctx.res
: The express response.ctx.method
: The name of the method.ctx.params
: Parameters to the called method.ctx.version
: The version of the router on which the method was called.ctx.result
: The result of the method, if one has been set.ctx.error
: The error of the method, if one has been set.ctx.extraErrors
: List of errors that have occurred in PostMiddleware functions.
ctx
is shared across middleware functions in the same pipeline, and may thus be used for state:
router.register({
method: 'foo'
}, (ctx) => {
ctx.blah = 'blah';
}, (ctx) => {
return ctx.blah;
});
PreMiddleware
PreMiddleware runs before relevant methods.
router.registerPreMiddleware({}, (ctx) => {
ctx.someProp = 'foo';
});
PreMiddleware may also be defined per-version, by adding them to a versionRouter
instead of the main router.
PostMiddleware
PostMiddleware runs before relevant methods. Errors in PostMiddleware functions are ignored.
router.registerPostMiddleware({}, (ctx) => {
console.log(`The method ${ctx.method} was just hit.`);
});
PostMiddleware may also be defined per-version, by adding them to a versionRouter
instead of the main router.
Interfaces
Interfaces transform requests/responses to a common format for API calls.
They are added to VersionRouter
s, like so:
router.version(1).addInterface(new SomeAPIInterface());
versionTwo.addInterface(new SomeAPIInterface());
HTTPRPC
HTTPRPC is an interface for RPC over HTTP.
This exposes the methods as routes accessible over HTTP.
All methods are exposed as POST requests, with all method parameters passed in the POST body.
All methods respond with a status code of 200
, whether the responses contain errors or not.
Methods will respond with a 500
status code if the requested endpoint is not found.
Consider the following method:
router.register({
method: 'foo'
}, () => {
return 'yay!';
});
This would be accessible via HTTP requests to /v1/rpc/foo
, for example.
The HTTPRPC interface translates methods segmented by .
into /
in the HTTP endpoints.
The following method would be accessible at /v1/rpc/foo/bar
:
router.register({
method: 'foo.bar'
}, () => {
return 'yay!';
});
See the HTTPRPCInterface file for more information.
JSONRPC
JSONRPC is an interface for following JSON RPC standard over HTTP.
Request posted to /${verion}/jsonrpc
is handled by this interface. It accept request body like this:
{
method: 'method.name',
id: 'request.id',
params: {
foo: 'bar'
}
}
It will always return with HTTP status 200 with following response:
{
id: 'request.id',
result: {
key: 'value'
},
error: {
code: 'error code',
message: 'some error message'
}
}
Note field result
and error
will not always exist. If an error is returned, result
field should have an value of null
. Likewise, error
will be null
if result
is returned.
Some extra options on the register object for JSONRPC:
manualResponse
: If set, and no route middlewares throw an error, no response will automatically be sent. The final middleware will be expected to directly manipulate the res object to construct the response.streamingResponse
: Use this option to stream a large number of objects back to the client. If set, the final route middleware is expected to return a readable object stream containing the response data, or a promise resolving with such a stream. Data from this stream will be converted into newline-separated JSON and written to the response. Once the stream terminates, a final special object will be written, containing a boolean flagsuccess
that indicates whether or not an error was thrown by the stream, and the error if applicable.keepAlive
: If set, and manualResponse is not set, the router will periodically insert a space into the response during downtime, in order to keep the connection open. Defaults to true.keepAliveInterval
: Specifies the length of time in ms to wait before sending a keep-alive space. Defaults to 10 seconds.
API Info
Yaar be implementin' yer API routes fer gettin' thar method infarrmation. Just call APIRouter#registerAPIInfoRoute()
and thar be
treasure called the "api-info" method.