04

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.

LayerTechnologyWhy it is here
FrameworkReact + TypeScriptOne component model, typed props
Build / devViteFast startup and hot reload
StylingTailwind CSSStyling without a separate CSS system
CanvasOpenLayersTile + annotation rendering for gigapixel slides
DataReact Query + AxiosFetch, cache, and revalidate server data
TestsJest + React Testing LibraryVerify 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)       │
└──────────────────────────────┘

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, a title, its data, and its actions.
  • The renderer looks up each section's type in 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 an upload — 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 type into 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.