Building Custom SuiteCommerce Extensions: A Developer's Start-to-Finish Guide
73% of SuiteCommerce implementations require custom extensions to meet business requirements. Yet most developers spend their first extension project fighting the framework instead of building features. After developing dozens of extensions across SuiteCommerce Advanced implementations, we've distilled the process into a repeatable methodology that eliminates the guesswork.
This guide takes you from zero to deployed extension, covering architecture decisions, file structure, frontend and backend integration, testing strategies, and deployment workflows. Whether you're building a custom product configurator, a specialized checkout flow, or a B2B portal feature, the patterns here apply universally.
Table of Contents
- Extension Architecture Overview
- Setting Up Your Development Environment
- Module Structure and Configuration
- Frontend Development: Templates, Views, and Models
- Backend Integration: SuiteScript Services
- State Management and Event Handling
- Testing Your Extension
- Deployment and Version Management
- Performance Considerations
- FAQ
Extension Architecture Overview
SuiteCommerce extensions follow a modular architecture built on Backbone.js with a custom AMD loader. Understanding this architecture before writing code saves hours of debugging.

The Extension Lifecycle
Every extension goes through these phases:
- Module Registration: The extension declares its modules in
ns.package.json - Dependency Resolution: SCA's module loader resolves and loads dependencies
- Entry Point Execution: Your main JavaScript file runs, registering components
- Mount Point Integration: Views attach to predefined mount points in the theme
- Runtime Execution: Event handlers, models, and services execute as users interact
Extension vs. Theme Customization
Before building an extension, ask whether your requirement actually needs one:
| Use an Extension When | Use Theme Customization When |
|---|---|
| Adding new functionality | Modifying existing layouts |
| Integrating third-party APIs | Changing styles and colors |
| Creating reusable components | Adjusting template markup |
| Building complex business logic | Minor text or image changes |
| Needing backend SuiteScript | Frontend-only changes |
Extensions live separately from themes, making them portable across theme updates. This isolation is their primary advantage.
Anatomy of an Extension
A typical extension contains:
MyExtension/
├── Modules/
│ └── MyModule/
│ ├── JavaScript/
│ │ ├── MyModule.js
│ │ ├── MyModule.View.js
│ │ ├── MyModule.Model.js
│ │ └── MyModule.Router.js
│ ├── Templates/
│ │ └── my_module.tpl
│ ├── Sass/
│ │ └── _my-module.scss
│ ├── SuiteScript/
│ │ └── MyModule.ServiceController.js
│ └── Configuration/
│ └── MyModule.json
├── ns.package.json
├── manifest.json
└── README.md
Every file has a purpose. Skipping or misplacing files causes silent failures that waste debugging time.
Setting Up Your Development Environment
A proper development environment reduces iteration time from minutes to seconds.
Required Tools
SuiteCommerce Developer Tools (SCC DevTools)
# Install globally
npm install -g @anthropic/suitecommerce-devtools
# Verify installation
scc --version
Node.js and npm
SuiteCommerce requires Node.js 16+ for local development. We recommend using nvm to manage versions:
nvm install 18
nvm use 18
NetSuite Account Configuration
Your account needs these permissions enabled:
- SuiteScript deployment
- SuiteCommerce file access
- Customization permission
Project Initialization
Create a new extension project:
# Create extension scaffolding
scc extension:create MyExtension --vendor "YourCompany"
# Navigate to the extension
cd MyExtension
# Install dependencies
npm install
The scaffolding generates a working extension structure. Resist the urge to deviate from this structure—SuiteCommerce's build process expects specific paths.
Local Development Server
Run the local development server to test changes without deploying:
# Start local development
scc extension:local --source MyExtension
# This proxies your local extension files against your live SuiteCommerce instance
The local server injects your extension files into the live site, letting you test changes instantly. This workflow is essential—deploying to test minor changes wastes 5-10 minutes per iteration.
Configuring Your Development Domain
Create a gulp/config/config.json file:
{
"credentials": {
"molecule": "your-molecule-id",
"nsVersion": "2024.1",
"applicationId": "YOUR_APP_ID"
},
"folders": {
"source": "Modules",
"distribution": "deploy"
},
"identity": {
"vendor": "YourCompany",
"name": "MyExtension",
"version": "1.0.0"
}
}
Replace placeholder values with your actual NetSuite credentials.
Module Structure and Configuration
Modules are the building blocks of extensions. Each module encapsulates a specific feature.
The ns.package.json File
This file tells SuiteCommerce what your extension contains:
{
"gulp": {
"javascript": [
"JavaScript/*.js"
],
"templates": [
"Templates/*.tpl"
],
"sass": [
"Sass/**/*.scss"
],
"ssp-libraries": [
"SuiteScript/*.js"
]
},
"overrides": {},
"dependencies": [],
"application": {
"myaccount": true,
"checkout": true,
"shopping": true
}
}
The application object controls which SuiteCommerce applications load your module. Set false for applications where your module shouldn't appear.
Entry Point Configuration
Your module's entry point JavaScript file must export a mountToApp function:
// MyModule.js
define('MyModule', [
'MyModule.View',
'MyModule.Router'
], function(
MyModuleView,
MyModuleRouter
) {
'use strict';
return {
mountToApp: function(application) {
// Register router
var router = new MyModuleRouter(application);
// Register child views to mount points
var PageType = application.getComponent('PageType');
PageType.registerPageType({
name: 'my-module-page',
routes: ['my-custom-route'],
view: MyModuleView
});
return router;
}
};
});
The mountToApp function receives the application instance, giving you access to all SuiteCommerce components and services.
Dependency Declaration
Declare module dependencies in the module's configuration:
{
"dependencies": [
"Backbone",
"underscore",
"jQuery",
"SC.Configuration",
"Utils"
]
}
Only declare dependencies you actually use. Each dependency adds to your bundle size and load time.
Frontend Development: Templates, Views, and Models
The frontend follows Backbone.js patterns with SuiteCommerce-specific conventions.

