TIL Thursday: NetSuite Professional Services' Best Practices

As you'll probably know, we have a commerce professional services team. They're responsible for a number of things, not least coming up with custom solutions that fulfill our customers' needs.

They work closely with our commerce applications team (the group of people who are in charge of developing the core SuiteCommerce products) to try and ensure there is alignment and to see if there's anything to be learned from each other's experiences.

In order to ensure effective communication and that the code is itself of the highest quality, the PS team produced a series of best practices and guidelines that they have implemented across their team. When working in a team on a development project, it's crucial that everyone approaches solving problems the same way, can understand each other's work, and follows the same conventions.

The result of this was a standardization of their approaches to create a series of best practices for creating customizations.

And we've decided that we can share them with you.

A lot of this stuff should be pretty intuitive because I've covered a number of our best practices before, for example, when it comes to extending JavaScript. We also talk about how to approach customizations in our documentation.

Let's consider this a cheatsheet or summary as I want to keep it brief and concise. I'm also aware that some of the tutorials and code snippets I've provided over the past years infringe on these, so I'm going to try and keep myself more inline with them too.

Modules

Modules are how we segment our code in SuiteCommerce Advanced. A module represents a self-contained set of functionality. It should have a clear, limited scope. Broadly speaking, there are four types:

  1. Application — a high-level collection of features; eg Shopping, My Account and Checkout
  2. Framework — general logic and base classes; eg Backbone
  3. Feature — a specific area of functionality; eg the store locator
  4. Utility — common functionality used across multiple modules; eg Utilities

As developers, you'll likely only write feature modules for your customizations.

Attributes of a Quality Module

Customizations should only be made when what the customer wants cannot be achieved through existing means, such as configuration or the site management tools. In other words, you need to be sure that what the customer wants requires the introduction of new functionality, ie a new feature module.

When it is established that a new feature is required, there are four principles that you should follow to ensure your module is of a high quality:

  1. Functional — the customization should meet the business requirements set by the customer
  2. Maintainable — the customization should be easy for other developers to understand, maintain, improve and fix
  3. Upgradable — the customization should not require work (or, at least, very minimal work) when the underlying framework is updated
  4. Reusable — the customization should be designed so that it requires no work to be implemented on another site

Obviously, these principles are catered towards a professional services team, especially the last one. However, it is still good practice: it will teach you to write clean code. Furthermore, don't forget that when you migrate to new releases, you're effectively reseting your site to a 'default' position where you'll have to slowly add your customizations back in.

Module Naming

Feature names must be capitalized, only containing letters and dots, and singular.

If you're creating a new module, it should clearly state what the feature is and what it's achieving. If you're extending an extending module, it should have the original module's name plus what customization they are making. Here are some good and bad examples:

NameGood/BadExplanation
DatePicker, ShippingRestrictions, PickupInStoreGoodFollows convention
datePicker, SHIPPINGRESTRICTIONS, pickup_in_storeBadCamel-cased, all capital letters, uses underscores
ItemDetails.RecipesGoodClearly shows what the base module is and then what functionality it's adding
ItemDetails.Customizations, ItemDetails.Extension, Custom.ItemDetailsBadWhile it states the base module, the vague names do not explain the purpose of the functionality

There are exceptions to this; for example, if you're making an extremely minor change (eg one function or one line) then it's fine to do something like ItemDetails.Site to indicate that it's a site-specific change. Use with caution, though.

Module Versioning

We use semantic versioning and it is strongly encouraged for any customizations that you write. I also encourage the use of version control.

There is too much detail to go into, so you should read their documentation on how to do it. Needless to say, you should conform to <major>.<minor>.<patch>.

If you are patching a core SuiteCommerce module, then you are encouraged to change the version number to make this clear (eg ItemDetails@2.1.0-patch). We also recommend adding a README.md into the module's folder detailing the changes made.

Module Overrides

You should never override an entire module. Don't copy an entire module, rename it and then swap out / override the old module's code with your copy — this is not maintainable.

Overrides are a way of introducing new code by replacing an existing file with a new one.

File Overrides

Generally speaking, templates are the only files where the use of overrides are encouraged. For all other files, you should use extension methods where possible.

