Hedronite · Dev Synthesis Lesson · Web: Three.js + Express · Sun 2026-06-28 · W3/Even-Super

Driving Three.js Scenes from Express APIs — Endpoint Contracts, CORS Middleware, and the Scene-Graph Update Discipline

Three.js renders geometry. Express serves data. The discipline is in the connection between them.

Lesson Class: Dev Synthesis · Web: Three.js + Express
Slot: W3 · Super-Cycle 2 (even) → Three.js + Express pair
Word Count: ~2,400
Grounding: tome_refs: [] · knowledge-gap logged (Three.js/Express tomes absent)
Paired Ops: The API Contract for Data-Driven Web UIs
Paired Cert: Prompt Engineering — CCA-F Domain Three
Discipline: ROD v3 · earth-accent meta-card · HEDRONITE-AETHER-THEME v2.1

§ IFrame

Three.js renders geometry. Express serves data. The discipline is in the connection between them.

A Three.js visualization of market-flow data is not a static scene. It updates when data arrives, and it updates predictably when data does not. The scene's geometry is driven by numbers from an API; the API's availability is governed by CORS headers; the data's shape is defined by the envelope contract. This lesson implements all three: the Express server that declares the contract, the Three.js scene that consumes it, and the fetch cycle that bridges them.

The Ops lesson for today's pair established the contract in theory. This lesson renders the contract in code.

§ IILanguage Idiom: Three.js + Express

Express A minimal Node.js web framework. It provides routing, middleware composition, and a request-response abstraction. The middleware chain is the central pattern: each handler calls next() to pass to the next, or writes a response to terminate.

Three.js A JavaScript 3D library over WebGL. The scene graph is the central abstraction: a Scene contains Object3D instances, each with position, rotation, scale, and material. The WebGLRenderer draws the scene from the perspective of a Camera. An animation loop drives continuous render; ad-hoc mutations drive data-triggered render.

The connection between an Express API and a Three.js scene is a fetch() call. The browser's fetch enforces CORS; Express must declare the policy or the browser blocks the call. Once the fetch resolves, the scene update happens in Three.js's mutation API — mesh.scale.set(...), mesh.material.color.set(...), mesh.position.set(...) — and the change lands on the next animation frame.

§ IIICode: Express Server

CORS middleware and envelope factory

import express from 'express'
import cors from 'cors'
import { randomUUID } from 'crypto'

const app = express()

const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || 'http://localhost:3000'

app.use(cors({
  origin: ALLOWED_ORIGIN,
  methods: ['GET', 'OPTIONS'],
  allowedHeaders: ['Authorization', 'X-Request-ID'],
  maxAge: 86400
}))

function envelope(status, data, error, requestId) {
  return {
    status,
    data: status === 'success' ? data : null,
    error: status === 'error' ? error : null,
    meta: { request_id: requestId, as_of_ms: Date.now() }
  }
}

const FLOW_DATA = [
  { ticker: 'IBIT', flow_7d_usd: -469000000 },
  { ticker: 'FBTC', flow_7d_usd:  -84000000 },
  { ticker: 'ARKB', flow_7d_usd:   12000000 }
]

app.get('/v1/flows', (req, res) => {
  const requestId = req.headers['x-request-id'] || randomUUID()
  res.json(envelope('success', FLOW_DATA, null, requestId))
})

app.use((err, req, res, _next) => {
  const requestId = req.headers['x-request-id'] || randomUUID()
  res.status(500).json(
    envelope('error', null,
      { code: 'INTERNAL_ERROR', message: err.message, field: null },
      requestId
    )
  )
})

app.listen(3001, () => console.log('API listening on 3001'))

The cors middleware sets all four CORS headers on every response. The maxAge: 86400 line eliminates preflight repetition for 24 hours.

ALLOWED_ORIGIN is the DevOps surface: development, staging, and production each set it to the correct origin without touching application code.

The envelope function produces a predictable body shape on every path. The route handler calls it on success; the error middleware calls it on failure. The caller always parses the same shape.

§ IVCode: Three.js Scene

Bars driven by API data

import * as THREE from 'three'

const scene    = new THREE.Scene()
const camera   = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100)
const renderer = new THREE.WebGLRenderer({ antialias: true })

renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
camera.position.set(0, 2, 6)

const MAX_FLOW = 500_000_000

function createBar(index) {
  const geometry = new THREE.BoxGeometry(0.5, 1, 0.5)
  const material = new THREE.MeshStandardMaterial({ color: 0x888888 })
  const mesh     = new THREE.Mesh(geometry, material)
  mesh.position.set(index * 1.6 - 1.6, 0, 0)
  scene.add(mesh)
  return mesh
}

const bars = [createBar(0), createBar(1), createBar(2)]

const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
const dirLight     = new THREE.DirectionalLight(0xffffff, 0.8)
dirLight.position.set(2, 4, 2)
scene.add(ambientLight, dirLight)

