Code Standards
Consistent coding standards make SuiteScript projects maintainable, readable, and easier to debug.
Script Header Standards
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* @NModuleScope SameAccount
* @description Invoice validation and approval routing
* @author Your Name
* @version 1.0.0
* @since 2024-01-15
*
* @changelog
* 1.0.0 - 2024-01-15 - Initial release
* 1.0.1 - 2024-02-01 - Added multi-currency support
*/
Required Annotations
| Annotation | Required | Description |
|---|---|---|
@NApiVersion | Yes | API version (always use 2.1) |
@NScriptType | Yes | Script type |
@NModuleScope | No | Module scope (default: SameAccount) |
@description | Recommended | What the script does |
File Organization
┌─────────────────────────────────────────────────────────────────────────────┐
│ RECOMMENDED FILE STRUCTURE │
└─────────────────────────────────────────────────────────────────────────────┘
src/FileCabinet/SuiteScripts/
│
├── Libraries/ # Shared libraries
│ ├── utils_lib.js
│ ├── validation_lib.js
│ └── email_lib.js
│
├── UserEvents/ # User Event scripts
│ ├── invoice_ue.js
│ └── salesorder_ue.js
│
├── ClientScripts/ # Client scripts
│ ├── invoice_cs.js
│ └── customer_cs.js
│
├── Suitelets/ # Suitelets
│ ├── reports/
│ │ └── sales_report_sl.js
│ └── portals/
│ └── customer_portal_sl.js
│
├── ScheduledScripts/ # Scheduled scripts
│ └── daily_sync_ss.js
│
├── MapReduce/ # Map/Reduce scripts
│ └── mass_update_mr.js
│
└── RESTlets/ # RESTlets
└── api/
└── customer_api_rl.js
Naming Conventions
File Names
┌─────────────────────────────────────────────────────────────────────────────┐
│ FILE NAMING PATTERN │
└─────────────────────────────────────────────────────────────────────────────┘
[purpose]_[type].js
Examples:
├── invoice_validation_ue.js (User Event)
├── customer_form_cs.js (Client Script)
├── sales_report_sl.js (Suitelet)
├── daily_sync_ss.js (Scheduled Script)
├── mass_update_mr.js (Map/Reduce)
├── customer_api_rl.js (RESTlet)
└── date_utils_lib.js (Library)
Script IDs
<!-- Pattern: customscript_[company]_[purpose]_[type] -->
<script scriptid="customscript_acme_invoice_val_ue">
<script scriptid="customscript_acme_customer_cs">
<script scriptid="customscript_acme_daily_sync_ss">
Deployment IDs
<!-- Pattern: customdeploy_[company]_[purpose]_[recordtype] -->
<scriptdeployment scriptid="customdeploy_acme_invoice_val_invoice">
<scriptdeployment scriptid="customdeploy_acme_customer_customer">
Custom Object IDs
| Type | Pattern | Example |
|---|---|---|
| Custom Record | customrecord_[company]_[name] | customrecord_acme_approval |
| Custom Field | custbody_[company]_[name] | custbody_acme_approval_status |
| Custom List | customlist_[company]_[name] | customlist_acme_priority |
| Saved Search | customsearch_[company]_[name] | customsearch_acme_open_orders |
JavaScript Standards
Use SuiteScript 2.1 Features
// GOOD: SuiteScript 2.1 with modern JavaScript
define(['N/record', 'N/search'], (record, search) => {
const processRecords = (recordIds) => {
const results = recordIds.map(id => {
const rec = record.load({ type: record.Type.CUSTOMER, id });
return {
id,
name: rec.getValue({ fieldId: 'companyname' }),
email: rec.getValue({ fieldId: 'email' })
};
});
return results.filter(r => r.email);
};
const getCustomerData = async (customerId) => {
const rec = await record.load.promise({
type: record.Type.CUSTOMER,
id: customerId
});
return rec;
};
return { processRecords, getCustomerData };
});
// AVOID: SuiteScript 2.0 / older JavaScript
define(['N/record', 'N/search'], function(record, search) {
function processRecords(recordIds) {
var results = [];
for (var i = 0; i < recordIds.length; i++) {
var rec = record.load({ type: record.Type.CUSTOMER, id: recordIds[i] });
results.push({
id: recordIds[i],
name: rec.getValue({ fieldId: 'companyname' })
});
}
return results;
}
return { processRecords: processRecords };
});
Variable Declarations
// GOOD: const for values that don't change
const MAX_RESULTS = 1000;
const recordType = record.Type.SALES_ORDER;
// GOOD: let for values that change
let totalAmount = 0;
let currentPage = 1;
// AVOID: var (function-scoped, can cause issues)
var count = 0;
Template Literals
// GOOD: Template literals
const message = `Order ${orderId} processed for customer ${customerName}`;
const query = `
SELECT id, name
FROM customer
WHERE balance > ${minBalance}
`;
// AVOID: String concatenation
var message = 'Order ' + orderId + ' processed for customer ' + customerName;
Arrow Functions
// GOOD: Arrow functions for callbacks
results.forEach(result => {
processResult(result);
});
const filtered = items.filter(item => item.active);
// GOOD: Regular functions for entry points
const beforeSubmit = (context) => {
// Entry point logic
};
return { beforeSubmit };
Function Standards
Single Responsibility
// GOOD: Each function does one thing
const validateCustomer = (customer) => {
if (!customer.email) return { valid: false, error: 'Email required' };
if (!customer.name) return { valid: false, error: 'Name required' };
return { valid: true };
};
const sendNotification = (customer) => {
email.send({
author: -5,
recipients: customer.email,
subject: 'Welcome',
body: buildWelcomeEmail(customer)
});
};
const createCustomer = (data) => {
const validation = validateCustomer(data);
if (!validation.valid) throw new Error(validation.error);
const customer = record.create({ type: record.Type.CUSTOMER });
// ... set values
const id = customer.save();
sendNotification(data);
return id;
};
// AVOID: Functions that do too much
const createCustomerAndSendEmailAndUpdateReport = (data) => {
// Validation
if (!data.email) throw new Error('Email required');
// Create customer
const customer = record.create({ type: record.Type.CUSTOMER });
// ... lots of code
// Send email
email.send({ /* ... */ });
// Update report
record.submitFields({ /* ... */ });
// More stuff...
};
Function Parameters
// GOOD: Use objects for multiple parameters
const createOrder = ({ customerId, items, memo = '', priority = 'normal' }) => {
const order = record.create({ type: record.Type.SALES_ORDER });
order.setValue({ fieldId: 'entity', value: customerId });
order.setValue({ fieldId: 'memo', value: memo });
// ...
};
// Call with named parameters
createOrder({
customerId: 123,
items: lineItems,
priority: 'high'
});
// AVOID: Many positional parameters
const createOrder = (customerId, items, memo, priority, shipMethod, terms) => {
// Hard to remember order
};
// Confusing to call
createOrder(123, items, '', 'high', 1, null);
Error Handling Standards
/**
* Standard error handling pattern
*/
const processRecord = (recordId) => {
try {
const rec = record.load({
type: record.Type.INVOICE,
id: recordId
});
// Process...
rec.save();
return { success: true, id: recordId };
} catch (e) {
// Log with context
log.error({
title: 'processRecord Error',
details: {
recordId,
message: e.message,
stack: e.stack
}
});
// Return error info (don't swallow errors)
return {
success: false,
id: recordId,
error: e.message
};
}
};
Logging Standards
/**
* Logging levels and usage
*/
// DEBUG: Development details (filtered out in production)
log.debug('Function Entry', { function: 'processOrder', orderId });
// AUDIT: Important business events (always logged)
log.audit('Order Processed', { orderId, total: orderTotal, customer: customerName });
// ERROR: Errors that need attention
log.error('Order Failed', { orderId, error: e.message });
// EMERGENCY: Critical system failures
log.emergency('System Failure', { message: 'Cannot connect to external API' });
Structured Logging
// GOOD: Structured, searchable logs
log.audit('Invoice Created', JSON.stringify({
invoiceId: inv.id,
customerId: customerId,
amount: total,
lineCount: lineCount,
duration: Date.now() - startTime
}));
// AVOID: Unstructured strings
log.audit('Created invoice ' + inv.id + ' for customer ' + customerId);
Module Organization
Library Module Pattern
/**
* @NApiVersion 2.1
* @NModuleScope SameAccount
* @description Date utility functions
*/
define([], () => {
// Private functions (not exported)
const padZero = (num) => String(num).padStart(2, '0');
// Public functions
const formatDate = (date) => {
const d = new Date(date);
return `${d.getFullYear()}-${padZero(d.getMonth() + 1)}-${padZero(d.getDate())}`;
};
const addDays = (date, days) => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
};
const isBusinessDay = (date) => {
const day = new Date(date).getDay();
return day !== 0 && day !== 6;
};
// Export public API
return {
formatDate,
addDays,
isBusinessDay
};
});
Using Libraries
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['./Libraries/date_utils_lib', 'N/record'], (dateUtils, record) => {
const beforeSubmit = (context) => {
const dueDate = context.newRecord.getValue({ fieldId: 'duedate' });
if (!dateUtils.isBusinessDay(dueDate)) {
throw new Error('Due date must be a business day');
}
};
return { beforeSubmit };
});
Constants and Configuration
/**
* Configuration module
* @NApiVersion 2.1
* @NModuleScope SameAccount
*/
define([], () => {
// Status constants
const STATUS = Object.freeze({
PENDING: 1,
APPROVED: 2,
REJECTED: 3,
CANCELLED: 4
});
// Record types
const RECORD_TYPES = Object.freeze({
APPROVAL_REQUEST: 'customrecord_approval_request',
APPROVAL_CONFIG: 'customrecord_approval_config'
});
// Field IDs
const FIELDS = Object.freeze({
APPROVAL_STATUS: 'custbody_approval_status',
APPROVER: 'custbody_approver',
APPROVAL_DATE: 'custbody_approval_date'
});
// Thresholds
const THRESHOLDS = Object.freeze({
MANAGER_APPROVAL: 1000,
DIRECTOR_APPROVAL: 5000,
VP_APPROVAL: 25000
});
return {
STATUS,
RECORD_TYPES,
FIELDS,
THRESHOLDS
};
});
Using Constants
define(['./config', 'N/record'], (config, record) => {
const beforeSubmit = (context) => {
const total = context.newRecord.getValue({ fieldId: 'total' });
if (total > config.THRESHOLDS.VP_APPROVAL) {
context.newRecord.setValue({
fieldId: config.FIELDS.APPROVAL_STATUS,
value: config.STATUS.PENDING
});
}
};
return { beforeSubmit };
});
Code Review Checklist
┌─────────────────────────────────────────────────────────────────────────────┐
│ CODE REVIEW CHECKLIST │
└─────────────────────────────────────────────────────────────────────────────┘
STRUCTURE
☐ Uses SuiteScript 2.1 (@NApiVersion 2.1)
☐ Proper JSDoc header with description
☐ File follows naming conventions
☐ Functions are single-responsibility
STYLE
☐ Uses const/let (no var)
☐ Uses arrow functions appropriately
☐ Uses template literals for strings
☐ Consistent indentation (2 or 4 spaces)
ERROR HANDLING
☐ All external calls wrapped in try/catch
☐ Errors logged with context
☐ User-friendly error messages
LOGGING
☐ Appropriate log levels used
☐ Structured log messages
☐ No debug logs in production code
PERFORMANCE
☐ Governance checked in loops
☐ No unnecessary record loads
☐ Search results paginated if large
SECURITY
☐ No hardcoded credentials
☐ Inputs validated
☐ No eval() or dynamic code execution
Next Steps
- Error Handling - Robust error management
- Performance - Optimization techniques
- Security - Security best practices