TypeFile ExtensionOverride?Comments
JavaScript.jsNoUse other techniques such as extending, wrapping, and prototyping
Sass.scssSometimesThese are still stylesheets, so you should try to cascade styles where possible but files like the base sass styles can be overridden
SuiteScript models.jsNoUse other techniques, such as extending, wrapping, and prototyping
SuiteScript services.ssNoAlmost all the time you will want to modify the model instead; there are extremely rare cases where it might not have the HTTP method you need so you'll need to override it to add it in
SuiteScript service controller.jsNoThese are designed to be extendable, eg see how it's done in add reCAPTCHA to newsletter signup
SuiteScript SSPs.sspNoIn rare cases you can edit the file directly, to bootstrap data into it, for example
Templates.tplYesBut generally only to add new elements; if you're only adding new classes, consider an alternative approach

Template Naming

Overall, the name of the template should match the name of the view that's rendering it but it must be lowercase and use underscores instead of dots. If a part contains multiple words, then you can also separate them with underscores for clarity.

// View:
ProductDetails.Quantity.View.js

// Template:
product_details_quantity.tpl

JavaScript

It's worth spending some time on the specifics related to JavaScript files.

SuiteCommerce Advanced is composed of three single-page Backbone applications. The majority of SCA's frontend JS code files are Backbone constructs that live in JavaScript folders. While some of the 'guts' of SCA do not follow this principle, the overwhelming majority of feature modules do and so should your feature code.

Backbone constructs are models, collections, models, views and routers.

All SCA JavaScript code should be written in AMD syntax using requireJS, with all the code wrapped inside a define() statement. Some third-party JS does not conform to AMD and they are shimmed into the application.

You should declare all dependencies and not assume the availability of any particular framework, utility, library or global variable; this includes Backbone, Underscore, jQuery and the configuration object.

A Backbone construct should be returned by the file, including every method and property it includes. There should be no unreachable method or variable because private methods are not customizable from the outside. In other words, if you have a function in your file, then it should be returned by the construct.

// BAD EXAMPLE

'use strict';

var CONSTANT = 1;
var someFunction = function someFunction(value) {
  return CONSTANT * value;
}

return Backbone.View.extend({
  template: someTemplate,

  getContext: function getContext() {
    return {
      value: someFunction(this.model.get('someValue'))
    }
  }
})

// GOOD EXAMPLE

'use strict';

return Backbone.View.extend({
  constant: 1,

  someFunction: function someFunction(value) {
    return this.constant * value;
  }

  template: someTemplate,

  getContext: function getContext() {
    return {
      value: this.someFunction(this.model.get('someValue'))
    }
  }
})

JavaScript and SuiteScript Naming

There are four parts to a file name:

  1. Module name
  2. Optionally, a word to describe the file if there's more than one of this type (eg multiple views)
  3. Type
  4. File extension

Each part is camel-cased (with initial capitals) and separated by dots, for example:

// Pattern:
<ModuleName>.<Descriptor>.<Type>.<file extension>

// Example:
Facets.Browse.View.js

Like module names, do not use words like 'extension' and 'customization' as file descriptors. If you are going to extend or customize an existing file then you should attach a meaningful descriptor (eg the new module name) to the end of existing name:

// Pattern:
<ExistingFileName>.<NewModuleName>.<file extension>

// Example:
Facets.Browse.View.PageTitleConfigurator.js

Configuration

Any values that can be configured to the user's requirements should be in either in the configuration files (pre-Vinson) or the configuration record (post-Vinson).

Remember that the configuration record is sent to the frontend unobscured, so do not use it for anything that should be kept secret (eg private API keys).

Models and Collections

These are for frontend business logic, data manipulation, response parsing and syncing back and forth between server and browser.

jQuery should not be used to make AJAX requests for NetSuite data.

Validate form data using Backbone.Validate.

If you can, use Backbone.CachedModel and BackboneCachedCollection — they are best with read-only data structures that do not contain user information.

If you introduce new custom fields for items, add them to the mapping in Item.KeyMapping — it means you don't have to use custitem_myfield across all your code.

Views

Views co-ordinate between the presentation layer (DOM and templates) and the business layer (models and collections).

At a minimum, a view must have a template property to render; in a lot of cases you will also probably need to pass values to it via the getContext property.

Since Elbrus, all views are composite views which means that they can have child views added to them without the need to add Backbone.CompositeView as a dependency.

Views can handle events that happened within their DOM or a child's DOM. Use this.listenTo other than this.onthis.listenTo allows the object to keep track of the events, and they can be removed all at once later on. The initial reference application was written before listenTo was available, which is why it doesn't use it.

