ApiMan
Generic API methods manager that is exportable to arbitrary protocols, including HTTP and websockets.
Key features:
- Hierarchical API methods stored on Resources
- Middleware support
- Promise-based: using the q package
- Full unit-tests
The Motivation
For a REST API, Express is a great choice, but imagine you need to support multiple protocols at the same time and want to have the code organized. Faking requests for Express is a tricky thing that is not guaranteed to function as it progresses...
ApiMan steps in: you define a tree of resources with named methods bound to them, and now just bind it to Express as a middleware. Wait, some methods should also be available through socket.io? No problem.
Now, we want some middleware for data preparation and authentication? Yes, we support that.
Enjoy it, guys :)
Table of Contents
Core Components
Resource, Root
A Resource is a collection of sub-resources, middleware and methods that is identified by path.
You create a sub-resource by calling the Resource.resource(path)
method of
a parent Resource
or the Root
container:
var root = ; var user = root;var user_profile = user;
You create a Root resource first, then continue defining the resources on it. The Root is actually a resource with an empty path.
Although we follow the HTTP-style slash-separated paths, you're free to use any convention you're comfortable with.
The following properties may be useful:
userroot; // Reference to the root Resourceuserparent; // Parent resourceuserpath; // Parent resource
Resource.resource(path):Resource
Add a new child Resource and assign a path
to it.
Returns: the new Resource
object.
Method
After you have set up the resources hierarchy, you can define methods on them, including the Root.
A Method
is defined with the Resource.method(verbs, ...callbacks)
method of a Resource
:
user_profilemethod'save' { return ;};
The method callback accepts two arguments: the Request
and Response
objects. Use them to access the request data and
send responses.
The method should return a promise which is resolved when the method sends a result with Response.send()
.
Resource.method(verbs[, middleware, ..., ], method):Resource
Add a Method to the Resource.
Arguments:
verbs: String|Array.<String>
: Method name, or an array of names. Later, the method will be available under this name.middleware: function(req: Request, res: Response):Q
: Optionally provide an array of middleware methods that will be called before the method itself. See: Middleware.method: function(req: Request, res: Response):Q
: The method function.
Request
The Request
object is created for each request and contains the info about the request: resource path, method name,
method arguments, fields added by the middleware, etc.
The Request
object has the following properties:
req.path
: The requested resource:'/user/profile'
req.verb
: The requested method:'save'
req.args
: Method arguments object:{ user: {login: 'kolypto', ...} }
req.path_arr
: An array of path components split on matched resources:['/user', '/profile']
req.path_tail
: The remaining path suffix that's left after matching the resources.
Response
The Response
object is created coupled with the corresponding Request
to handle the results of a method call:
a method reports errors and sends results through it.
Response has two channels to send the results with:
- System channel:
A promise which is automatically resolved when the middleware and the method has finished successfully.
If there was an unhandled exception, the promise is rejected with a runtime error.
This logic is handled by the
Response.system
promise. - Result channel:
A promise which is manually resolved by the middleware or the method using
Response.send()
. This returns a result, or an expected erorr. This logic is handled by theResponse.result
promise.
This separation allows to differentiate unexpected erorrs and expected error responses: the System channel reports unexpected runtime errors, while the Result channel handles the expected results, including errors, which are usually send to the client as is.
Response.send(err, result)
Send a result to the client: either an error or a successful result.
Note: when a promise, returned by a method, is resolved without sending any response, ApiMan creates a "No response sent" error:
rootmethod'empty' { ; // the method returns nothing, so the response is resolved before callback function is called. // This results in a "No response sent" error.};
This logic actually ensures that you'll never have your requests hanging indefinitely if a method does not send anything, for instance, in case of a runtime error.
To make the above code error-prone, just return a promise which resolves once all operations are finished.
Response.ok(result)
Convenience method that wraps Response.send(undefined, result)
Response.error(err)
Convenience method that wraps Response.send(err, undefined)
Response.isPending():Boolean
Check whether the response is in pending state: did not explicitly send any result.
When any middleware or method uses Response.send()
, the Response is resolved and no subsequent middleware/method
is executed. In other words, if a middleware function sends a response, the method is not executed.
Middleware
A middleware function is no different from the Method function: it accepts the Request
, Response
objects as arguments
and can send responses.
The difference is that the middleware is called before the Method function, and the middleware can be assigned to both Resources and Methods.
Method Middleware
Like in Express, each method can use an arbitrary list of middleware functions which are specified before the method function. See: Resource.method().
// Middleware functionvar { // Middleware if !requserisAdmin res;}; usermethod'delete' adminOnly // middleware { // method function return ; // remove the user };
Note that if any middleware sends a response, no subsequent middleware are executed, nor the method itself.
Resource Middleware
Moreover, a middleware can be attached to a Resource
: it will be executed for all methods of the resource itself
as well as for the methods of sub-resources:
admin = root;admin; // all methods & sub-resources are not admin-only
Resource.use(middleware[, ...]):Resource
Use the given middleware functions for the Resource.
Executing Methods
After the Resource hierarchy and the methods are set up, you can call the methods by resource path and method name.
Public API
Resource.exec(path, verb, args, req):Q
Locate a method by path
and verb
, then execute it with args
. Is usually called on the Root resource.
Arguments:
path: String
: Path to some resource.verb: String
: Name of the method to execute.args: Object?
: Method arguments object.req: Object?
: Additional fields for theRequest
object. Useful to pre-populate the user session. The provided object also receives all the fields set by ApiMan: see Request.
Returns: A promise for a result, or an error. For runtime errors (reported through the System channel), ApiMan sets
the Error object's system
property to true
: err.system = true
.
This method does the following:
- Create the
Request
andResponse
objects - Traverse the resources tree and find the matching resource with prefix matching.
For instance,
'/user/profile'
first matches the'/user'
resource, then its'/profile'
child resource. - Find the method by name
- Executes all resource middleware down the matching resources chain
- Executed the method middleware and the method
- If any middleware has sent a response, no subsequent middleware is executed, nor the method is.
- If no response was sent, a "No response sent" error is reported
- A promise for a result is returned
Internal Methods
While the Resource.exec()
is usually enough, you might need these also.
Resource.which(path, verb[, request]):Method?
Find a matching method by path and verb.
Arguments:
path: String
: Path to the wanted resourceverb: String
: Method name to look forrequest: Request?
: OptionalRequest
object. Is used to populate its fields.
Returns: The Method
object, or undefined
if not found.
Resource.request(request):Response
Process the provided Request and return a Response.
This method allows you to use a custom Request
object and process the Response
in an arbitrary fashion.
Handling Results
root;
Prefix Matching
Given a path, ApiMan performs a case-sensitive precise prefix matching. For instance, given the following resources chain:
var root = ;root ;
path '/user/device/commands/private'
recursively matches each resource by
prefix: '/user'
, '/device/commands'
, '/private'
.
Don't expect ApiMan to forgive extra or missing slashes: it's protocol-agnostic
by design and, potentially, all special characters might have a meaning.
For instance, you can use 'user.device.commands'
for resource names.
Anyway, nothing prevents you from making a preprocessor which tunes the input to your taste:
// Ensure a leading slash, no trailing slash, and collapse multiple slashespath = '/' + path;
Special Features
Endpoint Resources
You can create Resources that consume all requests that go into it: such resources have a single function that handles all requests.
Resource.endpointMethod([middleware, ...], method):Resource
Add an endpoint method on the Resource: the method that handles all requests that fall into the resource.
The Request
object will have the path_tail
property set to the remaining path suffix.
Arguments:
middleware: function(req: Request, res: Response):Q
: Optional middleware functions to use. See: Method Middlewaremethod: function(req: Request, res: Response):Q
: The endpoint method to use.
Example:
var root = new apiman.Root(),
upload = root.resource('/upload')
;
upload.endpointMethod(function(req, res){
req.path_tail; // path suffix
req.verb; // arbitrary method name
});
root.exec('/upload/file.txt', 'save', { file: ... })
.then(function(){
// upload saved
});
Controller Methods
Adding all the methods manually is not the only way to define them: you can feed a Resource with an arbitrary object, and ApiMan will import its methods. The MVC world knows this approach as Controllers.
Resource.controllerMethods(ctrl):Resource
Add methods from a controller object.
ApiMan imports a property only if:
- It is a function (non-functional properties are ignored)
- Its name does not start with an underscore
_
(protected members are ignored).
Arguments:
ctrl: Object
: The controller to import the methods from.
Notes:
- All methods maintain the
this
binding: you can freely use controller fields and protected methods! - In order to set middleware functions for a method, put them in the
middleware
proeprty of the method function.
Example:
// Controllervar { // constructor thissomething = something;}; UserCtrlprototype{ // method res;};UserCtrlprototypegetmiddleware = // middleware for the method { reqmw_worked = 'yesss!'; }; UserCtrlprototype{ // another method res;}; UserCtrlprototype{}; // Import an instantiated controllervar root = user = root ;user;
This will make the '/user:get' and '/user:set' methods available.
Bundled Middleware
All bundled middleware come in the require('apiman').middleware
module.
Session Middleware
Initializer: apiman.middleware.session(options)
Port of the connect.session middleware which allows you to reuse the session Store backends, like the connect-redis package.
var root = ;root
When a session middleware is in effect, the Request
object gets the following extra fields:
req.sessionID: String
: The session identifier stringreq.session: Object
: The persistent session objectreq.sessionStore: connect.Store
: The session store backend
The session is only saved if the middleware & the method has had no runtime errors (thrown exceptions)!
Example on how to make 2 requests using a single session:
var sessionID; // remember the session ID // First request: sign in, get the sessionvar req = {}; // sessionID will be stored here root // Second request: use the same session id ;
Exporting the APIs
In order to expose your APIs to some protocol, you need to implement the ApiMan method caller as a singular endpoint:
in other words, create a handler which transforms the input into an ApiMan Resource.exec()
call and formats the output.
You can either Export The APIs Manually or use one of the Bundled Adapters.
Bundled Adapters
Bundled adapters implement the most wanted protocol adapters in a reusable manner.
All bundled middleware come in the require('apiman').adapters
module.
Express Adapter
Express adapter is a middleware maker that catches all incoming requests under a path and handles them with ApiMan methods.
Initializer: apiman.adapters.express(root, options)
Arguments:
-
root: Resource|Root
: The resource to serve -
options: Object
: Middleware options-
prepareRequest: function(req: Object):Request?
: An optional custom function that converts the incoming Expressreq
request into an ApiMan request.It should return an object with the additional Request fields. It's also required to return:
path
,verb
,args
.Default: split the request URI in 2 on ':' and get the
path
&verb
; combine request query & body intoargs
; pass thereq.files
as is.As a result, you call methods with '/path/to/resource:methodName', the arguments are provided as query params or sent in the request body as JSON.
-
sessionCookie: { name: String, maxAge: Number }?
: When the Session Middleware is used, you probably want to pass the sessionID through a cookie. To do that, specify the cookie settings here.Default: disabled.
Fields:
name
is the name of the cookie (default: 'sessionID'),maxAge
is the session expire time in seconds.See express.cookie() and (connect.session)[http://www.senchalabs.org/connect/session.html] for more details.
-
fixSlashes: Boolean?
: Whether to forgive extra slashes in the path. See Prefix Matching.Default: true.
-
sendResult: function(req: Object, res: Object, result: *)?
: An optional custom function that sends the result with Expressres
response.Default: sends the result as JSON with HTTP code 200.
Arguments:
-
sendError: function(req: Object, res: Object, error: Object, e:*)
: An optional custom function that sends the error with Expressres
response.Default: sends the result as JSON
{ error: error }
, with HTTP status code 500 for system errors, 400 for method errors. If thee
error specifies thehttpcode
field, it overrides the chosen HTTP code.Arguments:
req
,res
are Express request and response ;e
is the original error;error
is the prepared error object which is guaranteed to be an object.Note: as methods in general can return errors of any type, this adapter casts them to a guaranteed object
{ message: String, system: Boolean }
format.
-
Example:
var apiman = express = ; // Resourcesvar root = ;root; // ApiMan sessions // Prepare Expressvar app = ;app; // enable cookiesapp; // enable JSON // Expose the APIsapp;
For a mature example, see /tests/adapters-express-test.js.
Exporting The APIs Manually
This section describes how to export the APIs manually. There are Bundled Adapters that simplify this part with convenient helpers.
Express
Assuming you already have your APIs set up under the root
Resource, let's export these to HTTP with
Express.
First, you need to decide on the conventions to use for:
-
Method call convention.
Example: Using URI for Resource paths, method is provided after a colon
:
. the arguments are sent either through the query params or in the request body as a JSON object. -
Successful responses
Example: encode the output as JSON, with HTTP code 200.
-
Error responses:
Example:
{ error: { code: Number, message: String } }
, as JSON. If the Error object has thehttpcode
property, send it as a code. -
Error responses and HTTP codes
Example: use HTTP code 400 by default.
-
System Error responses and HTTP codes
Example: use HTTP code 500 by default.
Here's a simple solution:
var express = _ = ; var root = ; // assuming the resources and methods are defined var app = ; app;
For a full solution which supports files, sessions, and handles errors correctly, see Express Adapter.
socket.io
Piece of cake: as socket.io can exchange json objects, you just need a handy convention for sending requests and getting responses.
The only difficulty is that socket.io does not support the request-response protocol out of the box, but we can easily overcome that by numbering the packets.
Given the above, let's use the following data exchange protocol:
- Request:
{{ id: Number, path: String, verb: String, args: Object }}
- Response:
{{ id: Number, result: Object, error: null }}
- Error:
{{ id: Number, error: { code: Number, message: String } }}
On the server:
iosockets;
And on the client:
{ var request = id: apicall_id++ // packet id path: path verb: verb args: args || {} ; apicall_waitrequestid = callback; socket;};apicall_id=0;apicall_wait = {}; // Listen for responsessocket; // Usage;
Weak points:
- On reconnect, the response can't be received transparently
- The exposed error objects can potentially contain sensitive data like stack traces
- Callback-based interface: use promises instead