npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@brokenrubik/netsuite-sdk

v0.2.0

Published

Production-ready NetSuite REST API client SDK with OAuth 2.0 M2M authentication, retry logic, RESTlet support, and SuiteQL.

Downloads

486

Readme

@brokenrubik/netsuite-sdk

CI

Production-ready TypeScript SDK for the NetSuite REST API with OAuth 2.0 M2M authentication.

Features

  • OAuth 2.0 M2M authentication with certificate-based JWT (PS256)
  • Full CRUD operations for NetSuite records + upsert via external IDs
  • SuiteQL query execution with auto-pagination
  • RESTlet support (custom scripts via restlets.api.netsuite.com)
  • Metadata catalog access (JSON, OpenAPI 3.0, JSON Schema)
  • Automatic retry with exponential backoff + jitter (429, 502, 503, 504)
  • Structured error hierarchy with parsed o:errorDetails
  • Per-scope token caching (SuiteTalk, RESTlets, Analytics)
  • TypeScript-first with full generics support
  • Dual CJS + ESM output
  • Zero dependencies beyond jsonwebtoken

Table of Contents

Installation

npm install @brokenrubik/netsuite-sdk

Requires Node.js >= 18.0.0 (uses native fetch)

Prerequisites

Before using this SDK, you need to set up OAuth 2.0 M2M authentication in your NetSuite account. This requires four credentials:

| Credential | Where to find it | |---|---| | Account ID | Setup > Company > Company Information (e.g., TSTDRV1234 or TSTDRV1234_SB1 for sandbox) | | Client ID | Setup > Integration > Manage Integrations > your integration record | | Certificate ID | Setup > Integration > OAuth 2.0 Client Credentials > Manage Certificates | | Private Key | The PEM file you generated when creating the certificate |

Step-by-step setup

  1. Generate a certificate and private key:

    # Generate a 4096-bit RSA private key
    openssl genrsa -out private.pem 4096
    
    # Extract the public key
    openssl rsa -in private.pem -pubout -out public.pem
  2. Create an Integration Record in NetSuite:

    • Navigate to Setup > Integration > Manage Integrations > New
    • Enable Token-Based Authentication and OAuth 2.0 Client Credentials (M2M)
    • Save and note the Client ID
  3. Upload the public certificate to NetSuite:

    • Navigate to Setup > Integration > OAuth 2.0 Client Credentials (M2M) > Create New
    • Select the integration record, upload public.pem
    • Assign a role (e.g., Administrator or a custom role)
    • Save and note the Certificate ID
  4. Store your private key securely. For environment variables, you can encode it as a single line:

    # Option A: Escape newlines for .env files
    NETSUITE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...base64...\n-----END PRIVATE KEY-----"
    
    # Option B: Read from file at runtime
    const privateKey = fs.readFileSync('/path/to/private.pem', 'utf-8');

For more details, see the official NetSuite OAuth 2.0 setup guide.

Quick Start

import { NetSuiteClient } from '@brokenrubik/netsuite-sdk';
import { readFileSync } from 'node:fs';

const client = new NetSuiteClient({
  accountId: process.env.NETSUITE_ACCOUNT_ID!,
  clientId: process.env.NETSUITE_CLIENT_ID!,
  certificateId: process.env.NETSUITE_CERTIFICATE_ID!,
  privateKey: readFileSync('./private.pem', 'utf-8'),
});

// Create a customer
const { id } = await client.createRecord('customer', {
  companyName: 'Acme Corporation',
  email: '[email protected]',
  subsidiary: { id: '1' },
});
console.log(`Created customer: ${id}`);

// Fetch it back
const customer = await client.getRecord('customer', id);
console.log(customer);

// Run a SuiteQL query
const results = await client.suiteql<{ id: string; companyname: string }>(
  'SELECT id, companyname FROM customer WHERE ROWNUM <= 10',
);
console.log(`Found ${results.totalResults} customers`);

Configuration