To reference elements in the view, don't use plain jQuery selectors; instead use this.$el and this.$('selector'). An exception to this is if you need to listen to whole window or document; however, if you do this you must de-register the events on destroy.

Use data attributes to manipulate DOM behavior, and avoid using IDs or classes — they are reserved for styling.

Backbone.FormView should always be used to validate and submit form data — remember that you must validate data on both the front and backends.

Routers

Routers map URLs to client side methods that handle the URL change instead of doing a full page refresh. Routes can be fixed, Backbone routing expressions, or regexes.

Entry Points

Feature modules also contain entry point files, which are interfaces between the application and the module. They must share a name with their module <ModuleName>.js.

There must only be one entry point file per module except in the very specific case where you want to have different entry points for different applications. To do this, you must attach the application name as a suffix to the file name, eg:

  • ModuleName.Shopping.js
  • ModuleName.Checkout.js
  • ModuleName.MyAccount.js

Backbone Events

Frontend applications trigger a series of events, notably during the rendering process. We have a separate blog post on Backbone events that I advise you to read and you should also refer to the Backbone catalog.

Models and routers can also trigger events.

EventTriggered By
beforeStartapplication
afterModulesLoadedapplication
afterStartapplication
beforeRenderlayout
afterRenderlayout
beforeAppendToDomlayout
afterAppendToDomlayout
beforeAppendViewlayout
afterAppendViewlayout
renderEnhancedPageContentlayout (with CDS)
beforeViewRenderview
afterViewRenderview
beforeCompositeViewRenderview
afterCompositeViewRenderview
saveCompletedmodel (form)
cms:renderedSMTs (online) or Backbone.Events (local)

Example: afterAppendView

A basic example to trigger some arbitrary function once the view has been appended to the layout.

define('MyModule'
, [
    // dependencies
  ]
, function
  (
    // dependencies
  )
{
  'use strict'

  return {
    mountToApp: function mountToApp(application)
    {
      var layout = application.getLayout();

      layout.on('afterAppendView', function afterAppendView(view)
      {
        // do something
      })
    }
  }
})

Example: afterViewRender

A more detailed example. We want to update the state of the email input when the user goes to create a new case. We've created a new JavaScript file and then used it to prototype and wrap the initialize function (ie take the existing function and then add new code) to add to the event listener. Then, finally, we add a new function to the prototyped view, which will have the code we want to run.

define('Case.Create.View.UpdateEmailInputState'
, [
    'Case.Create.View'
    // dependencies
    'underscore'
  ]
, function
  (
    CaseCreateView
    // dependencies
    _
  )
{
  'use strict';

  _.extend(CaseCreateView.prototype, {
    initialize: _.wrap(CaseCreateView.prototype.initialize, function (fn, options)
    {
      var self = this;
      fn.apply(this, _.toArray(arguments).slice(1));
      this.on('afterViewRender', function () {
        self.updateEmailInputState();
      });
    })
  , updateEmailInputState: function () {
      // do something
    }
  });
});

PluginContainer

The plugin container works in a similar way to the event listeners but is designed to hook into the processing process. I've included uses for the plugin container in a few tutorials, it appears in the aforementioned beginner's cheatsheet but I've talked about it in more detail in the slightly out of date post on how to add a widget without overriding a module.

The latter example, some jQuery is run to go through the DOM to inject some HTML with a data attribute attached to it; some later code adds a new child view which then renders in that HTML block. We use the plugin container because we need to run the code before the view is rendered:

define('ItemPriceEvolution'
, [
    'underscore'
  , 'ItemDetails.View'
  , 'PluginContainer'
  , 'ItemPriceEvolution.View'
  ]
, function (
    _
  ,  ItemDetailsView
  ,  PluginContainer
  ,  ItemPriceEvolutionView
  )
{
  'use strict';

  return {

    mountToApp: function (application)
    {
      // install the plugin container in the Itemdetails.View class
      ItemDetailsView.prototype.preRenderPlugins = ItemDetailsView.prototype.preRenderPlugins || new PluginContainer();

      // install a plugin that will add a box in the PDP, right before before .item-details-main-bottom-banner
      ItemDetailsView.prototype.preRenderPlugins.install({
        name: 'ItemPriceEvolutionContainer'
      , execute: function ($el, view)
        {
          $el
            .find('.item-details-main-bottom-banner')
            .before('<div data-view="ItemPriceEvolution"></div>');
          return $el;
        }
      });

      ItemDetailsView.prototype.childViews.ItemPriceEvolution = function()
      {
        var view = new ItemPriceEvolutionView({model: this.model});
        return view;
      };
    }
  };
});

