How the Backend Works and How React Ends Up Inside a Suitelet
I've written about the general shape of these reporting apps and the component library that came out of repeating it a few times too many, but both of those posts skipped over the part that actually makes the whole thing work, how a React build ends up running inside a NetSuite Suitelet in the first place, and what's actually happening on the backend every time someone picks a date range and hits search.
One RESTlet, not one per report
Every report talks to the same RESTlet. Not a different endpoint per report, the same URL every time, with the actual work decided by an action name in the request body.
POST /app/site/hosting/restlet.nl?script=XXXX&deploy=1
{
"action": "GET_SALES_REPORT",
"payload": { "startDate": "2026-09-01", "endDate": "2026-09-30" }
}
The RESTlet reads the action, routes to whatever handler matches, and every single handler returns the same shape back, success, data, and an error if something went wrong. The frontend only has to know about one response contract no matter which report it's looking at, which turned out to matter more than I expected. Adding a new report almost never means touching the RESTlet's routing, it means adding one more case and writing the query behind it.
SuiteQL does most of the actual data pulling, grouping records, summing quantities, the kind of aggregate work it's genuinely good at. But it can't get you everything. A couple of these reports need timing data, when a task actually started versus when it finished, and that history doesn't live anywhere SuiteQL can reach, it's sitting in system notes instead. For those, the handler runs a SuiteQL query for the numbers and a separate N/search over system notes for the timestamps, padding the search window a few days on either side of whatever range was requested and batching by record ID so it doesn't blow past NetSuite's search limits. Then it stitches the two results together before sending anything back.
There's no token or OAuth check anywhere in the RESTlet itself. That sounds worse than it is. The only way to reach it is from inside the Suitelet, which means you're already in an authenticated NetSuite session before you ever get close to calling it. NetSuite's own permissions are the access control. I don't maintain a second one on top of it.
Getting React to run inside a page NetSuite generated
This is the part that took some trial and error to get right.
A Suitelet renders server side HTML. It doesn't know what Vite is, it doesn't care about your build output, it just wants to hand the browser a page. So the Suitelet's entire job, as far as the frontend is concerned, is to render a nearly empty page, a #root div and a single script tag, with the script's URL read from a script parameter configured on the deployment.
<div id="root"></div>
<script src="https://.../sales-reporting-bundle.js" defer></script>
That script is the entire React app, bundled down to a single file. Vite normally splits things into multiple chunks and expects to manage its own module loading, which doesn't really work when the only thing serving your app is a Suitelet handing out one static file. A couple of Vite plugins fix that, one that forces everything into a single IIFE instead of ES modules, and another that inlines the CSS directly into that same JS file instead of generating a separate stylesheet. End result, one script tag, no separate CSS link, no module loading NetSuite has to know anything about. The Suitelet just points at a URL and the whole app shows up.
Before that script tag loads, the Suitelet also does one more useful thing. It reads the current user, their role, and whatever subsidiary restrictions apply to them, and drops it onto the page as window.NS_USER_CONTEXT before React ever mounts. The app picks that up on load instead of making a separate request just to ask who's logged in, since the Suitelet already knew the answer.
One side effect of running inside a page NetSuite controls, routing can't behave like it would on a normal site. There's no real browser history to hook into the way react-router usually expects, you're inside NetSuite's own URL the whole time. So instead of the usual browser based router, these apps use a memory router, React Router keeps track of navigation internally without ever touching the actual address bar. Switching between reports feels like normal client side routing from inside the app, it just never tries to rewrite a URL that isn't really yours to rewrite.
What that buys you
None of this is complicated once it's built, but it took a few wrong turns to land on. A single RESTlet with action based routing instead of a pile of endpoints, SuiteQL doing the real querying with search filling in the gaps it can't reach, and a React build flattened down to exactly one file so a Suitelet can hand it out without knowing or caring that it was ever a modern frontend app in the first place.