2.4.0 • Published 9 years ago

espressojs v2.4.0

Weekly downloads
5
License
MIT
Repository
github
Last release
9 years ago

espressojs

A framework for building REST APIs

Build status

espressojs is a small Open Source framework that can be used to build HTTP REST APIs in JavaScript. It's core concept are cascading requests that allow you to keep your resource handlers very DRY.

Also, it is a framework on top of any kind of server functionality, it does not come with a built-in server. It provides you a way of managing resources in a flexible way.

Want a usage example? There is a very well documented usage file available https://dak0rn.github.io/espressojs/ that shows you how to setup an API and expose it using express.

Cascading requests

The idea behind cascading requests is that a request may be handled with multiple handlers invoked from general ones to more specific ones and results are passed to the next handler every time. This allows you to write DRY and small handlers for resources.

npm.io

More information about the way espressojs uses resources handlers can be read in the wiki.

Installing

espressojs can be installed using the node package manager.

npm install --save espressojs

The espressojs API

This API description is not meant to be very detailed, please refer to the Wiki for more information about the way espressojs works

Creating a new API

espressojs exposes a constructor function that can be used to create new API instances.

var Espresso = require('espressojs');
var api = new Espresso();

The constructor function takes an optional argument with configuration options.

var Espresso = require('espressojs');

var options = {
    // ...
};
var api = new Espresso(options);

The following options are supported:

{
  "skipMissingHandlers": true,       // Skip non-existing handlers for URI segments

  // Can be used to turn off cascading request handling globally
  "cascading": true,

  // Resource that handles requests w/ empty paths
  // Useful if someone requests your API root w/out a leading slash
  "rewriteEmpty": "/",

  // Indicates if trailing slashes at the end of URLs should be ignored
  // Thus, a resource registered as `/api` can be accessed using
  // `/api/`
  "ignoreTrailingSlash": true,

  // Default response headers
  "headers": {},

  // These options will be used later to
  // dynamically build URLs
  "hostname": "localhost",          // Name of the server
  "protocol": "http",               // Protocol
  "apiRoot":  "/"                   // Root of the API,
  "port": ""                        // Port
}

More details about this can be found here. These options can be accessed using the Configurable interface every API instance provides.

Registering resources

Resources can be registered using the .resource function:

Espresso.prototype.resource = function(pattern, fn, options, context);
Espresso.prototype.resource = function(pattern, handlers, options, context);

pattern is a relative URL identifying a resource. It may contain placeholders starting with a colon, like /api/:version/users/:userid.

The second argument can either be a function (fn) that is meant to handle all requests or an object (handlers) mapping HTTP verbs to handler functions. Any HTTP verb that does not have a mapper will return a 405 Method not supported response if requested.

options is an optional argument with configuration items for this handler. The following flags are supported:

{
    name: '',       // Unique name for this resource. Used to get access to it later.
    cascading: true // Indicates if cascading should be available for parent handlers
}

More details can be found here.

context is an optional argument that is used as this when invoking a handler function. If not submitted, this will be the empty object ({}).

Here are some short examples:

api.resource('/api', function(request, response, api) {
    return 'API';
});

api.resource('/api/:version', {
    // We want GET requests only
    'get': function(request, response, api, value) {
        // value will be 'API'
        return value + request.params.version;  // 'API' + :version
    }
});

Handler functions may also return a promise:

api.resource('/api/:version/:collectionName', function(req, res, api, value) {
    var deferred = api.deferred();

    myAsyncTask(function(){
        deferred.resolve( ':collectionName contents' );
    });

    return deferred.promise;
});

Handler functions

As shown above, handler function have the following signature:

function(request, response, api, value)
  • request is a request given to the API. request.path will be the path of the resource handled by this function.
  • response is a response created by handler functions. This object is shared between every handler and the serializer function.
  • api is the currently used API object
  • value is the return or fulfillment value of the previous handler.

Getting resources

Registering handlers can be accessed with the .get() function.

Espresso.prototype.get = function(what) {};

The given argument has to be an object identifying a resource.

{
    name: 'name of the handler',   // Unique name of the handler
    // or
    pattern: '/handler/:pattern',  // Pattern of the handler
    // or
    path: '/handler/something'     // Handler that would handle this path
}

