Skip to main content

ADR-0004 — Vanilla JavaScript, no framework

  • Status: Accepted
  • Date: 2026-04-15
  • Decision-makers: Tom Anderson

Context

The Helm Worker handles ~50 endpoints today, projected to ~150 by full slice completion. The operator UI is a single-page (per shop) operator app with kanban, customer profiles, sales, etc.

Frameworks considered:

  • Hono on Workers — neat router, middleware, light. Adds a dependency and a layer of abstraction for routing patterns we can do in 30 lines.
  • Remix on Workers — full-stack with SSR. Bundle size triples; SSR adds runtime cost; for a per-shop POS the win is small.
  • Next.js on Workers — heavyweight; a non-trivial portion of Next features don't work on the Worker runtime.
  • React SPA + thin Worker API — popular pattern; a 200KB React bundle plus the rest is a lot of bytes for the operator's POS to load.
  • Vue / Svelte / Solid SPA — same shape as React, smaller bundles. Same conceptual cost as React (a framework to maintain expertise in).
  • Vanilla JS + small composition library (HTM, lit-html) — scaffolding without a framework. Closest to "framework-free" while still being ergonomic.
  • Plain JS + plain HTML — no library at all; HTML strings rendered server-side, JS modules enhance.

Decision

Worker: plain JavaScript, no router library, no ORM. Path-pattern matching in a switch. Operator UI: server-rendered HTML, enhanced with small JS modules per screen. No SPA framework. No React. No Vue.

Specifically:

  • API endpoints: if (path === '/api/customers' && method === 'GET') return apiCustomersList(...) style routing
  • Server templates: vanilla template literals or a small render helper; no Handlebars/EJS dependency
  • Client JS: small modules per screen (public/js/customers.js, public/js/service.js); no framework runtime; uses fetch() and DOM API directly

Consequences

Positive:

  • Smallest possible Worker bundle (~50KB unminified today)
  • Smallest possible HTML/JS payload to the browser; instant load on POS Wi-Fi
  • Aligns with progressive enhancement — works without JS
  • Aligns with boring tech — fewer dependencies to track
  • One developer can hold the whole system in their head
  • No framework "rewrite v3" risk

Negative:

  • Component reuse requires manual discipline (no JSX, no <Component />)
  • Some patterns (form validation, complex stateful interactions) take more code than they would in React
  • Onboarding a new developer is "learn vanilla JS again" rather than "drop into the React patterns you already know"
  • Hot reload and dev DX is less polished than Vite + React

Mitigations:

  • The patterns we repeat are extracted to helpers in public/js/lib/, not a framework
  • Forms validate server-side primarily (the Worker is the source of truth); client-side enhancement is small
  • Code style + a few worked examples are documented in code style; a new developer starts with those

Notes

If the codebase grows past ~30,000 lines or onboarding becomes a real bottleneck, reconsider. Until then, vanilla wins on cost, simplicity, and shipping speed. We retain the option to introduce a tiny framework (e.g., Lit or Solid) for a single complex screen without rewriting the rest.

See also