j-riv@localhost:~$

Building Reporting Apps Inside NetSuite

A good chunk of my work lately has been building small reporting apps that live inside NetSuite itself. Not a separate dashboard somewhere else that people have to remember to go check, but something genuinely embedded in a Suitelet, so whoever needs it just opens a page inside NetSuite and it's already there.

I've built a handful of these at this point for different teams and different data, and they all end up following roughly the same shape. Figured it was worth writing up the pattern instead of any one specific app.

The problem these solve

NetSuite's built in reporting is fine for a lot of things, but it starts to fall apart once you need something more specific. Saved searches can only get you so far, and the people who actually need the data, warehouse staff, operations leads, whoever's watching a particular number day to day, usually don't want to dig through a saved search to get it. They want something closer to a real dashboard. Filter by date, see the numbers, click into something for detail, export it if they need to hand it off.

Building that as a standalone app outside of NetSuite is an option, but then you're maintaining a separate login, separate hosting, and some way to keep it in sync with whatever is actually happening in NetSuite. Embedding it skips all of that.

How it's put together

The frontend is a normal React app, built with Vite. The only real twist is in how it gets deployed. A Suitelet can't serve a whole folder of assets the way a typical static host can, so the build gets bundled down into a single file. The Suitelet itself just renders a bare page with an empty root element and a script tag pointing at that bundle, and the React app takes it from there.

The Suitelet also hands the frontend a bit of context on load, who's logged in, their role, that kind of thing, so the app can use it without having to ask for it separately.

On the backend side there's a RESTlet that the frontend talks to over a single endpoint. Every request is the same shape, an action name and a payload, and every response comes back the same way too, success, data, and an error if something went wrong. Adding a new report usually just means adding a new action to that same RESTlet rather than standing up a new endpoint.

For the actual data, SuiteQL does most of the heavy lifting. It's good at the aggregate stuff, grouping records, summing quantities, that sort of thing. But it can't get you everything. A few of these reports also need timing data, when a task actually started and finished, and that kind of history lives in system notes rather than anywhere SuiteQL can reach. So those reports end up pulling from both, SuiteQL for the numbers and a search over system notes to fill in the timing, then stitching the two together before sending it back.

Why embed it at all

The honest answer is auth. If this were a standalone app I'd need to build and maintain a login system, figure out permissions, keep it reasonably secure, and make sure it stays in sync with who should actually have access. Embedding it in NetSuite means none of that is my problem. If you can get to the Suitelet, NetSuite has already decided you're allowed to be there. The app just needs to render something useful once you arrive.

It also means one less thing for people to remember. Nobody has to bookmark a separate dashboard or remember another password. It's just another page inside the system they're already working in all day.

Putting it to use

The first real test was a sales report, order totals broken out by rep and by date range, the kind of thing sales leadership actually opens every week instead of waiting on someone to build them a spreadsheet. Date filters, a quick search across customers and order numbers, and a totals footer doing the math automatically instead of someone exporting rows and adding them up by hand. Work order production followed not long after, different filters, different columns, same table and same primitives underneath. Every report since then has mostly been deciding what the response shape and filters look like, then wiring the existing pieces together instead of starting from a blank page.

The part that doesn't change

Every one of these ends up with the same rough skeleton, a React frontend bundled for a Suitelet, a single RESTlet acting as the API, and SuiteQL doing most of the querying with search filling in whatever gaps show up. The specifics change every time, different records, different metrics, different teams asking for it, but the shape underneath is basically the same app wearing a different report.

At some point I'll probably turn this into an actual reusable template instead of rebuilding the same scaffolding each time. For now, knowing the pattern by heart is apparently good enough.