Keep in mind the following additional advice:

  • A JavaScript file should contain only one AMD definition
  • Do not put configuration into JavaScript files, use the backend site configuration record or, for older sites, site configuration files
  • For the sake of reusability, avoid unreferenceable private functions

Service Controllers

You can intercept HTTP methods by extending service controller methods. This is something we did in the aforementioned post on how to add reCAPTCHA to the newsletter signup.

define('Newsletter.Extended.View'
, [
    'Newsletter.View'
  , 'Newsletter.Model'
  , 'Backbone'
  , 'underscore'
  , 'jQuery'
  , 'Utils'
  ]
, function (
    NewsletterView
  , NewsletterModel
  , Backbone
  , _
  )
{
  'use strict';

  // @extend Newsletter.View to add the captcha to Newsletter feature
  _.extend(NewsletterView.prototype, {

    // @method initialize Overrides Newsletter.View.initialize method with our own re-captcha logic
    initialize: _.wrap(NewsletterView.prototype.initialize, function wrapNewsletterInitialize (fn, options) {
      fn.apply(this, _.toArray(arguments).slice(1));
      window.renderCaptcha = _.bind(this.render, this);
      var url = 'https://www.google.com/recaptcha/api.js?onload=renderCaptcha&render=explicit';
      this.captchaPromise = jQuery.getScript(url);
    })

    // @method render Overrides Newsletter.View.render method with our own
  , render: function render ()
    {
      if (this.captchaPromise.state() === 'resolved')
      {
        Backbone.View.prototype.render.apply(this, arguments);

        try
        {
          grecaptcha.render(
            this.$el.find('#newsletter-grecaptcha')[0]
          , {
              'sitekey': '---YOUR---SITE---KEY---HERE---'
            }
          );
        }

        catch(err)
        {
          console.log('Error rendering recaptcha: ', err);
        }
      }
    }
  });

  _.extend(NewsletterView.prototype.feedback, {
    'NO_CAPTCHA' : {
      'type': 'error'
    , 'message': _('You must complete the captcha').translate()
    }
  });
});

Backend Models

While you can customize service controllers, most your backend customizations will likely be in the models.

The most common customizations come via backend events, before or after a function is called. However, as the models are simple objects, you can add new methods simply by extending like you would a normal JavaScript file (ie with _.extend) — just note that in these instances you don't use .prototype.

Example: Before a Line is Added to an Order

define('LiveOrder.GiftWrap'
, [
    'Application'
  , 'LiveOrder.Model'
  , 'underscore'
  ]
, function
  (
    Application
  , LiveOrder
  , _
  )
{
  'use strict';

  Application.on('before:LiveOrder.addLine', function (Model, currentLine)
  {
    //note how we modify the parameter currentLine by reference
    if (currentLine.options && !currentLine.options.custcol_ef_gw_giftwrap)
    {
      delete currentLine.options.custcol_ef_gw_id;
      delete currentLine.options.custcol_ef_gw_message;
    }
  });
  _.extend(LiveOrder,
  {
    reformatLines: function(lines)
    {
      lines.sort();
    }
  });
});

JavaScript Coding Style

From this point on, we're very much in the realms of opinion-based recommendations. For example, the PS team advocate not using comma-first syntax which you'll notice is something that I use and appears frequently in our code. As an organization, we may move towards adopting it, but that is not yet determined.

What coding style you adopt is up to you and your organization. Naturally, we would suggest you align with ours so that it's more seamless, but I know firsthand that changing your style can be a difficult process.

So, take the following with a pinch of salt.

ESLint

ESLint is the most popular linting tool and AirBNB's JS style guide is the most widely used style guide; naturally, it seems the best fit to use both so that we are most inline with the rest of the industry.

Linting is a way of enforcing the rules determined by your style guide. The ESLint website has good explanations on how their tool works and how to use it.

Here's an example of what your ruleset could include:

// enforce spacing before and after comma
'comma-spacing': ['error', { before: false, after: true }],

