Documentation

Installation

npm install percolator

Running Tests

make test

Hello World Quick Start:

Why should I care about Hypermedia??!?

Because Hypermedia APIs are pretty awesome for the people using your API.

Developers can find their way to all your endpoints just by following the links in the json payload instead of having to read a bunch of docs. Using a browser plugin like JSONView for Firefox or chrome makes it so developers can just surf around your API in their browser as if your responses are regular web pages.

These types of APIs are machine-readable as well, so they're spider-able, and even allow automatic form generation, or automated fuzz-testing.

In other frameworks adding links everywhere can be time-consuming. Percolator makes this either automatic or very easy though.

NOTE: If you're worried about the size of your responses, keep in mind that with proper caching and compression (gzip), there should be little to no cost to your clients' performance or bandwidth.

If you're still unconvinced, you can just pass autoLink : false to the Percolator constructor, and no links will be added automatically.

The Percolator Server Object

Any Percolator objects you create are just http servers (based on oneone) with a bunch of helpers for making JSON APIs more easily.
The constructor
The constructor returns a percolator server. It can take an optional single object as a parameter like so:
var server = new Percolator({"some" : "object"});

This is called the app object.

Anything you put in the app object will be available to all your HTTP handlers as req.app, so it's great for shared configuration, database objects, etc.

Additionally, the constructor recognizes a few special properties of the app object:

