Most "NetSuite headless commerce" guides stop at the strategy. This one shows the architecture — the API surface, the rate-limit math, the caching boundary, and the actual SuiteScript and backend-for-frontend code that makes it work.
The premise is simple: keep NetSuite as the system of record, and decouple the storefront. NetSuite stays authoritative for inventory, pricing, customers, and orders. A modern front end — React, Next.js, Shopify Hydrogen, or Medusa — talks to it through an API layer you control. Done right, you get a sub-second storefront without ever lying about stock or price.
Here is how the pieces fit together.
The architecture at a glance
Browser ──▶ Headless storefront (Next.js / Hydrogen / Medusa)
│
▼
Backend-for-Frontend (BFF) ──▶ Edge cache / CDN
│ cache · shape · auth
▼
NetSuite (system of record)
• SuiteTalk REST • SuiteQL • RESTlets
The BFF is the part teams skip and then regret. It is a thin server layer that sits between your storefront and NetSuite. It authenticates to NetSuite, shapes raw records into the exact payloads your front end needs, and — most importantly — caches, so NetSuite is not hit on every page view. Skip it and you will hit governance limits the first time a crawler or a traffic spike arrives.
NetSuite's API surface — pick the right door
NetSuite exposes several APIs, and choosing the wrong one is the most common early mistake.
| API | Best for | Notes |
|---|---|---|
| SuiteTalk REST Record | CRUD on individual records | Clean, governance-friendly, per-record |
SuiteQL (REST query or N/query) | Bulk reads, joins, search | Fastest read path; SQL-like; ideal for catalog |
| RESTlets (SuiteScript) | Custom endpoints, shaped payloads | You write the logic; full control over the response |
| SOAP SuiteTalk | Legacy integrations | Avoid for new headless builds |
For a storefront, the workhorses are SuiteQL for reads and a thin RESTlet that wraps your queries and write logic into endpoints shaped for the front end. SuiteTalk REST is fine for occasional single-record operations, but reading a catalog one record at a time will not scale.
A RESTlet that serves the catalog via SuiteQL
/**
* @NApiVersion 2.1
* @NScriptType Restlet
*/
define(['N/query'], (query) => {
const get = () => {
// SuiteQL reads are far cheaper than per-record REST calls.
// For catalogs over a few thousand rows, page the results.
const results = query
.runSuiteQL({
query: `
SELECT i.id, i.itemid, i.displayname, i.baseprice,
i.isinactive, i.custitem_brand AS brand
FROM item i
WHERE i.isinactive = 'F'
AND i.matrixtype IS NULL
ORDER BY i.itemid
`,
})
.asMappedResults();
return results;
};
return { get };
});For large catalogs use query.runSuiteQLPaged({ query, pageSize: 1000 }) and walk the pages, rather than pulling everything in one call. The point stands: one shaped query beats thousands of record reads.
The rate-limit reality
This is the constraint that defines the whole design. NetSuite governs concurrency — only a handful of simultaneous connections by default, expandable through SuiteCloud Plus licensing — and each script operation consumes a governance budget. A headless storefront that calls NetSuite synchronously on every request will exhaust that budget under normal traffic.
The fix is not "buy more concurrency." The fix is to stop calling NetSuite on the hot path. Read in bulk, cache the result, and revalidate on a schedule. Reserve live NetSuite calls for the few things that genuinely need to be fresh: stock availability at add-to-cart, and order creation at checkout.
Pattern 1 — the read path (bulk + cache)
Your storefront should read the catalog from cache, not from NetSuite. Here is a Next.js route handler acting as the BFF: it fetches from the RESTlet, caches the response, and serves it with stale-while-revalidate so a slow NetSuite never blocks a shopper.
// app/api/catalog/route.ts
import { NextResponse } from 'next/server';
import { oauthHeader } from '@/lib/netsuite-auth';
const RESTLET_URL = process.env.NETSUITE_CATALOG_RESTLET!;
export async function GET() {
// NetSuite is the source of truth — but it should not be hit per page view.
// Cache for 5 minutes; serve stale up to 10 while revalidating.
const res = await fetch(RESTLET_URL, {
headers: { Authorization: oauthHeader('GET', RESTLET_URL) },
next: { revalidate: 300, tags: ['catalog'] },
});
if (!res.ok) {
// Fail soft: serve the last good cache rather than a broken page.
return NextResponse.json({ error: 'upstream_unavailable' }, { status: 502 });
}
const items = await res.json();
return NextResponse.json(items, {
headers: { 'Cache-Control': 's-maxage=300, stale-while-revalidate=600' },
});
}When a price or product changes in NetSuite, you do not wait five minutes — you invalidate the tag (revalidateTag('catalog')) from a NetSuite User Event script via a webhook. That gives you cached speed with near-real-time correctness.
Pattern 2 — the backend-for-frontend layer
The BFF is where you decide what is real-time versus cached, and where you keep NetSuite credentials off the client entirely. A useful rule of thumb:
- Cache and revalidate: product data, descriptions, images, category structure, list pricing.
- Read live (short TTL): stock availability, customer-specific contract pricing at checkout.
- Write live (idempotent): cart-to-order, account updates.
Shaping data here also means your front end never sees a raw NetSuite record. It receives exactly the fields it renders — which keeps payloads small and your storefront code clean.
Pattern 3 — the write path (idempotency + backoff)
Order creation is the operation you cannot get wrong. A network blip must never create two sales orders. Two defenses handle this: an idempotency key (use NetSuite's externalId so a retried request maps to the same record) and exponential backoff on rate-limited or transient failures.
// lib/netsuite-orders.ts
export async function createSalesOrder(
payload: SalesOrderInput,
idempotencyKey: string,
) {
const url = `${process.env.NETSUITE_ORDERS_RESTLET}`;
const maxAttempts = 5;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const res = await fetch(url, {
method: 'POST',
headers: {
Authorization: oauthHeader('POST', url),
'Content-Type': 'application/json',
// The RESTlet uses this as externalId, so a retry maps to the same order.
'X-Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(payload),
});
// Success or a real client error (bad data): stop and return.
if (res.status !== 429 && res.status < 500) {
return res.json();
}
// Rate limited or transient 5xx: back off with jitter and retry.
const delay = Math.min(2 ** attempt * 250, 8000) + Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, delay));
}
throw new Error('NetSuite order write failed after retries');
}On the NetSuite side, the RESTlet looks up the externalId before creating: if an order with that key already exists, it returns the existing record instead of creating a duplicate. That single check is the difference between a reliable checkout and a support queue full of double orders.
Caching strategy in one paragraph
The whole design lives or dies on the cache boundary. Treat NetSuite as a database you read from rarely and write to carefully — not as a real-time API behind every page. Cache catalog and list pricing aggressively with tag-based invalidation. Keep stock and contract pricing on a short TTL or a live call at the moments that matter (add-to-cart, checkout). Push NetSuite changes outward with User Event webhooks instead of polling. Get this boundary right and a headless NetSuite storefront is genuinely fast; get it wrong and no amount of frontend polish will save it.
Anti-patterns we get called in to fix
- NetSuite as a live read API. Querying NetSuite on every page render. It will hit governance limits and feel slow even when nothing is wrong.
- No caching layer. Going "headless" without a BFF and cache just moves the monolith's slowness somewhere new.
- Over-syncing. Mirroring data that changes monthly as if it changed every second.
- Ignoring concurrency until launch. Load-test against NetSuite's real limits before go-live, not after.
- Rebuilding checkout from scratch when a proven path (SuiteCommerce checkout, or Shopify checkout with NetSuite as ERP) already solves it.
Which path should you build on?
The architecture above applies whether your front end is custom React, Shopify Hydrogen, or Medusa — what changes is how much you build yourself. SuiteCommerce keeps it Oracle-native and fastest to launch. Shopify plus NetSuite pairs a best-in-class storefront with NetSuite as ERP and order management. Medusa plus NetSuite gives you a fully composable, code-owned stack.
We walk through that decision in depth on the NetSuite headless commerce page, and compare the platforms in the SuiteCommerce vs Shopify guide. If you are still mapping options, start with the NetSuite ecommerce guide.
Medusa + NetSuite, already built: Gemstone
Everything above — the SuiteQL reads, the backend-for-frontend layer, the caching boundary, idempotent order writes, rate-limit handling — is exactly the work that goes into connecting Medusa to NetSuite properly. You can build it yourself with the patterns in this guide. But you don't have to.
We've spent a long time building Gemstone, our Medusa-to-NetSuite connector, and it is already running, tested, and production-ready. It handles the read and write paths, the caching, and the reliability work so your team doesn't have to reinvent any of it. If you want a headless commerce storefront on Medusa with NetSuite as the system of record, we can stand up a working example in your NetSuite sandbox in 48 hours — and we build the full storefront as a service.
Want Medusa + NetSuite without building the connector?
Gemstone is our tested, production-ready Medusa-to-NetSuite connector. We can have a working example running in your NetSuite sandbox in 48 hours, then build the full storefront for you.
Talk to us about GemstoneWhat clients ask before signing

BrokenRubik
NetSuite Development Agency
Expert team specializing in NetSuite ERP, SuiteCommerce development, and enterprise integrations. Oracle NetSuite partner with 8+ years of experience delivering scalable solutions for mid-market and enterprise clients worldwide.
Related Articles
Composable Commerce: MACH Architecture & NetSuite
What composable commerce is, how MACH architecture works, and when best-of-breed beats monolithic. Plus how NetSuite fits as the ERP backbone.
NetSuite B2B Portal: SuiteCommerce vs Alternatives
Compare every NetSuite B2B portal option — SuiteCommerce, MyAccount, and third-party alternatives. Real costs, features, and which approach fits your use case.
NetSuite Ecommerce Platform: The Complete Guide
SuiteCommerce costs $2.5K-5K/mo, Shopify integration $600-2K/mo. Compare every NetSuite ecommerce option with real pricing and when each one makes sense.
BrokenRubik