There are five kinds of rules that you could have:

  1. Possible errors — possible syntax or logic errors
  2. Best practices — better ways of doing things to help avoid problems
  3. Strict mode — strict mode directives
  4. Variables — variable declarations
  5. Stylistic issues — for consistency with how everyone else should be doing it

We use the legacy rule set because we're still using ECMAScript 3:

In our code, we:

  • Use requireJS
  • Use ECMAScript 3, so no const, let and other modern language features
  • Use four spaces for indentation
  • Camelcase variables with no underscores
  • Use use strict
  • Restrict whitespace
  • Do not use comma-first
  • Declare variables at the top, one per line
  • Need to be able to modify received parameter's properties (to modify prototypes)
  • Aim to support older browsers
  • Have numerous global variables (eg SC)

This is our lint file:

.eslintrc
{
    "psStandardVersion": "1.0.0-beta3",
    "basedOn": "eslint-config-airbnb/legacy@13.0.0",
    "extends": "eslint-config-airbnb/legacy",
    "env": {
        "browser": true,
        "amd":true,
        "es6": false,
        "node": false
    },
    "plugins": [
        "requirejs"
    ],
    "parserOptions": {
        "ecmaVersion": 3
    },

    "rules": {
        //Requirejs plugin
        "requirejs/no-invalid-define": 2,
        "requirejs/no-multiple-define": 2,
        "requirejs/no-commonjs-wrapper": 2,
        "requirejs/no-object-define": 1,
        "requirejs/one-dependency-per-line": 1,

        //extra
        "indent": ["error", 4], // Changing AirBNB 2 spaces to 4
        "max-len": ["error", 160, 0], // Longer lines
        "no-useless-escape": ["warn"], // Lowering from error to warning
        "complexity": ["warn", 12],
        "no-underscore-dangle": ["error", {"allowAfterThis": true }], // Allow this._ , mostly because of _render
        "no-plusplus": ["off"], // Nothing wrong with ++ && --
        "no-param-reassign": ["error", { "props": false }], // we need to reasign param props (prototype modifications!)
        "quote-props": ["error", "as-needed", { "keywords": true, "unnecessary": false, "numbers": true }], //mostly tweeaked for routers
        "no-throw-literal": ["off"], // we throw objects on backend services
        "new-cap": ["off"] // because of new nlobjColumn :(

    },
    "globals": {
        "_gat": false,
        "badRequestError": true,
        "CMS": true,
        "console": true,
        "context": true,
        "customer": true,
        "define": false,
        "JSON": true,
        "log": true,
        "methodNotAllowedError": true,
        "nlapiAttachRecord": true,
        "nlapiCreateError": true,
        "nlapiCreateForm": false,
        "nlapiCreateRecord": false,
        "nlapiCreateSearch": true,
        "nlapiDateToString": false,
        "nlapiDeleteFile": true,
        "nlapiGetCache": false,
        "nlapiGetContext": false,
        "nlapiGetNewRecord": false,
        "nlapiGetRecordId": false,
        "nlapiGetRecordType": false,
        "nlapiGetUser": true,
        "nlapiLoadFile": false,
        "nlapiLoadRecord": false,
        "nlapiLogExecution": false,
        "nlapiLookupField": true,
        "nlapiRequestURL": false,
        "nlapiResolveURL": false,
        "nlapiSearchRecord": false,
        "nlapiSelectNodes": false,
        "nlapiSelectValue": false,
        "nlapiSubmitField": false,
        "nlapiSubmitFile": false,
        "nlapiSubmitRecord": false,
        "nlapiYieldScript": true,
        "nlobjError": true,
        "nlobjSearchColumn": false,
        "nlobjSearchFilter": false,
        "notFoundError": true,
        "nsglobal": false,
        "order": true,
        "request": true,
        "require": false,
        "response": true,
        "SC": false,
        "session": true,
        "unauthorizedError": true,
        "util": true
    }
}

As previously mentioned, I don't follow these rules myself and they are applied inconsistently throughout our core code. We're likely to converge soon on the above rules.

The lint file can be added to the package.json file, with a Gulp task that lints when called. You can block deployment if there are errors in your code. You can also integrate the rules into your IDE so that errors are flagged on-the-fly.

Final Thoughts

I hope these guidelines provide you with worthwhile information on how to improve your code. As previously mentioned, they were developed to aid our internal team write customizations for our customers so that they are of high quality, re-usable, easy to understand and maintainable. Whether or not you adopt them is up to you, but we think that they'll save you a lot of time and headaches down the road.