Skip to content

Coding style suggestions and general 'gotchas' when working with Strongloop/Loopback

Notifications You must be signed in to change notification settings

NextFaze/Loopback-Style-Guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 

Repository files navigation

This is very work-in-progress but, in general, a central point to keep track of Loopback's various idiosyncrasies and define some best practices when working with Loopback.

  1. Project Structure
  2. Variable Declarations
  3. Custom Model Methods
  4. Observers and Remote Hooks
  5. Model Definition Layout
  6. Debugging
  7. Remote Methods
  8. Promises
  9. Testing

Project Structure

Prefer more, smaller files.

Why?: Mostly common sense.

Why?: It's far easier to navigate and maintain and results in less git conflicts when compared to large, monolithic files.

/* Avoid */
// roles.js
module.exports = function(app) {
  function roleResolverA() {
    // Role resolver definition for Role A
  }

  function roleResolverB() {
    // Role resolver definition for Role B
  }

  function roleResolverC() {
    // Role resolver definition for Role C
  }

  // etc...
}
/* Prefer */
// roles.js
module.exports = function(app) {
  require('roles/roleA')(app);
  require('roles/roleB')(app);
  require('roles/roleC')(app);
}

// roles/roleA.js
module.exports = function(app) {
  // Role resolver definition for Role A
}

// roles/roleB.js
// etc...

Variable Declarations

Keep variable names in alphabetical order

Why?: Easier to read and easier to find what you are looking for.

/* Avoid */
var fs = require('fs');
var uuid = require('uuid');
var _ = require('lodash');
var express = require('express');
/* Recommeded */
var _ = require('lodash');
var express = require('express');
var fs = require('fs');
var uuid = require('uuid');

Keep constants separate from main variables and use "screaming snake case"

Why?: Keeping them separate makes it easy to see what constants are available for use in the file and which are missing

Why?: External constants are constants and should be addressed as such (general programming convention)

Why?: Required constants mimic enums from other languages

/* Avoid */
var fs = require('fs');
var someConstant = require('./constants/some-constant');
var _ = require('lodash');
var anotherConstant = require('./constants/another-constant');
var express = require('express');
/* Prefer */
var ANOTHER_CONSTANT = require('./constants/another-constant');
var SOME_CONSTANT = require('./constants/some-constant');

var _ = require('lodash');
var express = require('express');
var fs = require('fs');

Custom Model Methods

Keep custom model methods at the top of the file and refer to named functions defined later, rather than anonymous functions.

Why?: It keeps all model definitions together in a neat list

Why?: It makes it easy to see what methods can be called on a model/instance and what observers/remote hooks have been added to the model.

Why?: It also encourages function re-use when defining multiple similar remote hooks.

/* Avoid */
Model.beforeRemote('find', function() {
  // Add some mandatory filters
});

Model.prototype.createClient = function() {
  // Custom instance method
};

Model.listUserClients = function() {
  // Custom static method
}
/* Recommended */
Model.beforeRemote('find', addFilters);
Model.prototype.createClient = createClient;
Model.listUserClients = listUserClients;

function addFilters() {

}

function createClient() {

}

function listUserClients() {

}

Observers and Remote Hooks

Prefer Model.beforeRemote over Model.observe unless you are sure that you always want the functionality to trigger.

Why?: beforeRemote will only trigger if a method is used from an API endpoint.

Why?: Methods are often called from within server-side model definitions and API specific triggers often want to be avoided.

/* Avoid */
Model.observe('after save', addClientId);

function addClientId() {
  // Trying to look for an access token here will break server-side
}
/* Recommended */
Model.beforeRemote('create', addClientId);
Model.beforeRemote('updateAttributes', addClientId);

function addClientId() {
  // We can look for an access token because this only triggers for REST
}

Model Definition Layout

Limit custom methods/endpoints/observers to single-line definitions that reference functions to be hoisted and keep them at the top of the file. Try to limit the Model.js file to only declarations of remote methods/observers etc and keep the implementation of these to separate files to be require()d.

Why?: It just keeps things organized Why?: Separate files leads to fewer merge conflicts and less daunting files Why?: It allows the reader to quickly read the top few lines of the model definition and answer important questions:

  • What are the custom endpoints I can call?
  • What requests are being intercepted?
  • What methods are being intercepted?
  • What other custom methods etc can I call?
/* Avoid */
var debug = require('debug'); // example only
var async = require('async'); //example only

module.exports = function(Model) {

  Model.doOneThing = function(input, cb) {
    // ... do one thing
  }

  Model.remoteMethod('doSomething', {
    http: {
      //config here
    },
    accepts: [
      // config here
    ],
    returns: {
      //more config here
    }
  })

  Model.doSomething = function(input, cb) {
    // ... do something
  }

  Model.observe('count', function(ctx, next) {
    // ...
  });

  Model.remoteMethod('doOneThing', {
    http: {
      // config
    },
    accepts: {
      // config
    },
    returns: {
      // config
    }
  });
}
/* Better */
var async = require('async'); //example only
var debug = require('debug'); // example only