Creating Views
Views manage DOM rendering and user interactions:
// MyModule.View.js
define('MyModule.View', [
'Backbone',
'my_module.tpl',
'Utils'
], function(
Backbone,
myModuleTemplate,
Utils
) {
'use strict';
return Backbone.View.extend({
template: myModuleTemplate,
title: Utils.translate('My Module Page'),
page_header: Utils.translate('My Module'),
events: {
'click [data-action="submit"]': 'handleSubmit',
'change [data-input="quantity"]': 'handleQuantityChange'
},
initialize: function(options) {
this.application = options.application;
this.model = options.model;
// Listen to model changes
this.model.on('change', this.render, this);
},
// Data passed to the template
getContext: function() {
return {
pageTitle: this.title,
items: this.model.get('items') || [],
isLoading: this.model.get('isLoading'),
errorMessage: this.model.get('errorMessage')
};
},
handleSubmit: function(event) {
event.preventDefault();
var self = this;
this.model.save().done(function() {
self.showConfirmation();
}).fail(function(error) {
self.showError(error);
});
},
handleQuantityChange: function(event) {
var quantity = parseInt(event.target.value, 10);
this.model.set('quantity', quantity);
},
showConfirmation: function() {
// Show success message
},
showError: function(error) {
this.model.set('errorMessage', error.message);
}
});
});
Template Syntax
SuiteCommerce uses Handlebars templates with custom helpers:
{{!-- my_module.tpl --}}
<div class="my-module" data-view="MyModule">
<h1 class="my-module-title">{{pageTitle}}</h1>
{{#if isLoading}}
<div class="my-module-loading">
<span class="my-module-loading-icon"></span>
{{translate 'Loading...'}}
</div>
{{else}}
{{#if errorMessage}}
<div class="my-module-error" data-type="alert-error">
{{errorMessage}}
</div>
{{/if}}
{{#if items.length}}
<ul class="my-module-items">
{{#each items}}
<li class="my-module-item" data-item-id="{{internalid}}">
<span class="my-module-item-name">{{name}}</span>
<span class="my-module-item-price">
{{formatCurrency price}}
</span>
<input
type="number"
data-input="quantity"
value="{{quantity}}"
min="1"
max="{{maxQuantity}}"
/>
</li>
{{/each}}
</ul>
{{else}}
<p class="my-module-empty">
{{translate 'No items found.'}}
</p>
{{/if}}
<button data-action="submit" class="my-module-submit-button">
{{translate 'Submit'}}
</button>
{{/if}}
{{!-- Child view mount point --}}
<div data-view="MyModule.ChildView"></div>
</div>
Key template conventions:
- Use
{{translate 'text'}}for all user-facing strings (enables i18n) - Use
data-attributes for JavaScript hooks, not classes - Use semantic class names following BEM conventions
- Create mount points for child views with
data-view
Models and Data Fetching
Models handle data and communicate with backend services:
// MyModule.Model.js
define('MyModule.Model', [
'Backbone',
'Utils'
], function(
Backbone,
Utils
) {
'use strict';
return Backbone.Model.extend({
urlRoot: Utils.getAbsoluteUrl(
'services/MyModule.Service.ss'
),
defaults: {
items: [],
isLoading: false,
errorMessage: null
},
initialize: function() {
// Model initialization
},
// Parse server response
parse: function(response) {
if (response.error) {
return {
errorMessage: response.error.message
};
}
return {
items: response.items || [],
total: response.total || 0
};
},
// Validate before saving
validate: function(attributes) {
var errors = [];
if (!attributes.items || attributes.items.length === 0) {
errors.push({
field: 'items',
message: Utils.translate('At least one item is required.')
});
}
if (errors.length) {
return errors;
}
},
// Custom fetch with loading state
fetch: function(options) {
var self = this;
this.set('isLoading', true);
this.set('errorMessage', null);
return Backbone.Model.prototype.fetch.call(this, options)
.always(function() {
self.set('isLoading', false);
});
}
});
});
Child Views and Composition
Build complex UIs by composing child views:
// MyModule.View.js (with child views)
define('MyModule.View', [
'Backbone',
'Backbone.CompositeView',
'MyModule.ChildView',
'my_module.tpl'
], function(
Backbone,
BackboneCompositeView,
MyModuleChildView,
myModuleTemplate
) {
'use strict';
return Backbone.View.extend({
template: myModuleTemplate,
initialize: function(options) {
BackboneCompositeView.add(this);
this.application = options.application;
},
childViews: {
'MyModule.ChildView': function() {
return new MyModuleChildView({
model: this.model,
application: this.application
});
}
},
getContext: function() {
return {
// context data
};
}
});
});
Backend Integration: SuiteScript Services
Most extensions need backend services to interact with NetSuite data.
Service Controller Pattern
Create a SuiteScript service controller:
// MyModule.ServiceController.js
define('MyModule.ServiceController', [
'ServiceController',
'MyModule.Model'
], function(
ServiceController,
MyModuleModel
) {
'use strict';
return ServiceController.extend({
name: 'MyModule.ServiceController',
// Handle GET requests
get: function() {
var filters = this.request.getParameter('filters');
var page = parseInt(this.request.getParameter('page'), 10) || 1;
var pageSize = parseInt(this.request.getParameter('pageSize'), 10) || 20;
try {
var model = new MyModuleModel();
var results = model.list({
filters: filters ? JSON.parse(filters) : {},
page: page,
pageSize: pageSize
});
return results;
} catch (error) {
return {
error: {
code: 'FETCH_ERROR',
message: error.message
}
};
}
},
// Handle POST requests
post: function() {
var data = this.data;
// Validate required fields
if (!data.items || !data.items.length) {
return {
error: {
code: 'VALIDATION_ERROR',
message: 'Items are required.'
}
};
}
try {
var model = new MyModuleModel();
var result = model.create(data);
return {
success: true,
id: result.id
};
} catch (error) {
return {
error: {
code: 'CREATE_ERROR',
message: error.message
}
};
}
},
// Handle PUT requests
put: function() {
var id = this.request.getParameter('internalid');
var data = this.data;
try {
var model = new MyModuleModel();
model.update(id, data);
return {
success: true,
id: id
};
} catch (error) {
return {
error: {
code: 'UPDATE_ERROR',
message: error.message
}
};
}
},
// Handle DELETE requests
delete: function() {
var id = this.request.getParameter('internalid');
try {
var model = new MyModuleModel();
model.remove(id);
return {
success: true
};
} catch (error) {
return {
error: {
code: 'DELETE_ERROR',
message: error.message
}
};
}
}
});
});
Backend Model Pattern
Separate data access from the controller:
// MyModule.Model.ss.js (Backend model)
define('MyModule.Model', [
'SC.Model',
'Application',
'Utils'
], function(
SCModel,
Application,
Utils
) {
'use strict';
return SCModel.extend({
name: 'MyModule.Model',
list: function(options) {
var filters = options.filters || {};
var page = options.page || 1;
var pageSize = options.pageSize || 20;
var search = nlapiCreateSearch('customrecord_my_record', [
['isinactive', 'is', 'F']
], [
new nlobjSearchColumn('name'),
new nlobjSearchColumn('custrecord_field1'),
new nlobjSearchColumn('custrecord_field2')
]);
var searchResults = search.runSearch();
var startIndex = (page - 1) * pageSize;
var results = searchResults.getResults(startIndex, startIndex + pageSize);
var items = results.map(function(result) {
return {
internalid: result.getId(),
name: result.getValue('name'),
field1: result.getValue('custrecord_field1'),
field2: result.getValue('custrecord_field2')
};
});
return {
items: items,
page: page,
pageSize: pageSize,
total: this.getCount(filters)
};
},
get: function(id) {
var record = nlapiLoadRecord('customrecord_my_record', id);
return {
internalid: record.getId(),
name: record.getFieldValue('name'),
field1: record.getFieldValue('custrecord_field1'),
field2: record.getFieldValue('custrecord_field2')
};
},
create: function(data) {
var record = nlapiCreateRecord('customrecord_my_record');
record.setFieldValue('name', data.name);
record.setFieldValue('custrecord_field1', data.field1);
record.setFieldValue('custrecord_field2', data.field2);
var id = nlapiSubmitRecord(record);
return {
id: id
};
},
update: function(id, data) {
var record = nlapiLoadRecord('customrecord_my_record', id);
if (data.name !== undefined) {
record.setFieldValue('name', data.name);
}
if (data.field1 !== undefined) {
record.setFieldValue('custrecord_field1', data.field1);
}
if (data.field2 !== undefined) {
record.setFieldValue('custrecord_field2', data.field2);
}
nlapiSubmitRecord(record);
},
remove: function(id) {
nlapiDeleteRecord('customrecord_my_record', id);
},
getCount: function(filters) {
var countSearch = nlapiCreateSearch('customrecord_my_record', [
['isinactive', 'is', 'F']
], [
new nlobjSearchColumn('internalid', null, 'count')
]);
var results = countSearch.runSearch().getResults(0, 1);
return parseInt(results[0].getValue('internalid', null, 'count'), 10);
}
});
});
Registering the Service
Add your service to the SSP application's service controller registration:
{
"ssp-libraries": {
"entry_point": "SuiteScript/MyModule.ServiceController.js",
"dependencies": [
"SuiteScript/MyModule.Model.ss.js"
]
}
}
State Management and Event Handling
Complex extensions need proper state management.
Application Events
Use SuiteCommerce's event system for cross-module communication:
// Publishing events
define('MyModule.Publisher', [
'Backbone'
], function(Backbone) {
'use strict';
return {
notifyItemAdded: function(item) {
Backbone.trigger('MyModule.ItemAdded', item);
},
notifyCartUpdated: function(cart) {
Backbone.trigger('MyModule.CartUpdated', cart);
}
};
});
// Subscribing to events
define('MyModule.Subscriber', [
'Backbone'
], function(Backbone) {
'use strict';
return {
initialize: function() {
Backbone.on('MyModule.ItemAdded', this.handleItemAdded, this);
Backbone.on('cart:updated', this.handleCartUpdate, this);
},
handleItemAdded: function(item) {
console.log('Item added:', item);
},
handleCartUpdate: function(cart) {
console.log('Cart updated:', cart);
},
destroy: function() {
Backbone.off('MyModule.ItemAdded', this.handleItemAdded, this);
Backbone.off('cart:updated', this.handleCartUpdate, this);
}
};
});
Integrating with Existing Components
Access and extend existing SuiteCommerce components:
// Adding items to cart programmatically
define('MyModule.CartIntegration', [
'LiveOrder.Model'
], function(LiveOrderModel) {
'use strict';
return {
addToCart: function(item, quantity) {
var cart = LiveOrderModel.getInstance();
return cart.addItem({
item: {
internalid: item.internalid
},
quantity: quantity
});
},
getCartSummary: function() {
var cart = LiveOrderModel.getInstance();
return {
itemCount: cart.get('lines').length,
subtotal: cart.get('summary').subtotal
};
}
};
});
Testing Your Extension
Testing prevents production bugs and regression issues.
Unit Testing with Jasmine
Set up Jasmine tests for your modules:
// tests/MyModule.View.spec.js
define([
'MyModule.View',
'MyModule.Model',
'Backbone'
], function(MyModuleView, MyModuleModel, Backbone) {
'use strict';
describe('MyModule.View', function() {
var view;
var model;
beforeEach(function() {
model = new MyModuleModel({
items: [
{ internalid: '1', name: 'Item 1', price: 10.00 },
{ internalid: '2', name: 'Item 2', price: 20.00 }
]
});
view = new MyModuleView({
model: model,
application: {
getComponent: function() { return {}; }
}
});
});
afterEach(function() {
view.destroy();
});
it('should render with items', function() {
view.render();
expect(view.$('.my-module-item').length).toBe(2);
});
it('should show loading state', function() {
model.set('isLoading', true);
view.render();
expect(view.$('.my-module-loading').length).toBe(1);
});
it('should handle submit click', function() {
spyOn(model, 'save').and.returnValue($.Deferred().resolve());
view.render();
view.$('[data-action="submit"]').click();
expect(model.save).toHaveBeenCalled();
});
it('should display error message', function() {
model.set('errorMessage', 'Something went wrong');
view.render();
expect(view.$('.my-module-error').text()).toContain('Something went wrong');
});
});
});
Integration Testing
Test the full flow from frontend to backend:
// tests/MyModule.Integration.spec.js
define([
'MyModule.Model',
'jQuery'
], function(MyModuleModel, $) {
'use strict';
describe('MyModule Integration', function() {
var model;
beforeEach(function() {
model = new MyModuleModel();
});
it('should fetch items from the server', function(done) {
model.fetch().done(function() {
expect(model.get('items')).toBeDefined();
expect(Array.isArray(model.get('items'))).toBe(true);
done();
}).fail(function(error) {
fail('Fetch should not fail: ' + error);
done();
});
});
it('should handle server errors gracefully', function(done) {
// Mock a server error
spyOn($, 'ajax').and.returnValue(
$.Deferred().reject({ status: 500 })
);
model.fetch().always(function() {
expect(model.get('isLoading')).toBe(false);
done();
});
});
});
});
Running Tests
Execute tests using the SuiteCommerce test runner:
# Run all tests
scc extension:test
# Run specific test file
scc extension:test --spec tests/MyModule.View.spec.js
# Run with coverage
scc extension:test --coverage
Deployment and Version Management
A reliable deployment process prevents production incidents.
Build Process
Build your extension for deployment:
# Build for production
scc extension:build --production
# This creates optimized bundles in the deploy/ directory
The build process:
- Compiles Sass to CSS
- Minifies JavaScript
- Compiles Handlebars templates
- Generates source maps (optional)
- Creates the deployment manifest
Deployment to NetSuite
Deploy using the SuiteCommerce developer tools:
# Deploy to sandbox
scc extension:deploy --target sandbox
# Deploy to production (requires confirmation)
scc extension:deploy --target production
# Deploy specific version
scc extension:deploy --target production --version 1.2.0
Version Control Best Practices
Structure your git workflow for extensions:
# Branch naming
feature/my-new-feature
bugfix/fix-cart-calculation
release/1.2.0
# Commit message format
[MODULE] Brief description
# Examples
[MyModule] Add quantity validation to cart
[MyModule] Fix price calculation for bulk orders
[Config] Update deployment configuration
Rollback Strategy
Always maintain rollback capability:
# Tag releases
git tag -a v1.2.0 -m "Release 1.2.0 - Added bulk pricing"
# Deploy previous version if needed
scc extension:deploy --target production --version 1.1.0
Performance Considerations
Extensions directly impact site performance. Build with performance in mind.
Bundle Size Optimization
Keep your JavaScript bundles small:
// Bad: Import entire library
define(['lodash'], function(_) {
return _.map(items, transform);
});
// Good: Import only what you need
define(['underscore'], function(_) {
return _.map(items, transform);
});
Lazy Loading
Load extension features only when needed:
// Lazy load heavy modules
define('MyModule', [], function() {
'use strict';
return {
mountToApp: function(application) {
var router = application.getComponent('Router');
router.route('my-heavy-feature', function() {
// Load the heavy view only when the route is accessed
require(['MyModule.HeavyView'], function(HeavyView) {
var view = new HeavyView({ application: application });
view.showInModal();
});
});
}
};
});
Caching Strategies
Cache expensive computations and API responses:
// Client-side caching
define('MyModule.Cache', [], function() {
'use strict';
var cache = {};
var CACHE_TTL = 5 * 60 * 1000; // 5 minutes
return {
get: function(key) {
var entry = cache[key];
if (entry && Date.now() < entry.expiry) {
return entry.value;
}
return null;
},
set: function(key, value, ttl) {
cache[key] = {
value: value,
expiry: Date.now() + (ttl || CACHE_TTL)
};
},
clear: function(key) {
if (key) {
delete cache[key];
} else {
cache = {};
}
}
};
});
Minimizing NetSuite API Calls
Batch requests where possible:
// Bad: Multiple separate requests
items.forEach(function(item) {
model.fetchItem(item.id);
});
// Good: Single batched request
model.fetchItems(items.map(function(item) {
return item.id;
}));
FAQ
How long does it take to build a typical extension?
Simple extensions (UI widgets, display modifications) take 2-4 days. Medium complexity (custom checkout steps, B2B features) take 1-2 weeks. Complex extensions (product configurators, custom integrations) take 3-6 weeks. Factor in testing and deployment time.
Can I use TypeScript for SuiteCommerce extensions?
Not natively. SuiteCommerce's build system expects JavaScript with AMD modules. You can compile TypeScript to JavaScript, but this adds build complexity. Most teams stick with JavaScript and JSDoc for type hints.
How do I debug extensions in production?
Enable verbose logging during development, then use browser DevTools and the Network tab to trace issues. For backend issues, use SuiteScript's nlapiLogExecution() and check the Execution Log in NetSuite.
What's the best way to handle extension configuration?
Use SuiteCommerce's Configuration component. Create a JSON schema for your settings and access them via SC.Configuration.get('mymodule.setting'). This allows settings changes without code deployments.
How do I make extensions theme-agnostic?
Avoid hard-coding CSS selectors from specific themes. Use data attributes for JavaScript hooks. Provide your own CSS classes and let theme developers override styles. Test with multiple themes before release.
Start Building
You now have the architecture knowledge and code patterns to build production-quality SuiteCommerce extensions. The patterns in this guide have been refined across dozens of implementations—they work.
If you're facing a complex extension requirement or need to accelerate development, our SuiteCommerce team has built extensions ranging from simple UI enhancements to full custom checkout experiences. We're happy to review your requirements.
For simpler customizations that don't require extensions, check our guide on SuiteCommerce theme development (coming soon).
The best extension is one that's maintainable, performant, and solves a real business problem. Build with those goals, and you'll succeed.
Need Help with Your NetSuite Project?
Our team of experts is ready to help you achieve your goals.


