Introduction
Most SuiteScripts start small β a few hundred lines of code to handle one automation.
But as your solution grows, scripts become messy: business logic mixes with UI code, record handling, and search queries all in one file.
Thatβs where multi-layer architecture comes in.
By separating logic into distinct layers β UI, Business, and Data β you gain clarity, scalability, and testability.
This post will show you exactly how to structure it.
βοΈ 1οΈβ£ Three-Layer SuiteScript Architecture Overview
Layer | Purpose | Example |
---|---|---|
Presentation Layer (UI) | Handles user interaction (Suitelet, Client Script) | Form display, validation |
Business Logic Layer | Applies rules and processes | Approval workflow, validation logic |
Data Access Layer | Handles record/search operations | Load, save, update, query records |
Each layer is modular and communicates only through defined interfaces.
π§± 2οΈβ£ Folder Structure Example
/SuiteScripts/
/app/
/ui/
SL_InvoiceDashboard.js
CS_InvoiceValidator.js
/business/
invoiceService.js
approvalManager.js
/data/
recordRepository.js
searchRepository.js
/lib/
logHelper.js
emailHelper.js
β Clean separation of logic and responsibilities.
π§© 3οΈβ£ Example: Invoice Approval Process
π§ Goal
Build a Suitelet to approve invoices, using a business logic module and a data repository.
π /data/recordRepository.js
/**
* @NApiVersion 2.1
* @NModuleScope Public
*/
define(['N/record','N/search'], (record, search) => {
const getInvoice = (id) => record.load({ type: record.Type.INVOICE, id });
const updateInvoiceStatus = (id, status) =>
record.submitFields({
type: record.Type.INVOICE,
id,
values: { approvalstatus: status }
});
const getPendingInvoices = () => {
const results = [];
search.create({
type: 'invoice',
filters: [['approvalstatus','anyof','1']],
columns: ['tranid','entity','amount']
}).run().each(r => {
results.push({
id: r.id,
tranid: r.getValue('tranid'),
customer: r.getText('entity'),
amount: r.getValue('amount')
});
return true;
});
return results;
};
return { getInvoice, updateInvoiceStatus, getPendingInvoices };
});
β Data layer handles all NetSuite record operations only.
π /business/invoiceService.js
/**
* @NApiVersion 2.1
* @NModuleScope Public
*/
define(['../data/recordRepository', '../lib/logHelper'], (repo, logHelper) => {
const approveInvoice = (invoiceId) => {
const invoice = repo.getInvoice(invoiceId);
if (!invoice.getValue('custbody_approved_by')) {
repo.updateInvoiceStatus(invoiceId, 2); // Approved
logHelper.info('Invoice Approved', invoiceId);
} else {
logHelper.warn('Already approved', invoiceId);
}
};
const listPendingInvoices = () => repo.getPendingInvoices();
return { approveInvoice, listPendingInvoices };
});
β Business layer applies logic and uses data layer β no direct NetSuite API calls here.
π /ui/SL_InvoiceDashboard.js
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*/
define(['N/ui/serverWidget', '../business/invoiceService'], (ui, service) => {
const onRequest = (ctx) => {
const form = ui.createForm({ title: 'Invoice Approval Dashboard' });
const invoices = service.listPendingInvoices();
const sublist = form.addSublist({
id: 'custpage_invoices',
type: ui.SublistType.LIST,
label: 'Pending Invoices'
});
sublist.addField({ id:'tranid', label:'Invoice #', type: ui.FieldType.TEXT });
sublist.addField({ id:'customer', label:'Customer', type: ui.FieldType.TEXT });
sublist.addField({ id:'amount', label:'Amount', type: ui.FieldType.CURRENCY });
invoices.forEach((i, idx) => {
sublist.setSublistValue({ id:'tranid', line: idx, value: i.tranid });
sublist.setSublistValue({ id:'customer', line: idx, value: i.customer });
sublist.setSublistValue({ id:'amount', line: idx, value: i.amount });
});
ctx.response.writePage(form);
};
return { onRequest };
});
β UI layer only renders data from the business service β clean and modular.
π§© 4οΈβ£ Key Benefits of Multi-Layer Architecture
Benefit | Description |
---|---|
β Separation of Concerns | UI, logic, and data are isolated and testable |
β Reusability | Business logic reusable across Suitelets, RESTlets, and workflows |
β Scalability | Easier to extend with new features |
β Testability | Business and data layers can be unit-tested independently |
β Team Collaboration | Multiple developers can work in parallel on different layers |
π§ 5οΈβ£ Integration with Other Scripts
Your business layer can also be reused in:
- RESTlets (API endpoints)
- Scheduled scripts (batch processing)
- User Events (triggered automation)
Example:
define(['../business/invoiceService'], (service) => {
const execute = () => service.approveInvoice(123);
return { execute };
});
β One business logic β many use cases.
βοΈ 6οΈβ£ Governance & Maintenance Tips
- Cache reference data (tax codes, roles) in the data layer.
- Keep heavy logic out of the UI layer.
- Use dependency injection for test environments.
- Add version headers for each layer (v1.0.0, v1.1.0).
π 7οΈβ£ Versioning Example
/business/invoiceService_v1.1.0.js
/business/invoiceService_v1.2.0.js
Update dependent scripts gradually for backward compatibility.
π 8οΈβ£ Common Pitfalls to Avoid
Pitfall | Fix |
---|---|
UI scripts calling record APIs directly | Move to Data layer |
Mixed search + record logic in one file | Split into repository module |
No logging or error handling | Use centralized logHelper |
Hardcoded record types | Use constants file (/lib/constants.js ) |
β 9οΈβ£ Summary
Layer | Focus | Example Module |
---|---|---|
UI | Forms, interaction | SL_InvoiceDashboard.js |
Business | Process rules | invoiceService.js |
Data | Record operations | recordRepository.js |
β Keep them independent β they form the backbone of scalable SuiteApps.
Conclusion
Multi-layer SuiteScript architecture makes your codebase clean, modular, and enterprise-ready.
By clearly separating presentation, business, and data layers, you enable faster development, simpler debugging, and long-term maintainability.
This structure is exactly how leading NetSuite partners design production-grade SuiteApps.
Discover more from The NetSuite Pro
Subscribe to get the latest posts sent to your email.
Leave a Reply