const client = new NetSuiteClient({
  // Required
  accountId: 'TSTDRV1234_SB1',  // Supports underscores — auto-formatted to hyphens for API URLs
  clientId: 'abc123...',          // From Integration Record
  certificateId: 'cert-id...',   // From OAuth 2.0 Client Credentials setup
  privateKey: '-----BEGIN...',   // PEM-formatted private key string

  // Optional — all have sensible defaults
  requestTimeout: 30_000,        // Per-request timeout in ms (default: 30s)
  tokenExpiryMargin: 60_000,     // Refresh token this many ms before expiry (default: 60s)
  maxRetries: 3,                 // Retry attempts for transient errors (default: 3)
  initialRetryDelay: 1_000,      // First retry delay in ms (default: 1s)
  maxRetryDelay: 60_000,         // Retry delay cap in ms (default: 60s)
  backoffMultiplier: 2,          // Exponential backoff factor (default: 2)
  logger: new ConsoleLogger(),   // Optional ILogger — see Logging section
});

Common Use Cases

Working with Records

Built-in record types

The SDK ships with type definitions for common record types, generated from the NetSuite metadata catalog. Use them directly or extend with your custom fields:

import type { Customer, SalesOrder, NsRef } from '@brokenrubik/netsuite-sdk';

// Use built-in types directly
const customer = await client.getRecord<Customer>('customer', '123');
console.log(customer.companyName); // typed as string | null
console.log(customer.subsidiary);  // typed as NsRef ({ id, refName? })

// Extend with your custom fields
interface MyCustomer extends Customer {
  custentity_loyalty_tier: NsRef | null;
  custentity_signup_source: string | null;
}

const myCustomer = await client.getRecord<MyCustomer>('customer', '123');
console.log(myCustomer.custentity_loyalty_tier); // typed!

Available types: Customer, SalesOrder, Invoice, Vendor, Employee, InventoryItem, Contact, Subsidiary, VendorBill, PurchaseOrder, Department, Classification, Location, Account, Currency.

Building blocks: NsRef (reference field), NsSublist (sublist), NsRecord (base record), NsLink (HATEOAS link).

Record field loading

NetSuite has three loading levels that affect what you get back:

| Load level | Reference fields (e.g. entity) | Sublists (e.g. item) | |---|---|---| | getRecord(type, id) | { id, refName, links } — always a ref, never the full record | { links } — collapsed, no items | | getRecord(type, id, { expandSubResources: true }) | { id, refName, links }same | { links, items: [...], totalResults } — items loaded | | getRecord(type, id, { fields: '...' }) | { id, refName, links } — if requested | Not returned (only body fields) |

Reference fields are never fully loaded inline. To get the full referenced record, follow the link:

import type { SalesOrder, SalesOrderItem, Customer } from '@brokenrubik/netsuite-sdk';

// Load a sales order with expanded sublists
const order = await client.getRecord<SalesOrder>('salesOrder', '456', {
  expandSubResources: true,
});

// entity is always { id, refName, links } — never the full customer
console.log(order.entity.refName);  // "Acme Corp"
console.log(order.entity.id);       // "123"

// To load the full customer, make a separate call:
const customer = await client.getRecord<Customer>('customer', order.entity.id);

// item sublist is expanded (because expandSubResources: true)
for (const line of order.item.items ?? []) {
  console.log(`${line.item.refName}: ${line.quantity} x ${line.rate} = ${line.amount}`);
}

Create a record

const result = await client.createRecord('customer', {
  companyName: 'Acme Corporation',
  email: '[email protected]',
  subsidiary: { id: '1' },
});

console.log(result);
// {
//   id: '12345',
//   location: 'https://tstdrv1234.suitetalk.api.netsuite.com/.../customer/12345',
//   success: true,
//   operationId: '...',   // NetSuite operation tracking ID
// }

Get a record by ID

// Simple get
const customer = await client.getRecord<Customer>('customer', '12345');

// With sublists and subrecords expanded
const full = await client.getRecord<Customer>('customer', '12345', {
  expandSubResources: true,
});

// Only specific fields (reduces payload size)
const partial = await client.getRecord<Pick<Customer, 'id' | 'email'>>('customer', '12345', {
  fields: 'id,email',
});

Note: You cannot combine expandSubResources and fields — NetSuite returns an INVALID_PARAMETER error.

List records with filtering

// Single page with query filter
const page = await client.listRecords<Customer>('customer', {
  limit: 50,
  offset: 0,
  q: 'email CONTAINS "@acme.com"',
});

console.log(`Showing ${page.count} of ${page.totalResults} total`);
for (const customer of page.items) {
  console.log(customer.companyName);
}