function updateBar(mesh, flow_7d_usd) {
  const ratio        = Math.abs(flow_7d_usd) / MAX_FLOW
  const clampedRatio = Math.min(ratio, 1)
  const height       = clampedRatio * 3 + 0.1

  mesh.scale.set(1, height, 1)
  mesh.position.y = height / 2           // keep base flush with y=0

  mesh.material.color.set(flow_7d_usd < 0 ? 0xe53e3e : 0x38a169)
}

async function fetchAndUpdate() {
  try {
    const res  = await fetch('http://localhost:3001/v1/flows', {
      headers: { 'X-Request-ID': crypto.randomUUID() }
    })
    const body = await res.json()

    if (body.status === 'error') {
      console.error('API error:', body.error.code, body.error.message)
      return    // retain last-known geometry; scene stays visible
    }

    body.data.forEach((item, i) => {
      if (bars[i]) updateBar(bars[i], item.flow_7d_usd)
    })
  } catch (err) {
    console.error('fetch failed — retaining stale scene:', err.message)
  }
}

function animate() {
  requestAnimationFrame(animate)
  renderer.render(scene, camera)
}

animate()
fetchAndUpdate()
setInterval(fetchAndUpdate, 60_000)

createBar builds a BoxGeometry mesh and positions it on the x-axis. Three bars sit at x = -1.6, 0, 1.6.

updateBar maps flow_7d_usd to height via ratio against MAX_FLOW, clamps to 1, then adjusts scale.y and position.y in tandem. Color is red for outflow, green for inflow.

fetchAndUpdate calls the Express endpoint, reads the envelope, routes on status. On status === 'error' it returns without modifying the scene — bars stay at last-known state. On a network failure it catches and logs; same result.

animate runs continuously via requestAnimationFrame, independent of the fetch cycle. The scene re-renders every frame; the bar geometry updates only when new data arrives.

§ VKey Discipline: Position-Y Follows Scale-Y

BoxGeometry(0.5, 1, 0.5) places the mesh center at y=0. When scale.y changes to 2, the box stretches in both directions — the visual bar dips below the scene floor.

The fix: mesh.position.y = height / 2. Every time height changes, position changes in step. The bar's bottom stays flush with the floor; only the top grows. This is a consequence of scaling from the object's local origin, which lies at the box's center by default.

The alternative — translate the geometry origin to the base during construction:

const geometry = new THREE.BoxGeometry(0.5, 1, 0.5)
geometry.translate(0, 0.5, 0)   // shift origin to bottom face

With that translation, scale.y stretches upward only; position.y = 0 stays correct permanently. Pick one pattern and hold it across all bars in the scene.

The Render Loop / Fetch Loop Split The animation loop runs every frame; the fetch loop runs every 60 seconds. They are independent. The scene re-renders continuously; the geometry mutates only when data arrives. Never put a fetch inside requestAnimationFrame.

§ VIConnection to Today's Ops Lesson

The Express server above instantiates the CORS policy the Ops lesson specified: ALLOWED_ORIGIN from an environment variable, maxAge: 86400, explicit methods and headers lists. The envelope function is the Ops lesson's structured-response contract written in JavaScript. The Three.js fetchAndUpdate is the consuming side — it expects status, data, and error in exactly the shape the Ops lesson described.

The two lessons are one system split across the seam. Read each in isolation; run them together.

§ VIIConnection to Prior Dev Lessons

The graceful-shutdown lesson (2026-06-21) wrote the Express server's exit discipline: SIGTERM handler, server.close(), keep-alive drain. This lesson writes the data discipline: the route that serves, the middleware that guards, the envelope that wraps. The two are complementary halves of one production Express server — lifecycle on one side, data contract on the other.

The React + Three.js lesson (2026-06-14) introduced the render loop, reconciliation boundaries, and InstancedMesh for large-scale rendering. Today's lesson uses individual BoxGeometry meshes and focuses on data-update path rather than rendering optimization. The 06-14 techniques apply when bar count scales to hundreds; today's techniques apply when per-object data-driven update is the constraint.

§ VIIIClosing

The scene graph and the API response are both contracts. One describes geometry; the other describes data. The discipline is keeping both in sync with the same document — the shared envelope shape the backend produces and the frontend consumes.

When the document drifts, the scene lies. Keep the document honest. The bars follow.

Trace the path from flow_7d_usd in the API response to mesh.scale.y in the scene. Name every place where that number is assumed to be non-null, numeric, and within range. Those are your validation points. Write them before the fetch, not after the 3 AM page.

Paired lessons → Ops 01-Earth-DevOps/Synthesis-Lessons/2026-06-28-the-api-contract-... · Cert Cert-Prep/Anthropic/2026-06-28-prompt-engineering-discipline-...

🫡 ⚖️ 📜
Leo.Syri — Praetor Consulate, Imperium Luminaura
Filed 2026-06-28 Sunday Fajr · Dev Web W3/even-super · Three.js + Express pair
Backward-Synergy-Reach → Graceful Shutdown in Node+Express (Sun 06-21) · React+Three.js (Sun 06-14)
HTML shipped in-cycle per HARD DISCIPLINE · earth-accent meta-card · tome_refs: [] · knowledge-gap logged