.get() will return null if no handler can be found.

The function returns a Handler object;

Getting all resources

If you need to have access to all registered resources you can use .getResources() that returns an array of all Handlers.

Espresso.prototype.getResources = function() {};

Deleting resources

Resources can be deleted with the .delete() function.

Espresso.prototype.delete = function(what) {};

The given argument has to be an object identifying a resource.

{
    name: 'name of the handler',   // Unique name of the handler
    // or
    pattern: '/handler/:pattern',  // Pattern of the handler
    // or
    path: '/handler/something'     // Handler that would handle this path
}

The properties are checked exactly in this order, so if you submit both .pattern and .name only .name will be used to find a handler.

If you do not submit a valid object with valid values or if no handler was found the function will exit silently.

api.resource('/a', function(){}, { name: 'a' }); // #1
api.resource('/a/:b', function(){});             // #2
api.resource('/a/b/:c', function(){});           // #3

// This would remove #1 even though the pattern for
// #2 is also given
api.delete({ pattern: '/a/:b', name: 'a'});

// This would remove #3
api.delete({ path: '/a/b/42' });

// This would remove #2
api.delete({ pattern: '/a/:b' });

It is also possible to delete the currently executed handler:

api.resource('/something', function(request, response, api) {
    api.delete(this);
});

More information can be found here.

Setting the serializer function

Serializer functions are used to create a serialized version of the last handler's return/fulfillment value. They have the same signature as resource handlers and should return something that can be sent to the client. It also can return a promise that will provide its fulfillment value to the function that has sent the request.

api.setSerializer( function(req, res, api, value) {

    if( "application/json" === res.headers['Content-type'] )
        return JSON.stringify( value );
    else
        // Something else here

});
  • request is a request given to the API. request.path will be the path of the resource handled by this function.
  • response is a response created by handler functions. This object is shared between every handler and the serializer function.
  • api is the currently used API object
  • value is the return or fulfillment value of the last handler.

The default handler will just invoke JSON.stringify on whatever it gets.

Getting the serializer function

If you want to get the serializer function use the .getSerializer function for that.

var fn = function() {};

api.setSerializer(fn);
api.getSerializer() === fn; // true

Promises

An espressojs object offers a .deferred() function that returns a deferred object and can be used to perform asynchronous operations without getting annoyed by callbacks.

function(res, res, api) {
    var deferred = api.deferred();

    databaseOperation(function(err, result) {
        if( err )
            deferred.reject(err);
        else
            deferred.resolve(result);
    });

    return deferred.promise;        // Return only the promise part
}

Request

espressojs offers a Request object used to send requests to the API. It stores important information about the request including the required fields for the HTTP method (.method) and the relative path (.path).

Request
    .method = undefined;            // Request method, required
    .body   = {};                   // Request body, key-value-mappings
    .hostname = undefined;          // Name of the host
    .ip       = undefined;          // Client's IP address
    .path     = undefined;          // The request path, required
    .protocol = undefined;          // Request protocol, e.g. 'https'
    .query    = {};                 // Request's query string, key-value-mappings
    .cookie   = {};                 // Key-value-mapping of cookie values
    .headers  = {};                 // Key-value-mapping of all header fields
    .params   = {};                 // Key-value-mapping of parameters from URL patterns

The constructor function allows you to provide an object to overwrite properties or add custom attributes.

var req = new Espresso.Request({ method: 'get', path: '/api/v1/users/dartvader' });

req.method; // get
req.path;   // /api/v1/users/darthvader

Response

espressojs offers a Response object used to store meta data about the generated request. An instance of it is shared between resource handlers and may be changed multiple times. While it offers some generic properties it depends a lot on the underlying server and the rest of the application.

Response
    .status = '200';        // HTTP status code
    .headers = {};          // Response headers
    .body = undefined;      // Response body
    .cookies = {};          // Key-value-mapping of cookies
    .rawBody = undefined;   // Raw body, not serialized. Will be set before
                            // given the response to the invoking function

Handler

A Handler represents a resource handler in the API. It stores information about the pattern, handlers for HTTP verbs and the context to be executed in. Since it uses the Configurable interface there are also options available described here.

