The Frontend: Render Anything From JSON
A build case study of Orca, SpatialX's gigapixel cancer-AI platform. • Day 4 opens the Frontend box from Day 3. Its whole job is to be a faithful, generic renderer: the backend describes a page as a list of section objects, and the frontend draws each one by type — so a new screen is a JSON edit, not new code.
The frontend's one job: render JSON
On Day 3 the rule was that the backend serves the UI as data. That makes the frontend's job narrow and precise: it is a generic renderer — one program that draws whatever the backend describes, instead of a hand-built screen for each page. It does not decide what a page contains; it only knows how to turn a description into pixels.
Orca ships this as two React and TypeScript apps: a main explore app (projects, canvas, viewer, annotations, AI models) and a separate secure viewer that streams images straight from S3. Both obey the Day 3 boundary — they talk only to the API Gateway, never to a database or compute directly.
Keeping the client dumb is the point. If the frontend never invents a layout or a URL of its own, the backend stays in full control of both the UI and the data — and there is only one render path to get right.
The stack: fewer moving parts
The stack is chosen for simplicity, not novelty — the bar is that a new engineer is productive on day one. Each row below is the lowest-maintenance tool that does its job. A few terms glossed: Vite is the fast build/dev tool; Tailwind is utility-CSS for styling; OpenLayers is a map library reused here to render image tiles and vector annotations; React Query + Axios fetch data and cache it; Jest + React Testing Library (RTL) run the tests.
| Layer | Technology | Why it is here |
|---|---|---|
| Framework | React + TypeScript | One component model, typed props |
| Build / dev | Vite | Fast startup and hot reload |
| Styling | Tailwind CSS | Styling without a separate CSS system |
| Canvas | OpenLayers | Tile + annotation rendering for gigapixel slides |
| Data | React Query + Axios | Fetch, cache, and revalidate server data |
| Tests | Jest + React Testing Library | Verify sections in isolation |
The discipline is config over code: a new screen, a new workflow, or a new job should be a JSON edit, not a new architecture. Fewer dependencies and one renderer mean there is simply less to learn.
A page is a list of sections
The core idea of the frontend is one sentence: a page is an ordered list of sections. A section is a self-contained region of the UI described by a small JSON object — it names its type, a title, the data it shows, and (as we will see) the actions it can perform. The backend decides the layout by choosing which sections to list and in what order.
Here is the smallest possible page — a name and an empty list. It renders to a blank projects page. Nothing bespoke exists yet, and that is the point.
{
"page": "projects",
"sections": []
}
Grow the list and the screen grows with it. Because the layout is the list, the backend can rearrange, add, or remove regions of a page without the frontend shipping a single new line.
The generic renderer and the component registry
How does the frontend turn "type": "image_list" into an actual list on screen? Through a component registry: a lookup table that maps each section type name to a React component. The renderer walks the sections in order, looks each type up in the registry, mounts the matching component, and hands it the section's data. The screen you see is just those components stacked in the listed order.
This is the whole server-driven idea in one flow: the backend lists sections, the JSON crosses the single API Gateway, and one generic renderer draws each region by type.
Adding a new kind of surface is therefore small: one new section type on the backend, and at most one new component registered on the frontend. The backend decides when it appears; the frontend just knows how that type looks.
Build a page by growing the list
Watch a real page assemble itself purely by appending to the list — no screen code is written at any step. Start from the empty page above, then a junior engineer adds one section: an image_list bound to the org's projects.
{
"page": "projects",
"sections": [
{
"type": "image_list",
"title": "Projects",
"data": "list of projects"
}
]
}
Append one more object — an upload section — and the page gains an upload control. The frontend did not know or care that it was an upload; it recognized another section type and mounted its region.
{
"page": "projects",
"sections": [
{ "type": "header", "title": "Projects" },
{
"type": "image_list",
"title": "My projects",
"data": "list of projects"
},
{
"type": "upload",
"title": "Upload slide",
"accept": "wsi"
}
]
}
That JSON renders to a complete page — a header, a project list, and an upload button — with zero bespoke screen logic.
┌──────────────────────────────┐ │ projects + New │ │ ── Projects ── │ │ My projects │ │ [ img ] [ img ] [ img ] │ │ ↑ Upload slide (wsi) │ └──────────────────────────────┘
Actions are links too
A section carries more than what to show — it also carries what you may do, as an actions map of { url, method } links. Instead of hard-coding endpoints, the frontend follows the link the backend handed it. This is a lightweight form of HATEOAS (the REST idea that a response includes the links to its next steps) — call it HATEOAS-lite.
{
"id": "images_section",
"options": { "selectable": true },
"actions": {
"load": { "url": "/folders/42/sections/images", "method": "GET" },
"read_multiple": { "url": "/folders/42/images", "method": "GET" },
"create_one": { "url": "/folders/42/images", "method": "POST" },
"update_one": { "url": "/images/<int:id>", "method": "PATCH" },
"delete_one": { "url": "/images/<int:id>", "method": "DELETE" }
}
}
The rule is simple and uniform: load fetches the section, then the read / create / update / delete links drive its data. Where a link has a typed placeholder like <int:id>, the frontend fills it in from the current route; it never builds a URL string by hand.
Because every section speaks the same { url, method } contract, one shared HTTP client can attach auth, unwrap the response, and route errors in one place — so each section component stays tiny.
Why one render path fits a tiny team
Simplicity is the bar, and this design meets it with two well-known principles. The renderer is DRY (Don't Repeat Yourself): there is exactly one render path, so a fix to rendering fixes every page at once. Each section is SRP (Single Responsibility): it owns one region and one job. An engineer only ever touches their new section — a JSON spec on the backend plus, at most, one registered component on the frontend.
That is why the design carries a small team. A new surface — a stats panel, a models table, a new canvas tool — is the same section contract, not new plumbing.
- A junior ships a screen by copy-pasting a section JSON and reusing a component that is already registered.
- Review becomes a JSON diff — minutes, not a new-screen audit — so a senior can approve it and go back to the hard model and backend work.
- One renderer, one data-fetch pattern, one section shape: learn one, understand all.
The field's requests keep landing as configuration instead of code, which is exactly what let a three-person team ship a broad product.
Key takeaways
- The frontend is a generic renderer: it draws whatever the backend describes and never invents a layout or a URL of its own.
- Orca ships two React + TypeScript apps (explore app and secure S3 viewer); both talk only to the API Gateway.
- A page is an ordered list of section objects. Each section names a
type, atitle, itsdata, and itsactions. - The renderer looks up each section's
typein a component registry and stacks the matching components in order — so a new surface is a new type plus at most one registered component. - Build a page by growing the list: start empty, append an
image_list, append anupload— the screen grows with the JSON, with no screen code written. - Actions are
{ url, method }links the frontend follows (HATEOAS-lite); typed placeholders are filled from the route, and one shared HTTP client handles auth, unwrapping, and errors. - One render path (DRY) and single-responsibility sections (SRP) turn new work into a JSON diff, which is how a tiny team shipped a broad product.
Checklist
- [ ] I can explain what a "generic renderer" is and why it keeps the client dumb.
- [ ] I can name the stack layers and say what each tool does.
- [ ] I can describe a page as an ordered list of section objects and name a section's fields.
- [ ] I can explain how the component registry turns a section
typeinto a rendered component. - [ ] I can grow a page by appending sections and predict what renders.
- [ ] I can explain "actions are links" (HATEOAS-lite) and why the frontend never builds a URL by hand.
- [ ] I can state how DRY renderer + SRP sections let a junior ship a screen and a senior review a JSON diff.