Fetch all records (auto-pagination)

// Automatically fetches all pages (1000 records per page)
const allCustomers = await client.listAllRecords<Customer>('customer', {
  q: 'isinactive IS false',
});

console.log(`Total active customers: ${allCustomers.length}`);

Update a record

const result = await client.updateRecord('customer', '12345', {
  email: '[email protected]',
  phone: '555-0100',
});

console.log(result.success); // true

Delete a record

const result = await client.deleteRecord('customer', '12345');
console.log(result.success); // true

Upsert a record (create or update by external ID)

// If customer with external ID 'CID-002' exists, update it; otherwise, create it
const result = await client.upsertRecord('customer', 'CID-002', {
  companyName: 'Updated Corp',
  email: '[email protected]',
  subsidiary: { id: '1' },
});

// The 'eid:' prefix is added automatically — these are equivalent:
await client.upsertRecord('customer', 'CID-002', data);
await client.upsertRecord('customer', 'eid:CID-002', data);

Working with custom records

// Custom records use their scriptId as the record type
const result = await client.createRecord('customrecord_my_record', {
  name: 'Test Entry',
  custrecord_field1: 'value1',
});

const record = await client.getRecord('customrecord_my_record', result.id);

SuiteQL Queries

SuiteQL lets you query NetSuite data using SQL-like syntax. It's the most powerful way to retrieve data — supports JOINs, aggregation, and ordering (which the Record API doesn't).

Basic query

const result = await client.suiteql<{ id: string; companyname: string }>(
  'SELECT id, companyname FROM customer WHERE companyname LIKE \'%Acme%\' ORDER BY companyname',
  { limit: 100 },
);

for (const row of result.items) {
  console.log(`${row.id}: ${row.companyname}`);
}

Pagination

// Manual pagination
let offset = 0;
const pageSize = 1000;

while (true) {
  const page = await client.suiteql<{ id: string }>(
    'SELECT id FROM transaction WHERE type = \'SalesOrd\'',
    { limit: pageSize, offset },
  );

  console.log(`Page at offset ${offset}: ${page.items.length} rows`);

  if (!page.hasMore) break;
  offset += page.items.length;
}

Auto-paginate all results

// Automatically fetches all pages (up to 100,000 rows, NetSuite limit)
const allOrders = await client.suiteqlAll<{
  id: string;
  tranid: string;
  total: string;
}>('SELECT id, tranid, total FROM transaction WHERE type = \'SalesOrd\' ORDER BY tranid');

console.log(`Total orders: ${allOrders.length}`);

JOINs and aggregation

// Count orders per customer
const stats = await client.suiteql<{
  companyname: string;
  order_count: string;
}>(`
  SELECT c.companyname, COUNT(t.id) as order_count
  FROM customer c
  INNER JOIN transaction t ON t.entity = c.id
  WHERE t.type = 'SalesOrd'
  GROUP BY c.companyname
  ORDER BY order_count DESC
`);

for (const row of stats.items) {
  console.log(`${row.companyname}: ${row.order_count} orders`);
}

RESTlets

RESTlets are custom SuiteScript endpoints deployed in NetSuite. They use a different hostname (restlets.api.netsuite.com) and OAuth scope (restlets), which the SDK handles automatically.

// GET request to a RESTlet
const searchResults = await client.callRestlet({
  scriptId: '1234',
  deployId: '1',
  method: 'GET',
  queryParams: { action: 'search', term: 'widget' },
});

// POST request with a body
const created = await client.callRestlet({
  scriptId: '1234',
  deployId: '1',
  method: 'POST',
  body: {
    recordType: 'inventoryitem',
    values: { itemid: 'WIDGET-001', displayname: 'Widget' },
  },
});

// Custom headers
const result = await client.callRestlet({
  scriptId: '1234',
  deployId: '1',
  method: 'POST',
  body: { data: 'value' },
  headers: { 'X-Custom-Header': 'my-value' },
});

Tip: The SDK automatically uses the restlets OAuth scope and the restlets.api.netsuite.com hostname. Token caching is per-scope, so SuiteTalk and RESTlet tokens are managed independently.

Metadata Catalog

The metadata catalog describes the structure of all available record types, including custom records and fields.