protocol - 'http' or 'https'
resourcePath - the url path that all the resource will be routed from (eg. Setting it to '/' will serve the resources from http://yourdomain.com/ while setting it to '/api' will serve the resources from http://yourdomain.com/api .
staticDir - The directory on the filesystem from which you will serve static content (use an absolute path!).
port - the http port. A low port like 80 will not work unless you run the app with root privileges.

autoLink - A boolean that indicates whether or not to automatically add some links to your json payloads for you. This value is optional, and the default is 'true', meaning that hyperlinks will be added automatically to your json payloads.
port - the http port. A low port like 80 will not work unless you run the app with root privileges.
key - the key for https. If you're not doing https, this is ignored.
cert - the cert for https. If you're not doing https, this is ignored.
pfx - the pfx for https. If you're not doing https, this is ignored.
parseBody - A boolean that indicates whether or not to automatically wait for the entire body to stream in before calling an HTTP method handler. If set to true, the body will be available as req.rawBody or parsed as json to an object available as req.body . The default for this is false, because the req.onBody and req.onJson functions allow you to access the same data on a case-by-case basis. It's recommended that you try those first.

You're obviously going to want to limit the number of app object variables that you add beyond the necessary ones, but certain types of objects might make sense in that shared space.

route(routeString, handler)

The route() method of the server takes two parameters, the route string and the resource object.

It then sets a route using the route string that the given handler object will handle when an incoming request matches that route.

Route strings can be regular expressions, or express/sinatra-style route strings.

The resource object is any object that has methods on it that match HTTP methods like POST, PUT, GET, DELETE, etc. The method takes the standard req and res parameters.

The Hello World example above has a resource object that implements only GET:

{
  GET : function(req, res){
    res.object({message : 'Hello World!'}).send();
  }
}
Of course, other HTTP methods (PUT, POST, DELETE, etc) can be added to this object as well to make this API do more. Here it is, passed to route() with a routeString of '/', the root path:
server.route('/', {  GET : function(req, res){
                              res.object({message : 'Hello World!'}).send();
                            }});
Here it is with a POST handler as well:
server.route('/', {
                    GET : function(req, res){
                              res.object({message : 'Hello World!'}).send();
                    },
                    POST : function(req, res){
                      req.onJson(function(err, obj){
                        res.object({posted:obj}).send();
                      });
                    }
});
Note that because resource objects are just objects, you can load them from a file as modules, or create components that you re-use for different modules.
listen([cb])
Call listen() on the server to start it listening. The default port (unless overridden by the app object) is 3000. listen() takes an optional callback as well, that is called when the server is listening, or when there's been an error. The error will be the first parameter to this callback, if it exists.
server.listen(function(err){
  console.log('server is listening on port ', server.port);
});
before(cb)

The before() method takes a callback as its only parameter. The callback is called when a request occurs.

The callback takes req, res, handler and cb parameters.

The req and res parameters are the standard node request and response objects. If you want to augment them for some reason, for all requests, this is a great place to do it.

The handler is the resource object that is routed to the request url in the router. It will contain the functions meant to handle the different HTTP methods.

The cb parameter is a callback that will be called to signal that pre-processing is complete.

Here's an example:

server.before(function(req, res, handler, cb){
  // print the request method and url before every request.
  console.log(' <-- ', req.method, ' ', req.url);
  cb();
});
after(cb)

The after() method takes a callback as its only parameter. The callback is called after a response is ended.

The callback takes req, res, and handler parameters.

The req and res parameters are the standard node request and response objects. If you want to augment them for some reason, for all requests, this is a great place to do it.

The handler is the resource object that is routed to the request url in the router. It will contain the functions meant to handle the different HTTP methods.

Here's an example:

server.after(function(req, res, handler){
  // print the request method and url after every request.
  console.log(' <-- ', req.method, ' ', req.url);
});
connectMiddleware(middleware)

The connectMiddleware() method takes a connect compatible middleware as its only parameter.

Here's an example:

server.connectMiddleware(connect.favicon());
// add the favicon middleware to the percolator server

The Handler Object

The handler object is not an object in the Percolator framework, but rather is one that an application written on the Percolator framework should provide for each route that it defines. It is simply the object that is passed to calls to Server.route() that specifies how a route should handle requests.

It may or may not implement any of the methods listed below. If it does not implement a particular HTTP method, requests to the handler for that method will result in an automatic 405 (method not allowed) response.

GET(req, res)

Implementing this method on a handler object will allow it to respond to GET requests. The only parameters are the standard request and response objects from node. Any return value will be ignored.

POST(req, res)

Implementing this method on a handler object will allow it to respond to POST requests. The only parameters are the standard request and response objects from node. Any return value will be ignored.

DELETE(req, res)

Implementing this method on a handler object will allow it to respond to DELETE requests. The only parameters are the standard request and response objects from node. Any return value will be ignored.

PUT(req, res)

Implementing this method on a handler object will allow it to respond to PUT requests. The only parameters are the standard request and response objects from node. Any return value will be ignored.

authenticate(req, res, cb)

Implementing authenticate() is not required but is convenient if you want to specify a general authentication strategy for all methods on the resource. If you implement authenticate(), it will automatically return 401 responses for unauthenticated access while permitting authenticated access as normal.

The authenticate() function has three required parameters: the standard req and res parameters , and a callback (cb).

The cb argument is a callback that takes two parameters. The first parameter is an error object if any error occurred. If the error object is strictly true, then the response will be a 401. If the error object is non-strict-true but still truthy, then the response will be a 500 (internal server error). The second parameter should be an object that represents the logged in user. It will automatically by added to the req object as req.authenticated in any member resource methods that you implement.

server.route('/someProtectedPath', {
  authenticate : function(req, res, cb){
    // try to get the user here, based on cookie, Authentication header, etc
    if (cannotGetUser){
      return cb(true);  // Percolator will 401 for you
    }
    cb(someError, theUser);
    // if there wasn't some other error, theUser will be available 
    // at req.authenticated in all methods
  },
  GET : function(req, res){
    res.object({youAre : req.authenticated}).send();
  }
});
basicAuthenticate(username, password, req, res, cb)

Implementing basicAuthenticate() is not required but is convenient if you want to use basic http authentication on the resource. If you implement basicAuthenticate(), it will automatically return 401 responses for unauthenticated access while permitting authenticated access as normal.

The basicAuthenticate() function has five required parameters: the basic http auth username, the basic http auth password, the standard node req and res objects, and a callback, cb.

The cb argument is a callback that takes two parameters. The first parameter is an error object if any error occurred. If the error object is strictly true, then the response will be a 401. If the error object is non-strict-true but still truthy, then the response will be a 500 (internal server error). The second parameter should be an object that represents the logged in user. It will automatically by added to the req object as req.authenticated in any member resource methods that you implement.

server.route('/someProtectedPath', {
  basicAuthenticate : function(username, password, req, res, cb){
    // try to get the user here, based on cookie, Authentication header, etc
    if (username === 'Pierre' && password === 'Collateur'){
      return cb(null, {username : "Pierre", twitter_handle : "@Percolator"});
      // user object will be available in req.authenticated in all methods
    } else {
      return cb(true);  // Percolator will 401 for you
    }
  },
  GET : function(req, res){
    res.object({youAre : req.authenticated}).send();
  }
});
fetch(req, res, cb)

Implementing fetch() on a handler object is an unnecessary but sometimes useful way to specify how 404s will be determined, so you don't need to write the same 404-handling code in all methods that you support. If you implement fetch(), fetch can do the 404-ing for you, before your regular handler methods are even run. In the event of a 404, a call to res.status.notFound() will be made automatically.

The fetch() function has three required parameters: the standard node req and res objects, and a callback (cb).

The cb argument is a callback that takes two parameters. The first parameter is an error object if any error occurred. The second parameter should be the object that was fetched from your data source for the current uri. It will automatically by added to the req object as req.fetched in any member resource methods that you implement.

The fetch() function will 404 for you automatically if you pass strict true to the callback ( cb) as the first parameter.

A handler object can also have a boolean property by the name of 'fetchOnPUT' that defaults to true, but allows you to turn fetch off for PUT, even though fetch usually applies to all methods. This is useful if you use PUT for creating new resources at the given URL, because a non-existent resource would otherwise cause a 404.

var module = new CRUDCollection({
  fetch : function(req, res, cb){
    // get some item from your data source, probably based on req.uri.child()
    cb(null, theObject);
  }
});
res.status

The status module is automatically attached to your resource handler at request time. It is just a bunch of helper functions for dealing with response statuses.

This is an important module because building great APIs requires excellent and consistent error and status reporting.

To understand what the codes mean, please refer to http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.

Usage
These methods generally set the HTTP status and end the response, so in general you should not expect to write more to the response after these. If a response body makes sense, it will generally be written automatically. For clarity, it's recommended that when you call one of these functions, you call it with return in front of it. Here's an example:
server.route('/', {  GET : function(req, res){
                              return res.status.redirect('/someOtherUrl');
                            }});

Here are the functions that it makes available in your method handler:

Redirect scenarios
res.status.created(redirectUrl);
This method is used for HTTP STATUS 201 scenarios when the server has just created a resource successfully so that the server can tell the client where to find it. It sets the status to 201 and sets the 'Location' header to the redirectUrl.
res.status.movedPermanently(redirectUrl);
This method is used for HTTP STATUS 301 scenarios where a resource has been permanently moved somewhere else so the server can tell the client where to find it. It sets the status to 301 and sets the 'Location' header to the redirectUrl.
res.status.redirect(redirectUrl);
This is just an alias of movedPermanently()
Success responses

"200 OK" statuses are the default, so you don't need to specify those explicitly.

201 Created statuses are described in the redirect section above.

res.status.accepted();
Used to indicate that a response has been accepted, but not yet processed, this response will emit a "202 Accepted" status.
res.status.noContent();
Used to indicate that a request was successful, but there's no body to return (for example, a successful DELETE). This response will emit a "204 No Content" status.
req.status.resetContent();
Used to indicate that a request was sucessful so a related UI (usually a form) should clear its content. This response will emit a "205 Reset Content" status.
Error Scenarios
All of the error scenarios are handled similarly and attempt to show a response body that indicates the error that occurred as well. The status code will be set on the response as well as in that response body. All of these methods additionally take a single parameter where additional detail information can be added. For example:
server.route('/', {  GET : function(req, res){
                              return res.status.internalServerError('The server is on fire.');
                            }});
Output:
{"type":500,"message":"Internal Server Error","detail":"The server is on fire"}
Error response methods:
res.status.badRequest([detail])
{"type":400,"message":"Bad Request"}
res.status.unauthenticated([detail])
{"type":401,"message":"Unauthenticated"}
res.status.forbidden([detail])
{"type":403,"message":"Forbidden"}
res.status.notFound([detail])
{"type":404,"message":"Not Found"}
res.status.methodNotAllowed([detail])
{"type":405,"message":"Method Not Allowed"}
res.status.notAcceptable([detail])
{"type":406,"message":"Not Acceptable"}
res.status.conflict([detail])
{"type":409,"message":"Conflict"}
res.status.gone([detail])
{"type":410,"message":"Gone"}
res.status.lengthRequired([detail])
{"type":411,"message":"Length Required"}
res.status.preconditionFailed([detail])
{"type":412,"message":"Precondition Failed"}
res.status.requestEntityTooLarge([detail])
{"type":413,"message":"'Request Entity Too Large"}
res.status.requestUriTooLong([detail])
{"type":414,"message":"Request URI Too Long"}
res.status.unsupportedMediaType([detail])
{"type":415,"message":"Unsupported Media Type"}
res.status.unprocessableEntity([detail])
{"type":422,"message":"'Unprocessable Entity"}
res.status.tooManyRequests([detail])
{"type":429,"message":"Too Many Requests"}
res.status.internalServerError([detail])
{"type":500,"message":"Internal Server Error"}
res.status.notImplemented([detail])
{"type":501,"message":"Not Implemented"}
res.status.badGateway([detail])
{"type":502,"message":"Bad Gateway"}
res.status.serviceUnavailable([detail])
{"type":503,"message":"Service Unavailable"}
res.status.gatewayTimeout([detail])
{"type":504,"message":"Gateway Timeout"}
req.uri

Each method you define has access to a 'uri' object instantiated from the uri of the current request. The uri object has a chainable/fluent interface that makes a number of methods available for for querying different aspects the current uri, and even modifying it to create new uris.

Most methods are named after different parts of the uri and allow you to read that part from the current uri if you don't pass any parameters, or they allow you to generate a new uri with a change to that part in the current uri if you do pass a parameter.

For the examples below, we'll use the following uri:

https://user:pass@subdomain.asdf.com/path/kid?asdf=1234#frag

Here are example usages:

General Usage:
server.route('/', {  GET : function(req, res){
                              res.object({self : req.uri}).send();
                                // --> {"self" : "http://example.com/currentUrl"}
                            }});
Api specifics:
req.uri.child([lastPart])
Setter/getter for the last part of a path:
      req.uri.child(); // returns "kid" 
      req.uri.child("grandkid"); // returns a new uri object with the uri 
                                 // https://user:pass@subdomain.asdf.com/path/kid/grandkid?asdf=1234#frag
   
see also: req.uri.parent(), req.uri.path().
req.uri.decode(encodedString);
Returns the decoded version of the input string using node's standard querystring.unescape().
      req.uri.decode('this%20is%20a%20test');  // returns "this is a test"
    
see also: req.uri.encode().
req.uri.encode(unencodedString);
Returns the encoded version of the input string using node's standard querystring.escape().
      req.uri.encode('this is a test'); // returns 'this%20is%20a%20test'
    
see also: req.uri.decode().
req.uri.hash([newHash])
Setter/getter for the url fragment/anchor/hash of a path.
      req.uri.hash(); // returns 'frag'
      req.uri.hash("blah"); // returns a new uri object with the uri
                            // https://user:pass@subdomain.asdf.com/path/kid/?asdf=1234#blah
req.uri.hostname([newHostname])
Setter/getter for the url hostname.
      req.uri.hostname(); // returns 'subdomain.asdf.com'
      req.uri.hostname("geocities.com"); // returns a new uri object with the uri
                            // https://user:pass@geocities.com/path/kid/?asdf=1234#frag
req.uri.parent();
Get the parent URI of the current URI. (This property is read-only).
      req.uri.parent();  // returns a new uri object with the uri
                         // https://user:pass@subdomain.asdf.com/path/?asdf=1234#frag
see also: req.uri.child(), req.uri.path().
req.uri.password([newPassword]);
Setter/getter for the password portion of the url.
      req.uri.password(); // returns 'pass'
      req.uri.password("newpass"); // returns a new uri object with the uri
                            // https://user:newpass@subdomain.asdf.com/path/kid/?asdf=1234#frag
see also: req.uri.username(),
req.uri.path([mixed]);
Setter/getter for the path portion of the url.
      req.uri.path(); // returns '/path/kid'
      req.uri.path("newpath"); // returns a new uri object with the uri
                               // https://user:newpass@subdomain.asdf.com/newpath

      // ALSO, req.uri.path() can take arrays of strings as input as well:
      req.uri.path(['qwer', '/asdf'], 'qwer/1234/', '/1234/'); 
                      // this returns a new uri object with the uri
                      // https://user:newpass@subdomain.asdf.com/qwer/asdf/qwer/1234/1234
    

Note: changing the path will remove the querystring and hash, since they rarely make sense on a new path.

see also: req.uri.child(), req.uri.parent().
req.uri.port([newPort]);
Setter/getter for the port portion of the url.
      req.uri.port(); // returns 80
      req.uri.port(8080); // returns a new uri object with the uri
                          // https://user:pass@subdomain.asdf.com:8080/path/kid/?asdf=1234#frag
req.uri.protocol([newProtocol]);
Setter/getter for the protocol portion of the url.
      req.uri.protocol(); // returns 'https'
      req.uri.protocol("http"); // returns a new uri object with the uri
                                // http://user:pass@subdomain.asdf.com/path/kid/?asdf=1234#frag
    
req.uri.query([mixed]);
Setter/getter for the querystring using javascript objects.
      req.uri.query(); // returns {asdf : 1234}
      req.uri.query(false); // returns a new uri object with the querystring-free uri
                            // https://user:pass@subdomain.asdf.com/path/kid#frag
      req.uri.query({spaced : 'space test'})
                                // returns a new uri object with the input object serialized
                                // and merged into the querystring like so:
                                // https://user:pass@subdomain.asdf.com/path/kid/?asdf=1234&spaced=space%20test#frag
    

NOTE: escaping and unescaping of applicable characters happens automatically. (eg " " to "%20", and vice versa)

NOTE: an input object will overwrite an existing querystring where they have the same names.

NOTE: an input object will remove an existing name-value pair where they have the same names and the value in the input name-value pair is null.

see also: req.uri.queryString(),

req.uri.queryString([newQueryString]);
Setter/getter for the querystring using a plain string representation. This is lower-level than $.req.query(), but allows complete control of the querystring.
      req.uri.queryString(); // returns asdf=1234  (notice there is no leading '?')
      req.uri.queryString("blah"); // returns a new uri object with a new querystring
                            // https://user:pass@subdomain.asdf.com/path/kid?blah#frag
    

NOTE: no escaping/unescaping of applicable characters will occur. This must be done manually.

see also: req.uri.query(),

req.uri.toJson();
Returns the json representation of the uri object, which is simply the uri as a string. The output is exactly the same as req.uri.toString(). This method is read-only.
      req.uri.toJson(); // returns
                        // "https://user:pass@subdomain.asdf.com/path/kid/?asdf=1234#frag"
req.uri.toString();
Returns the string representation of the uri object, which is simply the uri as a string. This method is read-only.
      req.uri.toString(); // returns
                        // "https://user:pass@subdomain.asdf.com/path/kid/?asdf=1234#frag"
req.uri.username([newUsername])
Setter/getter for the username portion of the url.
      req.uri.username(); // returns 'user'
      req.uri.username("newuser"); // returns a new uri object with the uri
                            // https://newuser:pass@subdomain.asdf.com/path/kid/?asdf=1234#frag
see also: req.uri.password(),
res.object
res.object is a function that takes an object for later output as json. It returns a "HyperJSON" object which is just a fluid (chained) interfaced for creating json with links easily.
toString()
returns a json string representation of the input object.
res.object({some : "test"}).toString();
// returns '{"some":"test"}'
toObject()
returns your input object, with any alterations you've made so far
res.object({some : "test"}).toObject();
// returns {some:"test"}
property(name, value)
adds a property to your input object
res.object({some : "test"}).property("also", "this").toObject();
// returns {some:"test",also:"this"}
send()
sends the input object as json in the http response and ends the response.
res.object({some : "test"}).send();
res.collection
This object is meant for creating and manipulating json "collections" which are useful in general when you want to output a list of things in json. It has all the same capibilities as res.object, but it also adds some additional capability for dealing with the individual items in the list. res.collection() is function that takes either an array or an object that will be treated as a hash. It will then add that list/hash to an _items property in a wrapper object.
// object/hash version
res.collection({someitem : "1234",someotheritem : 4567}).toObject();
// returns:
{
  _items : {
    someitem : "1234",
    someotheritem : "4567"
  },
  _links : {
    parent: {
      href: "http://example.com/api"
    }
  }
}
// array version
res.collection([{someitem : "1234"},{someotheritem : 4567}]).toObject();
// returns:
{
  _items : [
    {someitem : "1234"},
    {someotheritem : "4567"}
  ],
  _links : {
    parent: {
      href: "http://example.com/api"
    }
  }
}

each(cb)
runs a given callback on each item in the list. The list can be an array or an object that will be treated as a hash. The callback takes an individual item as its only parameter and replaces that item with the return value
res.collection({some : "test"}).each(function(item){return item + "2";}).toObject();
// returns...
{
  _items : {
      some: "test2"
  },
  _links : {
    parent: {
      href: "http://example.com/api"
    }
  }
}
res.collection([{some:"test"}]).each(function(item){return item + "2";}).toObject();
// returns...
{
  _items : [
    { some: "test2" }
  ],
  _links : {
    parent: {
      href: "http://example.com/api"
    }
  }
}
linkEach(rel, cb)
You can use this function for adding a link to each item in the collection. The rel argument is the rel property of the link. The rel property is usually a way of describing what this link will lead to. There are a bunch of predefined rel names or you can make your own. The cb argument is a callback that will be run on each item in the list and to create a link with the given rel from the callback's return value and add it to that item. The callback takes an individual item as its only parameter and replaces that item with the return value
res.collection({some : "test"}).linkEach('plustwo', function(item){return 'http://server.com/' + item + "2";}).toObject();
// returns...
{
  _items : {
    some: "test",
    _links : {
      plustwo : {
        href : 'http://server.com/test2'
      }
    }
  },
  _links : {
    parent: {
      href: "http://example.com/api"
    }
  }
}
res.collection([{some : "test"}]).linkEach('plustwo', function(item){return 'http://server.com/' + item['some'] + "2";}).toObject();
// returns...
{
  _items : [
    some: "test",
    _links : {
      plustwo : {
        href : 'http://server.com/test2'
      }
    }
  ],
  _links : {
    parent: {
      href: "http://example.com/api"
    }
  }
}
req.onJson([schema], cb)

req.onJson() is used for handling a request where the request body is json (normally a PUT or POST). It takes care of accepting the incoming stream, attempting to parse it to json, and automatically sending a 400 error if it does not parse.

The first, optional parameter, schema, is a json schema as a javascript object (not as a string). This schema object can be used to validate the incoming json object if desired. req.onJson will return an appropriate error to the user if the incoming json is not acceptable according to the schema.

The last (or only) parameter is a callback that is used if the incoming json parses successfully. The callback will take an 'error' and an 'object' parameter, where the error parameter will contain any error that was not automatically handled (like a network error), and the object parameter will contain any object that was deserialized from the incoming json (if possible).

// without a json schema...
req.onJson(function(err, obj){
  console.log('error: ', err);
  console.log('object: ', obj);
});
// with a json schema...
var schema = {
    properties : {
      "username" : {
        type : "string",
        required : true
      }
    };
req.onJson(schema, function(err, obj){
  console.log('error: ', err);
  console.log('object: ', obj);
});
req.onBody(cb)

req.onBody() is used for handling the streaming of non-json request bodies.

The only parameter is a callback that takes an error and a body parameter. The error parameter will be null unless an error occurred during streaming of the body, in which case it will contain the error. The body parameter will contain the request body as a string for use inside the callback.

req.onBody(function(err, body){
  console.log('error: ', err);
  console.log('body: ', body);  // body is just a string here
});

NOTE: If you're expecting json, then you should normally use req.onJson() instead of this method.

CRUDCollection

A CRUDCollection is a module that implements handlers for a json collection, and all its members. It allows you to implement just your CRUD (create, read, update, delete) logic for the collection and its members without having to deal with much HTTP/API logic at all.

Creating a CRUDCollection

Here's how it's constructed:

var CRUDCollection = require('percolator').CRUDCollection;
var module = new CRUDCollection({
  // implementation strategy goes here!
});

The module that is created will have two properties: handler and member. The handler property is a handler for a collection and can just be routed like this:

server.route('/myCollection', module.handler);

The wildcard property is a handler for the members of the collection and can be routed like this:

server.route('/myCollection/:memberid', module.wildcard);

list(req, res, cb)

Implement the list() function to provide the module with all the items that should show up in the collections GET view. CRUDCollection requires that either this function or collectionGET() is implemented to ensure that your collection is accessible via GET.

var module = new CRUDCollection({
  list : function(req, res, cb){
    // generate the list to return for the collection view.
    cb(null, [{some : "object"}, {some : "otherobject"}]);
  }
});
fetch(req, res, cb)

Implement the fetch() function to provide the GET handler for individual members of the collection. If you don't implement this, you'll need to implement memberGET() to be able to GET resources for individual members.

The fetch() function has three required parameters: the standard node req and res objects, and cb.

The cb argument is a callback that takes two parameters. The first parameter is an error object if any error occurred. The second parameter should be the object that was fetched from your data source for the current uri. It will automatically by added to the req object as req.fetched in any member resource methods that you implement.

The fetch() function will 404 for you automatically if you pass strict true to the callback ( cb) as the first parameter.

This function is also used for automatically determining if it should 404 for PUTs and DELETEs to the individual members as well.

var module = new CRUDCollection({
  fetch : function(req, res, cb){
    // get some item from your data source, probably based on req.uri.child()
    cb(null, theObject);
  }
});
create(req, res, object, cb)

The create() function is used for creating new resources via POST to the collection when the unique ID for the user (and its URI in general) are server-generated and NOT client-provided.

Implement create() to allow this kind of object creation. The CRUDCollection will do json parsing and validation for you (if you supply a schema or a createSchema ) and it will respond with appropriate error messages automatically in the case of failures.

The function will be passed the standard node req and res objects, the unserialized json object, and a callback. The callback takes no parameters and can optionally be called in the event of successful creation to respond with a 201 CREATED status. You have the option of not calling the callback at all and instead responding however you want.

var module = new CRUDCollection({
  create : function(req, res, object, cb){
    // store your object here
    cb();
  }
});
update(req, res, id, obj, cb)

The update() function is used for updating existing resources via PUT to the individual resource's existing URI.

Implement update() to allow resources to be updated. The CRUDCollection will handle the json parsing and validation for you ( if you supply a schema or an updateSchema) and it will respond with appropriate error messages automatically in the case of failures.

The function will be passed the standard node req and res objects, the 'id' at the end of the resource's URI, the object returned from fetch(), and a callback. The callback takes no parameters and can optionally be called in the event of successful update to respond with a 303 redirect. You have the option of not calling the callback at all and instead responding however you want.

var module = new CRUDCollection({
  update : function(req, res, id, obj, cb){
    // update your object here
    cb();
  };
});
upsert(req, res, id, obj, cb)

In general, "upsert" is short for "update if existing, and insert if not already existing".

The upsert() function is used for updating existing resources or creating resources if they don't already exist via PUT to the individual resource's URI. If the URI already exists, the resource there will be updated. If the URI doesn't already exist, the resource there will be created. You would use upsert() when the unique ID and URI of the resource is determined by the client and is NOT server-generated.

Implement upsert() to allow resources to be upserted. The CRUDCollection will handle the json parsing and validation for you ( if you supply a schema or both updateSchema AND createSchema ) and it will respond with appropriate error messages automatically in the case of failures.

The function will be passed the standard node req and res objects, the 'id' at the end of the resource's URI, the object returned from fetch(), and a callback. The callback takes no parameters and can optionally be called in the event of successful 'upsertion' to respond with a 303 redirect. You have the option of not calling the callback at all and instead responding however you want.

var module = new CRUDCollection({
  upsert : function(req, res, id, obj, cb){
    // upsert your object here
    cb();
  };
});
destroy(req, res, id, cb)

The destroy() function is used for removing existing resources via DELETE to the individual resource's URI. If the URI already exists, the resource there will be removed. If the URI doesn't exist, CRUDCollection will automatically respond with a 404 NOT FOUND status.

Implement destroy() to allow resources to be removed.

The function will be passed the standard node req and res objects, the 'id' at the end of the resource's URI, and a callback. The callback takes no parameters and can optionally be called in the event of successful 'removal' to respond with a 204 to indicate a success with no response body. You have the option of not calling the callback at all and instead responding however you want.

var module = new CRUDCollection({
  destroy : function(req, res, id, cb){
    // remove your object here
    cb();
  };
});
collectionGET(req, res)

The collectionGET() function is a way of completely handling the GET for the collection resource if you don't want the automatic handling that list() provides. This usually shouldn't be necessary. Also note that this is basically just an alias for crudCollection.handler.GET.

var module = new CRUDCollection({
  collectionGET : function(req, res){
    res.object({"make":"your own response"}).send();
  };
});
memberGET(req, res)

The memberGET() function is a way of completely handling the GET for member resources (items in the collection) if you don't want the automatic handling that fetch() provides. This usually shouldn't be necessary. Also note that this is basically just an alias for crudCollection.wildcard.GET.

var module = new CRUDCollection({
  memberGET : function(req, res){
    res.object({"make":"your own response"}).send();
  };
});
updateSchema

The updateSchema property is a way to specify a json schema object that will be used to validate json used for updating the resource. If you want the same validation for creating the resource as you want for updating it, you can just use the schema property instead.

The updateSchema object will also be used in the update link for the individual collection member resource to show developers and machines how an update is validated.


createSchema

The createSchema property is a way to specify a json schema object that will be used to validate json used for creating a resource. If you want the same validation for creating the resource as you want for updating it, you can just use the schema property instead.

The createSchema object will also be used in the create link in the collection resource to show developers and machines how a create (POST) is validated.


schema

The schema property is a way to specify a json schema object that will be used to validate json used for creating or updating a resource. If you want different validations for creating the resource than you do for updating it, you can use the specific createSchema and updateSchema properties instead.

The schema object will also be used in the create link in the collection resource and the update link in the individual collection member resource to show developers and machines how creates and updates are validated.


Advanced Routing

Hello World Refactored:

Percolator also allows a more advanced style of routing that lets you load your route-handling resources from external files instead.

  • With the "Hello World" example, we could instead move the handler into a file at the path ./resources/index.js. (First create a resources directory and then the index.js file in it, and then copying the handler logic into index.js like so:
  • exports.handler = {
      GET : function(req, res){
        res.end('Hello World!');
      }
    }
    
    We'll call files like that "resources" from now on.
  • Change your server.js to call routeDirectory() instead of server.route() like so:
  • var Percolator = require('Percolator');
    
    var server = new Percolator();
    server.routeDirectory(__dirname + '/resources', '/api', function(err){
      if (!!err) {console.log(err);}
      server.listen(function(err){
        console.log('server is listening on port ', server.port);
      });
    });
    
  • Run the server: node server.js

  • See your "Hello World" output at http://localhost:3000/api . You should see the same output as the original "Hello World" example.

This worked because index.js is the reserved filename for responding to '/'. If you want to respond to uris like '/hello', you can simply drop a hello.js in that folder (that exports a similar handler) and it will respond to http://localhost:3000/api/hello . The advantage to this way of routing is that all your resources can be in separate files so that your project can be more easily organized.