Build a Node.js Client for Your REST+JSON API

Preview:

DESCRIPTION

In this presentation, Les Hazlewood - Stormpath CTO and Apache Shiro PMC Chair - will share all of the golden nuggets learned while designing, implementing and supporting a Node.js Client purpose-built for a real-world REST+JSON API. Further reading: http://www.stormpath.com/blog Stormpath is a user management and authentication service for developers. By offloading user management and authentication to Stormpath, developers can bring applications to market faster, reduce development costs, and protect their users. Easy and secure, the flexible cloud service can manage millions of users with a scalable pricing model.

Citation preview

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

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

Translating to Code

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

Design!

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

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

Usage Paradigm

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

Proxy Pattern

Learn more at Stormpath.com

Component Architecture

Learn more at Stormpath.com

Component Architectureaccount .save()

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

Caching

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

Queries

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

Queries

Use a Fluent API!

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

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

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

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!

• les@stormpath.com• Twitter: @lhazlewood• http://www.stormpath.com

Learn more at Stormpath.com

Recommended