Building a Node.js Clientfor Your REST+JSON API
Les Hazlewood @lhazlewoodCTO, Stormpath stormpath.com
.com• User Management and Authentication
API• Security for your applications• User security workflows• Security best practices• Developer tools, SDKs, libraries
Overview• Resources• Public / Private API• Proxy Design• Active Record• Fluent API• Configuration• Caching• Authentication• Pluggability• Lessons Learned
HATEOAS
• Hypermedia
• As
• The
• Engine
• Of
• Application
• State
Learn more at Stormpath.com
Resources
• Nouns, not verbs• Coarse-grained, not fine-grained• Support many use cases• Globally unique HREF
Learn more at Stormpath.com
Collection Resource
• Example: /applications
• First class resource w/ own properties:• offset• limit• items• first, next, previous, last• etc
• items contains instance resources
Learn more at Stormpath.com
Instance Resource
• Example:/applications/8sZxUoExA30mP74
• Child of a collection• RUD (no Create - done via parent collection)
Learn more at Stormpath.com
Resource
var util = require('util');
function Resource(...) { ... }util.inherits(Resource, Object);
someResource.href
Learn more at Stormpath.com
Instance Resourcefunction InstanceResource(...) {...}util.inherits(InstanceResource, Resource);
anInstanceResource.save(function (err, saved) { ...});
anInstanceResource.delete(function (err) { ...});
Learn more at Stormpath.com
Collection Resourcefunction CollectionResource(...) {...}util.inherits(CollectionResource, Resource);
aCollResource.each(function (item, callback) { ...}, function onCompletion(err) { ... });
aCollResource.eachSeriesaCollResource.mapaCollResource.filter... other async.js methods ...
Learn more at Stormpath.com
Example: ApplicationListapplications.each(function(app, callback){ console.log(app); callback();}, function finished(err) { if (err) console.log(‘Error: ‘ + err);});
Learn more at Stormpath.com
Encapsulation
• Public API• Internal/Private Implementations• Extensions
• Allows for change w/ minimal impacthttp://semver.org
Learn more at Stormpath.com
Encapsulation in practice
• Use an underscore prefix: _• Super clear code comments:
mark @public or @private• Public API docs: don’t display private
classes/methods/functions
Learn more at Stormpath.com
Public API
• All non-@private functions/vars• Builder methods (method chaining)• Object literals (config) is public too!
Learn more at Stormpath.com
Public prototypical OO Classes
• Client• ApiKey• Application• Directory• Account• Group• etc
Learn more at Stormpath.com
Classes with static helper methodsClient client = Clients.builder() ... .build();
• Create multiple helper classesseparation of concerns
Learn more at Stormpath.com
Builder methods (method chaining)
client.getApplications() .where(name).startsWith(‘*foo’) .orderBy(name).asc() .limit(10) .execute(function (err, apps){ ... });
clients.getApplications() ApplicationRequestBuilder
Single Responsibility Principle!
Learn more at Stormpath.com
Private API
• Implementations + SPI interfaces• Builder implementations• Implementation Plugins
Learn more at Stormpath.com
Resource Implementations• Create a base Resource class:• Property manipulation methods• Dirty checking• Reference to DataStore• Lazy Loading
• Create base InstanceResource and CollectionResource implementations
• Extend from InstanceResource or CollectionResource
Learn more at Stormpath.com
Resourcevar utils = require(’utils');
function Resource(data, dataStore) { var DataStore = require('../ds/DataStore'); if (!dataStore && data instanceof DataStore){ dataStore = data; data = null; } data = data || {};
for (var key in data) { if (data.hasOwnProperty(key)) { this[key] = data[key]; } }
var ds = null; //private var, not enumerable Object.defineProperty(this, 'dataStore', { get: function getDataStore() { return ds; }, set: function setDataStore(dataStore) { ds = dataStore; } }); if (dataStore) { this.dataStore = dataStore; }}utils.inherits(Resource, Object);
module.exports = Resource;
Learn more at Stormpath.com
InstanceResourcevar utils = require(‘utils');var Resource = require('./Resource');
function InstanceResource() { InstanceResource.super_.apply(this, arguments);}utils.inherits(InstanceResource, Resource);
InstanceResource.prototype.save = function saveResource(callback) { this.dataStore.saveResource(this, callback);};
InstanceResource.prototype.delete = function deleteResource(callback) { this.dataStore.deleteResource(this, callback);};
Learn more at Stormpath.com
Applicationvar utils = require(‘utils');var InstanceResource = require('./InstanceResource');
function Application() { Application.super_.apply(this, arguments);}utils.inherits(Application, InstanceResource);
Application.prototype.getAccounts = function getApplicationAccounts(/* [options,] callback */) { var self = this; var args = Array.prototype.slice.call(arguments); var callback = args.pop(); var options = (args.length > 0) ? args.shift() : null;
return self.dataStore.getResource(self.accounts.href, options, require('./Account'), callback);};
Learn more at Stormpath.com
Account JSON Resource{ “href”: “https://api.stormpath.com/v1/accounts/x7y8z9”, “givenName”: “Tony”, “surname”: “Stark”, …, “directory”: { “href”: “https://api.stormpath.com/v1/directories/g4h5i6” }}
Learn more at Stormpath.com
Proxy PatternString href = “https://api.stormpath.com/v1/....”;
client.getAccount(href, function(err, acct) { if (err) throw err;
account.getDirectory(function(err, dir) { if (err) throw err; console.log(dir); });
});
Learn more at Stormpath.com
Component Architectureaccount .save()
DataStore
Learn more at Stormpath.com
Component Architectureaccount .save()
CacheManager
DataStore
Learn more at Stormpath.com
Component Architectureaccount .save()
CacheManager
DataStore
CacheCacheCache
Learn more at Stormpath.com
Component Architectureaccount .save()
RequestExecutorCacheManager
DataStore
CacheCacheCache
Learn more at Stormpath.com
Component Architectureaccount .save()
RequestExecutor
AuthenticationStrategy
CacheManager
DataStore
RequestAuthenticator
CacheCacheCache
Learn more at Stormpath.com
Component Architectureaccount
API Server
.save()
RequestExecutor
AuthenticationStrategy
CacheManager
DataStore
RequestAuthenticator
CacheCacheCache
Learn more at Stormpath.com
Component Architectureaccount
API Server
.save()
RequestExecutor ResourceFactory JSON Resource
AuthenticationStrategy
CacheManager
DataStore
RequestAuthenticator
CacheCacheCache
Learn more at Stormpath.com
Cachingvar cache = cacheManager.getCache(regionName);
cache.ttl //time to livecache.tti //time to idlecache.get(href, function(err, obj) { ...});
Learn more at Stormpath.com
Cachingclient.getAccount(href, function(err, acct) {...});
// in the DataStore:var cache = cacheManager.getCache(‘accounts’);cache.get(href, function(err, entry) { if (err) return callback(err); if (entry) { ... omitted for brevity ... return callback(entry.value); } //otherwise, cache miss – execute a request: requestExecutor.get(href, function(err, body) { //1. cache body //2. convert to Resource instance //3. invoke callback w/ instance }}
Learn more at Stormpath.com
Queriesaccount.getGroups(function(err,groups){...});//results in a request to://https://api.stormpath.com/v1/accounts/a1b2c3/groups
• What about query parameters?
Learn more at Stormpath.com
Queriesaccount.getGroups( { name: ‘foo*’, description: ‘*test*’, orderBy: ‘name desc’, limit: 100 }, function onResult(err, groups) { ... });
//results in a request to:
https://api.stormpath.com/v1/accounts/a1b2c3/groups? name=foo*&description=*test*&orderBy=name%20desc&limit=100
Learn more at Stormpath.com
Queriesaccount.getGroups().where() .name().startsWith(“foo”) .description().contains(“test”) .orderBy(“name”).desc() .limitTo(100));//results in a request to:
https://api.stormpath.com/v1/accounts/a1b2c3/groups? name=foo*&description=*test*&orderBy=name%20desc&limit=100
Learn more at Stormpath.com
Authentication• Favor a digest algorithm over HTTP Basic• Prevents Man-in-the-Middle attacks (SSL won’t guarantee
this!)
• Also support Basic for environments that require it (Dammit Google!)• ONLY use Basic over SSL
• Represent this as an AuthenticationScheme to your Client / RequestExecutor
Learn more at Stormpath.com
Authentication• AuthenticationScheme.SAUTHC1• AuthenticationScheme.BASIC• AuthenticationScheme.OAUTH10a• ... etc ...
Client client = new stormpath.Client({ //defaults to sauthc1 authcScheme: ‘basic’});
Client/RequestExecutor uses a Sauthc1RequestAuthenticator or BasicRequestAuthenticator or OAuth10aRequestAuthenticator, etc.
Learn more at Stormpath.com
Plugins
• Plugins or Extensions module• One sub-module per plugin• Keep dependencies to a minimum
plugins/|- request/|- foo/
Learn more at Stormpath.com
Lessons Learned
• Recursive caching if you support resource expansion
• Dirty checking logic is not too hard, but it does add complexity. Start off without it.
Learn more at Stormpath.com
Lessons Learned: Promisesvar promise = account.getGroups();
promise.then(function() { //called on success}, function() { //called on error}, function() { //called during progress});
Learn more at Stormpath.com
Lessons Learned: async.jsasync.waterfall([ function(callback){ callback(null, 'one', 'two'); }, function(arg1, arg2, callback){ // arg1 now equals 'one' and arg2 now equals 'two' callback(null, 'three'); }, function(arg1, callback){ // arg1 now equals 'three' callback(null, 'done'); }], function (err, result) { // result now equals 'done' });
Learn more at Stormpath.com
Code
$ git clone https://github.com/stormpath/stormpath-sdk-node.git
$ cd stormpath-sdk-node
$ npm install$ grunt
Learn more at Stormpath.com
Thank You!
• [email protected]• Twitter: @lhazlewood• http://www.stormpath.com
Learn more at Stormpath.com