// Fetch the full catalog
const catalog = await client.getMetadata();

// Fetch metadata for a specific record type
const customerMeta = await client.getMetadata({ select: 'customer' });

// Get OpenAPI 3.0 (Swagger) definition
const swagger = await client.getMetadata({
  select: 'customer',
  format: 'openapi3',
});

// Get JSON Schema definition
const schema = await client.getMetadata({
  select: 'salesorder',
  format: 'jsonschema',
});

Available formats:

| Format | Accept header | Use case | |---|---|---| | 'json' (default) | application/json | Browse record structure | | 'openapi3' | application/swagger+json | Generate API clients, import into Postman | | 'jsonschema' | application/schema+json | Data validation |

Error Handling

The SDK provides a structured error hierarchy that maps to NetSuite's error response format:

NetSuiteError (base)
  ├── NetSuiteAuthError          — 401/403 authentication failures
  ├── NetSuiteValidationError    — 400 with parsed o:errorDetails
  ├── NetSuiteNotFoundError      — 404 record/type not found
  ├── NetSuiteRateLimitError     — 429 concurrency limit exceeded
  └── NetSuiteTimeoutError       — request timeout

Catching specific errors

import {
  NetSuiteError,
  NetSuiteAuthError,
  NetSuiteValidationError,
  NetSuiteNotFoundError,
  NetSuiteRateLimitError,
  NetSuiteTimeoutError,
} from '@brokenrubik/netsuite-sdk';

try {
  await client.createRecord('customer', { /* missing required fields */ });
} catch (error) {
  if (error instanceof NetSuiteValidationError) {
    // 400 Bad Request — NetSuite tells you exactly what's wrong
    console.error('Validation failed:', error.message);
    console.error('Error code:', error.errorCode);   // e.g., 'REQUIRED_FIELD'
    console.error('Error path:', error.errorPath);    // e.g., '/companyname'

    // Full list of validation issues
    for (const detail of error.errorDetails) {
      console.error(`  - ${detail.errorCode}: ${detail.detail} (${detail.errorPath})`);
    }
  } else if (error instanceof NetSuiteNotFoundError) {
    // 404 — record or record type doesn't exist
    console.error('Not found:', error.message);
  } else if (error instanceof NetSuiteAuthError) {
    // 401/403 — credentials are wrong or expired
    console.error('Authentication failed:', error.message);
    console.error('Check your clientId, certificateId, and privateKey');
  } else if (error instanceof NetSuiteRateLimitError) {
    // 429 — too many concurrent requests
    // Note: The SDK already retries these automatically (maxRetries times)
    // This only fires if all retries are exhausted
    console.error('Concurrency limit exceeded after all retries');
  } else if (error instanceof NetSuiteTimeoutError) {
    // Request took longer than requestTimeout
    console.error('Request timed out');
  } else if (error instanceof NetSuiteError) {
    // Any other NetSuite API error (500, etc.)
    console.error(`API Error ${error.status}: ${error.message}`);
    console.error('Response body:', error.body);
  }
}

Error properties

All errors extend NetSuiteError and include:

| Property | Type | Description | |---|---|---| | message | string | Human-readable error message | | status | number | HTTP status code (0 for timeouts) | | statusCode | number | Alias for status | | body | Record<string, unknown> | Raw response body (if JSON) |

NetSuiteValidationError adds:

| Property | Type | Description | |---|---|---| | errorDetails | NetSuiteErrorDetail[] | Parsed list of validation errors | | errorCode | string | First error's code (e.g., REQUIRED_FIELD) | | errorPath | string | First error's path (e.g., /companyname) |

Automatic retries

The SDK automatically retries requests that fail with transient errors:

  • 429 — Rate limit / concurrency limit exceeded
  • 502 — Bad gateway
  • 503 — Service unavailable
  • 504 — Gateway timeout
  • Network errorsECONNRESET, ETIMEDOUT, ECONNREFUSED

Retries use exponential backoff with jitter (e.g., 1s, 2s, 4s with ±10% randomization). 500 errors are NOT retried — per NetSuite docs, these indicate server-side issues that require contacting support.

Logging

Built-in console logger

import { NetSuiteClient, ConsoleLogger } from '@brokenrubik/netsuite-sdk';

