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.
- Project Structure
- Variable Declarations
- Custom Model Methods
- Observers and Remote Hooks
- Model Definition Layout
- Debugging
- Remote Methods
- Promises
- Testing
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.jsmodule.exports=function(app){functionroleResolverA(){// Role resolver definition for Role A}functionroleResolverB(){// Role resolver definition for Role B}functionroleResolverC(){// Role resolver definition for Role C}// etc...}/* Prefer */// roles.jsmodule.exports=function(app){require('roles/roleA')(app);require('roles/roleB')(app);require('roles/roleC')(app);}// roles/roleA.jsmodule.exports=function(app){// Role resolver definition for Role A}// roles/roleB.js// etc...Keep variable names in alphabetical order
Why?: Easier to read and easier to find what you are looking for.
/* Avoid */varfs=require('fs');varuuid=require('uuid');var_=require('lodash');varexpress=require('express');/* Recommeded */var_=require('lodash');varexpress=require('express');varfs=require('fs');varuuid=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 */varfs=require('fs');varsomeConstant=require('./constants/some-constant');var_=require('lodash');varanotherConstant=require('./constants/another-constant');varexpress=require('express');/* Prefer */varANOTHER_CONSTANT=require('./constants/another-constant');varSOME_CONSTANT=require('./constants/some-constant');var_=require('lodash');varexpress=require('express');varfs=require('fs');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;functionaddFilters(){}functioncreateClient(){}functionlistUserClients(){}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);functionaddClientId(){// Trying to look for an access token here will break server-side}/* Recommended */Model.beforeRemote('create',addClientId);Model.beforeRemote('updateAttributes',addClientId);functionaddClientId(){// We can look for an access token because this only triggers for REST}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 */vardebug=require('debug');// example onlyvarasync=require('async');//example onlymodule.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 */varasync=require('async');//example onlyvardebug=require('debug');// example onlymodule.exports=function(Model){// Remote MethodsModel.remoteMethod('doSomething',doSomethingConfig());Model.remoteMethod('doOneThing',doOneThingConfig());// ObserversModel.observe('count',doAnotherthing());Model.observe('find',doSomethingElse());// Remote HooksModel.beforeRemote('remoteMethod1',beforeRemoteMethod1Config());Model.afterRemote('remoteMethod2',afterRemoteMethod2Config());// MethodsfunctionafterRemoteMethod2Config(){// stuff}functionbeforeRemoteMethod1Config(){// stuff}functiondoOneThing(){// stuff}functiondoAnotherThing(){// stuff}functiondoOneThingConfig(){// stuff}functiondoAnotherThingConfig(){// stuff}// etc...}/** * Best - model definition only describes the model and no implementation * of the components. This leads to less merge conflicts */varasync=require('async');//example onlyvardebug=require('debug');// example onlymodule.exports=function(Model){// Remote MethodsModel.remoteMethod('doSomething',require('./Model/doSomethingConfig'));Model.remoteMethod('doOneThing',require('./Model/doOneThingConfig'));// ObserversModel.observe('count',require('./Model/doAnotherThing'));Model.observe('find',require('./Model/beforeFind'));// Remote HooksModel.beforeRemote('remoteMethod1',require('./Model/remoteMethod1'));Model.afterRemote('remoteMethod2',require('./Model/remoteMethod2'));}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 */vardebug=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.jsvardebug=require('debug')('ModelA');// ModelB.jsvardebug=require('debug')('ModelA');// from terminal:// $ DEBUG=ModelA,ModelB node ./* Recommended */// ModelA.jsvardebug=require('debug')('models:custom:ModelA');// ModelB.jsvardebug=require('debug')('models:custom:ModelB');// from terminal:// $ DEBUG=models:custom:* node .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.
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){throwerr;}cb(null,result);})/* Avoid */modelInstance.save().then(function(result){cb(null,result);}).catch(function(err){throwerr;})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 inreturnsomePromise()}).then(function(result){// Put left foot outreturnsomePromise()}).then(function(result){// Put left foot inreturnsomePromise()}).then(function(result){// Shake it all aboutreturnsomePromise()}).catch(function(err){throwerr;})/* Prefer */modelInstance.save().then(putLeftFootIn).then(putLeftFootOut).then(putLeftFootIn).then(shakeItAllAbout).catch(handleError);functionputLeftFootIn(){// Put left foot inreturnsomePromise;}// ... etcWhen 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){returndone(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(){varerrorMessage='';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;returndone(err);});});});