handler.getPattern();       // metacarattere pattern
handler.getCallback('get'); // handler function for HTTP GET
handler.getCallbacks();     // all callbacks
handler.getContext();       // Handler's `this` context

Configurable

All espressojs classes that inherits from the Configurable class provide the following methods:

  • .setOption(key, value) sets the option named key to value
  • .getOption(key) returns the value of the option named key
  • .setAll(options) sets all options from the given object
  • .getAll() returns all options

Dispatching a request

The .dispatchRequest() function is the interface to your application. It takes a request and returns a promise that will be resolved with a response.

Make sure to set at least .path and .method if your request so that it can be handled correctly.

Espresso.prototype.dispatchRequest = function(request) {
    // magic
    return response;
};
var r = new Espresso.Request({
    method: 'get',
    path: '/api/v2/users/_all'
});

api.dispatchRequest(r).then(function(response) {
    // Request successful
}).catch(function(response){
    // Request failed
});

An error is always provided as an object in response.body. It has a key error containing the error message.

The following rules concerning errors apply:

  • If the given Request is undefined, null or not a valid object a Response with status code 500 will be created. It's body contains the error message '"invalid Espresso.Request given"'.
  • If the given Request does not have a .method set a Response with status code 500 will be created. It's body contains the error message '"invalid Espresso.Request given"'.
  • If the given Request does have .method set to something that is not a string a Response with status code 500 will be created. It's body contains the error message '"invalid Espresso.Request given"'.
  • If the given Request does not have a .method set a Response with status code 500 will be created. It's body contains the error message '"invalid Espresso.Request given"'.
  • If the given Request does not have a .path set a Response with status code 500 will be created. It's body contains the error message '"invalid Espresso.Request given"'.
  • If the given Request does have .path set to something that is not a string a Response with status code 500 will be created. It's body contains the error message '"invalid Espresso.Request given"'.

  • If no handler for the requested path is available a Response with status code 400 will be created. It's body contains the error message '"resource not found"'.

  • If the global option cascading is set to true and the global option skipMissingHandlers is set to false ( = all handlers in a cascade chain have to be executed ) and if a handler for a parent URI of the requested one is missing a Response with status code 500 will be created. It's body contains the error message '"incomplete resource handler chain"'.

Side note: Errors listed above will not be passed to the serializer but given to the returned Promise directly when it is rejected.

Utility functions

chainComplete

The api.chainComplete() function can be used to detect if the current handler has been invoked in a chain. It expects one argument, the context of the handler.

Espresso.prototype.chainComplete = function( handlerContext ){};
api.resource('/some/thing', function(response, request, api) {

    if( api.chainComplete(this) )
        // ...

});

linkTo

The api.linkTo() function can be used to create hypermedia links to a resource. All of its two arguments are optional and it returns an URL which is built using the server options stored in the API.

Espresso.prototype.linkTo = function(what, replacers){};

The first argument can be

  • omitted, the API URL is returned then
  • a string which will be appended to the API URL
  • an object {name: 'something'} where 'something' is the name of a resource handler
  • the this context of the current handler. The API URL + the current path will be returned

If you use the object argument to link to a handler the second argument will be used to replace placeholder values in its URI.

var api = new Espresso({
    protocol: 'https',
    hostname: 'apiserver.example.org',
    apiRoot: '/api/v1',
    port: '1024'
});

api.resource('/key/:storage/all', function(){}, {name: 'allkeys'});

api.linkTo();                       // https://apiserver.example.org:1024/api/v1
api.linkTo('/permission');          // https://apiserver.example.org:1024/api/v1/permission
api.linkTo( { name: 'allkeys'} );   // https://apiserver.example.org:1024/api/v1/key/:storage/all
api.linkTo(
    {name: 'allkeys'},
    {storage: 'users'});   // https://apiserver.example.org:1024/api/v1/key/users/all
2.4.0

9 years ago

2.3.5

9 years ago

2.3.4

9 years ago

2.3.3

9 years ago

2.3.2

9 years ago

2.3.1

9 years ago

2.3.0

9 years ago

2.2.6

9 years ago

2.2.5

9 years ago

2.2.4

9 years ago

2.2.3

9 years ago

2.2.2

9 years ago

2.2.1

9 years ago

2.2.0

9 years ago

2.1.0

9 years ago