const client = new NetSuiteClient({
  ...credentials,
  logger: new ConsoleLogger('MyApp'), // Prefix for log messages
});

// Output:
// [MyApp] Requesting new access token { scope: 'rest_webservices' }
// [MyApp] Access token obtained { scope: 'rest_webservices', expiresIn: 3600 }
// [MyApp] Creating record { recordType: 'customer' }

Custom logger (pino, winston, etc.)

Implement the ILogger interface:

import type { ILogger } from '@brokenrubik/netsuite-sdk';
import pino from 'pino';

const pinoInstance = pino();

const logger: ILogger = {
  debug: (msg, data) => pinoInstance.debug(data, msg),
  info: (msg, data) => pinoInstance.info(data, msg),
  warn: (msg, data) => pinoInstance.warn(data, msg),
  error: (msg, err) => pinoInstance.error(err, msg),
};

const client = new NetSuiteClient({
  ...credentials,
  logger,
});

Silent mode (default)

If you don't pass a logger, the SDK uses a NoopLogger internally — no output at all.

Advanced Usage

Low-level requests

For endpoints not covered by the SDK methods, use request() directly:

// Hit any SuiteTalk endpoint
const result = await client.request(
  '/services/rest/record/v1/invoice/123',
  'GET',
  { headers: { Accept: 'application/schema+json' } },
);

// Hit a restlets endpoint
const restletResult = await client.request(
  '/app/site/hosting/restlet.nl?script=100&deploy=1',
  'POST',
  {
    body: { action: 'custom' },
    baseHost: 'restlets',
    scope: 'restlets',
  },
);

Token management

// Check if a valid token is cached
client.hasValidToken();              // rest_webservices scope (default)
client.hasValidToken('restlets');    // restlets scope

// Get token expiration timestamp (ms since epoch, or null)
const exp = client.getTokenExpiration();
if (exp) {
  console.log(`Token expires at: ${new Date(exp).toISOString()}`);
}

// Force token refresh on next request
client.clearTokenCache();            // Clear all scopes
client.clearTokenCache('restlets');  // Clear specific scope

Bulk operations with SuiteQL

// Export all active inventory items
const items = await client.suiteqlAll<{
  id: string;
  itemid: string;
  displayname: string;
  quantityavailable: string;
}>(`
  SELECT id, itemid, displayname, quantityavailable
  FROM item
  WHERE isinactive = 'F'
    AND itemtype = 'InvtPart'
  ORDER BY itemid
`);

console.log(`Exported ${items.length} inventory items`);

Sync pattern: find-or-create

async function findOrCreateCustomer(
  client: NetSuiteClient,
  externalId: string,
  data: Record<string, unknown>,
) {
  // Check if customer exists via SuiteQL
  const existing = await client.suiteql<{ id: string }>(
    `SELECT id FROM customer WHERE externalid = '${externalId}'`,
    { limit: 1 },
  );

  if (existing.items.length > 0) {
    // Update existing
    return client.updateRecord('customer', existing.items[0].id, data);
  } else {
    // Create new
    return client.createRecord('customer', { ...data, externalid: externalId });
  }
}

Or, use the built-in upsert which does this atomically:

const result = await client.upsertRecord('customer', externalId, data);

API Reference

NetSuiteClient

| Method | Signature | Description | |---|---|---| | getRecord | <T>(type, id, options?) => Promise<T> | Get a record by internal ID | | listRecords | <T>(type, options?) => Promise<PaginatedResponse<T>> | List records (single page) | | listAllRecords | <T>(type, options?) => Promise<T[]> | List all records (auto-paginate) | | createRecord | (type, data) => Promise<MutationResult> | Create a new record | | updateRecord | (type, id, data) => Promise<MutationResult> | Update a record by ID | | deleteRecord | (type, id) => Promise<MutationResult> | Delete a record by ID | | upsertRecord | (type, externalId, data) => Promise<MutationResult> | Create or update by external ID | | suiteql | <T>(query, options?) => Promise<PaginatedResponse<T>> | Run a SuiteQL query (single page) | | suiteqlAll | <T>(query) => Promise<T[]> | Run a SuiteQL query (auto-paginate) | | callRestlet | <T>(options) => Promise<T> | Call a RESTlet script | | getMetadata | <T>(options?) => Promise<T> | Fetch metadata catalog | | request | <T>(endpoint, method, options?) => Promise<T> | Low-level request | | hasValidToken | (scope?) => boolean | Check if a cached token is valid | | clearTokenCache | (scope?) => void | Clear cached tokens | | getTokenExpiration | (scope?) => number \| null | Get token expiration timestamp |