module.exports = function(Model) {
  // Remote Methods
  Model.remoteMethod('doSomething', doSomethingConfig());
  Model.remoteMethod('doOneThing', doOneThingConfig());

  // Observers
  Model.observe('count', doAnotherthing());
  Model.observe('find', doSomethingElse());

  // Remote Hooks
  Model.beforeRemote('remoteMethod1', beforeRemoteMethod1Config());
  Model.afterRemote('remoteMethod2', afterRemoteMethod2Config());

  // Methods
  function afterRemoteMethod2Config() {
    // stuff
  }
  function beforeRemoteMethod1Config() {
    // stuff
  }
  function doOneThing() {
    // stuff
  }
  function doAnotherThing() {
    // stuff
  }
  function doOneThingConfig() {
    // stuff
  }
  function doAnotherThingConfig() {
    // stuff
  }
  // etc...
}
/**
 *  Best - model definition only describes the model and no implementation
 *  of the components. This leads to less merge conflicts
 */

var async = require('async'); //example only
var debug = require('debug'); // example only

module.exports = function(Model) {
  // Remote Methods
  Model.remoteMethod('doSomething', require('./Model/doSomethingConfig'));
  Model.remoteMethod('doOneThing', require('./Model/doOneThingConfig'));

  // Observers
  Model.observe('count', require('./Model/doAnotherThing'));
  Model.observe('find', require('./Model/beforeFind'));

  // Remote Hooks
  Model.beforeRemote('remoteMethod1', require('./Model/remoteMethod1'));
  Model.afterRemote('remoteMethod2', require('./Model/remoteMethod2'));
}

Debugging

Prefer the debug module over console.log

Why?: It's easy to forget to remove console.logs and commit them to a project

Why?: It allows for filtering of specific debug information that you want to see

Why?: Excessive console.logs can flood the app output and make debugging difficult

/* Avoid */
module.exports = function(MyModel) {
  console.log('Model has been loaded')
}
/* Recommended */
var debug = require('debug')('MyModel');

module.exports = function(MyModel) {
  debug('Model has been loaded');
}
// from terminal:
// $ DEBUG=MyModel node .

Use modular debug names separated with a colon

Why?: It allows broad stroke debugging using *

/* Recommended */

// ModelA.js
var debug = require('debug')('ModelA');

// ModelB.js
var debug = require('debug')('ModelA');

// from terminal:
// $ DEBUG=ModelA,ModelB node .
/* Recommended */

// ModelA.js
var debug = require('debug')('models:custom:ModelA');

// ModelB.js
var debug = require('debug')('models:custom:ModelB');

// from terminal:
// $ DEBUG=models:custom:* node .

Remote Methods

Be careful with remote hook observers on certain properties. Instance methods may require prototype.{method} in the observer:

/* Will NOT work */
Model.beforeRemote('updateAttributes', interceptUpdate);
/* WILL work */
Model.beforeRemote('prototype.updateAttributes', interceptUpdate);

See this table for a list of the default observers and remote hooks.

Promises

Prefer promises over callbacks

Why?: Promises generally lead to neater, more readable code

Why?: It's easier to separate out components in a promise chain into separate functions

NOTE Not all built in Loopback methods are promisified, and some have broken promises see here. In cases where promises are not supported, prefer to add custom promises over using callbacks for the reasons stated above

/* Avoid */
modelInstance.save(function(err, result) {
  if(err) {
    throw err;
  }
  cb(null, result);
})
/* Avoid */
modelInstance.save()
.then(function(result) {
  cb(null, result);
})
.catch(function(err) {
  throw err;
})

Prefer named functions over long anonymous functions in promise chains

Why?: It's much easier to read and allows other developers to quickly scan and get an overall sense of what is happening in the promise chain

/* Avoid */
modelInstance.save()
.then(function(result) {
  // Put left foot in
  return somePromise()
})
.then(function(result) {
  // Put left foot out
  return somePromise()
})
.then(function(result) {
  // Put left foot in
  return somePromise()
})
.then(function(result) {
  // Shake it all about
  return somePromise()
})
.catch(function(err) {
  throw err;
})
/* Prefer */
modelInstance.save()
.then(putLeftFootIn)
.then(putLeftFootOut)
.then(putLeftFootIn)
.then(shakeItAllAbout)
.catch(handleError);

function putLeftFootIn() {
  // Put left foot in
  return somePromise;
}
// ... etc

Testing

When using supertest-as-promised the errors returned aren't always particularly helpful. A test like the following might return something like 422 Unprocessable Entity before the expect(204) instead of the error message generated by the server.

describe('POST /api/trainers/:id/updateStripeBankAccount', function() {
  it('should update a Stripe bank account', function(done) {
    json('post', '/api/trainers/' + trainer.id + '/updateStripeBankAccount')
    .set('Authorization', trainer.token.id)
    .send(testData.stripeBankAccountUpdate)
    .expect(204)
    .then(function(res) {
      done();
    })
    .catch(function(err) {
      return done(err);
    });
  });
});

If we declare a variable like errorMessage we can capture any errors and add them to the final error message like so

describe('POST /api/trainers/:id/updateStripeBankAccount', function() {
  var errorMessage = '';
  it('should update a Stripe bank account', function(done) {
    json('post', '/api/trainers/' + trainer.id + '/updateStripeBankAccount')
    .set('Authorization', trainer.token.id)
    .send(testData.stripeBankAccountUpdate)
    .expect(function(res) {
      if(res.body.error){
        errorMessage = res.body.error.message;
      }
    })
    .expect(204)
    .then(function(res) {
      done();
    })
    .catch(function(err) {
      err.message = err.message + ': ' + errorMessage;
      return done(err);
    });
  });
});

About

Coding style suggestions and general 'gotchas' when working with Strongloop/Loopback

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published