npm install percolator
make test
server.js
in your project directory, and copy this code below into it:var Percolator = require('percolator').Percolator; var server = new Percolator(); server.route('/', { GET : function(req, res){ res.object({message : 'Hello World!'}).send(); }}); 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/ and be completely floored by the greatest API of all time. Or not.
The output json look like this:{ message: "Hello World!", _links: { self: { href: "http://localhost:3000/" } } }
You'll notice that Percolator automatically adds a link to the document itself. This is because it tries to help you create a surfable "Hypermedia" API where every endpoint is accessible from a link in another endpoint, and where everything that can be done with the API is described in the API itself.
See the spec for the hyper+json media type for more details on how Percolator outputs its links by default.
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.
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.
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.
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.
server.listen(function(err){ console.log('server is listening on port ', server.port); });
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(); });
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); });
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 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.
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.
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.
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.
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.
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(); } });
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(); } });
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); } });
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.
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:
"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.
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"}
{"type":400,"message":"Bad Request"}
{"type":401,"message":"Unauthenticated"}
{"type":403,"message":"Forbidden"}
{"type":404,"message":"Not Found"}
{"type":405,"message":"Method Not Allowed"}
{"type":406,"message":"Not Acceptable"}
{"type":409,"message":"Conflict"}
{"type":410,"message":"Gone"}
{"type":411,"message":"Length Required"}
{"type":412,"message":"Precondition Failed"}
{"type":413,"message":"'Request Entity Too Large"}
{"type":414,"message":"Request URI Too Long"}
{"type":415,"message":"Unsupported Media Type"}
{"type":422,"message":"'Unprocessable Entity"}
{"type":429,"message":"Too Many Requests"}
{"type":500,"message":"Internal Server Error"}
{"type":501,"message":"Not Implemented"}
{"type":502,"message":"Bad Gateway"}
{"type":504,"message":"Gateway Timeout"}
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#fragHere are example usages:
server.route('/', { GET : function(req, res){ res.object({self : req.uri}).send(); // --> {"self" : "http://example.com/currentUrl"} }});
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#fragsee also: req.uri.parent(), req.uri.path().
req.uri.decode('this%20is%20a%20test'); // returns "this is a test"see also: req.uri.encode().
req.uri.encode('this is a test'); // returns 'this%20is%20a%20test'see also: req.uri.decode().
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(); // 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(); // returns a new uri object with the uri // https://user:pass@subdomain.asdf.com/path/?asdf=1234#fragsee also: req.uri.child(), req.uri.path().
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#fragsee also: req.uri.username(),
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(); // 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(); // 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(); // 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(); // 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 // "https://user:pass@subdomain.asdf.com/path/kid/?asdf=1234#frag"
req.uri.toString(); // returns // "https://user:pass@subdomain.asdf.com/path/kid/?asdf=1234#frag"
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#fragsee also: req.uri.password(),
res.object({some : "test"}).toString(); // returns '{"some":"test"}'
res.object({some : "test"}).toObject(); // returns {some:"test"}
res.object({some : "test"}).property("also", "this").toObject(); // returns {some:"test",also:"this"}
res.object({some : "test"}).link("somerel", "http://some.example.com").toObject(); // returns {some:"test",_links:{somerel:{href:"http://some.example.com"}}}
res.object({some : "test"}).link("somerel", "http://some.example.com", {method:"POST", schema:{}}).toObject(); // returns {some:"test",_links:{somerel:{href:"http://some.example.com",method:"POST",schema:{}}}}
res.object({some : "test"}).send();
// 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" } } }
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" } } }
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() 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() 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.
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.
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);
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"}]); } });
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); } });
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(); } });
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(); }; });
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(); }; });
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(); }; });
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(); }; });
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(); }; });
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.
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.
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.
Percolator also allows a more advanced style of routing that lets you load your route-handling resources from external files instead.
./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.
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.