Exported Types

import type {
  // Client
  NetSuiteClientOptions,
  ILogger,

  // Records — options & results
  GetRecordOptions,     // { expandSubResources?, simpleEnumFormat?, fields? }
  ListRecordsOptions,   // { limit?, offset?, q? }
  MutationResult,       // { id, location, success, operationId?, jobId?, propertyValidation? }

  // Records — type definitions (from metadata catalog)
  NsRef,                // { id: string; refName?: string } — reference field
  NsSublist,            // { links, items?, totalResults?, ... } — sublist
  NsRecord,             // Base: { id, externalId?, links, refName }
  NsLink,               // { rel: string; href: string }
  NsRefLink,            // NsRef + links — actual shape of reference fields
  Customer,
  SalesOrder,
  Invoice,
  Vendor,
  Employee,
  InventoryItem,
  Contact,
  Subsidiary,
  VendorBill,
  PurchaseOrder,
  Department,
  Classification,       // "Class" in NetSuite UI
  Location,
  Account,
  Currency,

  // SuiteQL
  SuiteQLOptions,       // { limit?, offset? }

  // RESTlets
  RestletOptions,       // { scriptId, deployId, method, body?, queryParams?, headers? }

  // Metadata
  MetadataFormat,       // 'json' | 'openapi3' | 'jsonschema'
  MetadataOptions,      // { select?, format? }

  // Common
  HttpMethod,           // 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS'
  AuthScope,            // 'rest_webservices' | 'restlets' | 'suite_analytics'
  PaginatedResponse,    // { items, count, hasMore, offset, totalResults, links? }
  Link,                 // { rel, href }
  RequestOptions,       // { headers?, baseHost?, scope? }

  // Errors
  NetSuiteErrorDetail,  // { detail, errorCode, errorPath?, urlPath? }
} from '@brokenrubik/netsuite-sdk';

Exported Classes

import {
  NetSuiteClient,

  // Errors
  NetSuiteError,
  NetSuiteAuthError,
  NetSuiteValidationError,
  NetSuiteNotFoundError,
  NetSuiteRateLimitError,
  NetSuiteTimeoutError,

  // Logger utilities
  ConsoleLogger,
  NoopLogger,
} from '@brokenrubik/netsuite-sdk';

Running Tests

Unit tests

npm test             # Run all tests
npm run test:unit    # Unit tests only
npm run test:coverage # With coverage report (100% stmt/line/func, 94% branch)

Integration tests

Integration tests run against a real NetSuite sandbox. Copy the env template and fill in your credentials:

cp .env.template .env
NETSUITE_ACCOUNT_ID=TSTDRV1234_SB1
NETSUITE_CLIENT_ID=your-client-id
NETSUITE_CERTIFICATE_ID=your-cert-id
NETSUITE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----"

# Optional: RESTlet testing
NETSUITE_RESTLET_SCRIPT_ID=1234
NETSUITE_RESTLET_DEPLOY_ID=1

Then run:

npm run test:integration

Integration tests cover:

  • Full CRUD lifecycle (create, get, get+fields, get+expand, update, list, delete)
  • Reading multiple record types (subsidiary, currency, account, department, item)
  • Pagination with offset and next links
  • Record filtering with q parameter
  • SuiteQL: SELECT, WHERE, ORDER BY, COUNT, GROUP BY, JOIN, LIKE, pagination, empty results
  • Metadata catalog in JSON, OpenAPI 3.0, and JSON Schema formats
  • Error handling for non-existent records, invalid record types, and invalid SQL

Tests skip automatically if credentials are not set.

CI

GitHub Actions runs on every PR and push to main:

| Job | What it does | Required to merge | |---|---|---| | Unit Tests | Runs on Node 18, 20, 22 | Yes | | Coverage | Enforces coverage thresholds | Yes | | Integration Tests | Runs against real NetSuite sandbox (via secrets) | No (informational) |

Branch protection on main requires Unit Tests and Coverage to pass.

Official NetSuite Documentation

License

ISC