๐ค AI-Readable JSON for this phase
All 10 sub-phases of Foundations as JSON โ perfect for AI agents.
{
"phase": {
"id": 1,
"name": "Foundations",
"icon": "๐ฆ",
"tagline": "Stack backbone โ เธเนเธญเธเธกเธตเธเนเธญเธเนเธฃเธดเนเนเธกเธเธธเธเนเธเธฃเนเธเธ",
"color": "#ec4899",
"slug": "foundations"
},
"sub_phases": [
{
"id": "1.1",
"title": "HTML5 Boilerplate",
"icon": "๐",
"description": "Single-file HTML (CSS+JS inline). No build step, no npm, CDN-only deps. Fastest path to a working app.",
"when_to_use": "เธ์ตเธเธ 1000 line; prototype; demo; internal tool; offline-able; static hosting",
"when_not_to_use": "App > 50k lines; need TypeScript; need SSR/SEO; > 5 devs; npm ecosystem required",
"pros": [
"Loads in 80-200ms with no dependency graph; nginx serves index.html in one TCP roundtrip",
"Works file:// โ no install needed for offline demos / training / fieldwork",
"CDN-only deps = zero supply-chain npm risk (no postinstall scripts to audit)",
"Debug = what user sees; Chrome DevTools shows identical HTML/CSS/JS, no source-map confusion",
"Works on 10-year-old browsers that dropped React 16+ (e.g. Smart TV firmware)"
],
"cons": [
"DOM lookups O(nยฒ) past 5k elements โ use canvas or virtualize for tables/lists",
"No bundled minification = 2-5x larger payload vs Webpack/Vite; ~150KB vs ~30KB",
"Global scope pollution; each <script> shares window unless using IIFE or modules",
"Manual asset versioning required (add ?v=Date.now() to bust iOS Safari's 7-day cache)",
"No first-class TS = IDE type hints weak; refactor across 10 files = manual find/replace"
],
"gotchas": [
"iOS Safari caches static HTML for 7 days even with Cache-Control: no-cache; append ?v=Date.now() to URL",
"CSP blocks inline <script> unless you add 'unsafe-inline' or use external src=.js files",
"type=module defers script loading โ DOMContentLoaded fires before all modules imported",
"CORS preflight (OPTIONS) fires on every XHR with custom headers; 5x latency hit per request",
"Touch handlers need {passive:false} to call preventDefault() โ default is passive:true in Chrome",
"Async <script> executes after DOMContentLoaded without explicit await (use defer instead)",
"Old Android (Stock browser) can choke on > 50 <script> tags; bundle to one file",
"fetch() doesn't send cookies cross-origin without credentials: 'include' (default is omit)"
],
"anti_patterns": [
"Inline event handlers (onclick=) in HTML โ use addEventListener with delegation for 100+ listeners",
"Mixing inline <style> with external CSS โ specificity wars and no cache benefit",
"Using document.write() inside async code โ wipes the entire DOM tree",
"Loading jQuery to do querySelectorAll โ 30KB for what querySelector does in 1 line"
],
"prereqs": [],
"next_steps": [
"1.2",
"1.3",
"1.7"
],
"examples": [
"karaoke-studio",
"noodle-shop",
"stronghold-3d",
"fitcheck",
"demo-coin-flip"
],
"snippet": "<!DOCTYPE html><html lang=en><head><meta charset=UTF-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>App</title><style>body{margin:0;background:#0a0a14;color:#fff;font:14px system-ui}</style></head><body><div id=app></div><script>document.getElementById('app').textContent='Hello v1';</script></body></html>",
"snippet_full": "// Full SPA shell with module loading + global state + viewport meta\nconst App = (() => {\n const state = { user: null, theme: 'dark' };\n const listeners = new Set();\n const set = (k, v) => { state[k] = v; listeners.forEach(fn => fn(state)); };\n return {\n state,\n on: (fn) => listeners.add(fn),\n init: () => {\n document.documentElement.dataset.theme = state.theme;\n document.getElementById('app').innerHTML = '<h1>Hello</h1>';\n }\n };\n})();\nApp.on(s => console.log('state:', s));\nApp.init();",
"tags": [
"html",
"vanilla",
"boilerplate",
"spa",
"shell"
],
"difficulty": "Beginner",
"time_to_learn": "5 min",
"docs_url": "https://developer.mozilla.org/en-US/docs/Learn/HTML/Introduction_to_HTML",
"compare_with": [
"Vite + React (5x more deps, faster DX)",
"Next.js (SSR + routing overkill for 5-page apps)"
]
},
{
"id": "1.2",
"title": "CSS Variables & Design Tokens",
"icon": "๐จ",
"description": ":root variables (--accent, --bg, --text) for theme switching via data-theme. 1-line theme change.",
"when_to_use": "multi-theme; white-label; design system; > 5 reusable components",
"when_not_to_use": "single static page < 5 styles; print stylesheet (use static colors)",
"pros": [
"Brand color change = 1 line of CSS (no JS, no rebuild, no React Context re-render)",
"Cascade-aware: child components can override --accent without specificity war",
"live-edit in Chrome DevTools updates entire UI immediately; no build step",
"Theme persistence via single localStorage key + one documentElement.dataset write"
],
"cons": [
"IE 11 doesn't support (use PostCSS plugin if IE is required; rare in 2026)",
"Specificity tied to element where var() is used โ can't override in deep selectors without :where()",
"Huge variable lists hurt readability โ use namespaced vars like --color-text-primary, not --text1",
"No compile-time checks โ typo in var(--accnet) silently fails to var() fallback"
],
"gotchas": [
"var() needs a fallback for missing tokens: var(--accent, #ec4899) โ prevents flash of unstyled",
"HSL > hex for theming โ hue/lightness adjustable per-theme: hsl(240 10% 5%) โ hsl(0 0% 98%)",
"CSS @property registers vars with type so calc() and animation work (CSS Properties API)",
"Cascade inheritance only for inherited properties โ background-color needs explicit inheritance",
"Theme flash on page load โ inline a <script> in <head> setting data-theme BEFORE render to avoid FOUC",
"Tokens nested in @layer have lower specificity; check ordering if your tokens don't apply",
"Color-mix() in CSS Color Module 4 (2023) gives you derived tokens: --accent-fade: color-mix(in srgb, var(--accent) 30%, transparent)"
],
"anti_patterns": [
"var() in @keyframes without @property โ animations may not interpolate",
"Using JS to mutate :root vars on every render โ garbage collection churn, use class toggles",
"Token names tied to values: --blue-500 โ ties you to one brand; use --color-primary instead",
"Mixing pixel and rem units in spacing tokens โ visual rhythm broken across zoom levels"
],
"prereqs": [
"1.1"
],
"next_steps": [
"2.1",
"2.2",
"2.8"
],
"examples": [
"stronghold-3d",
"codehub",
"karaoke-studio",
"flowboard"
],
"snippet": ":root { --accent:#ec4899; --bg:#0a0a14; --text:#f5f5f7; --radius:12px; } .btn { background:var(--accent); border-radius:var(--radius); }",
"snippet_full": "/* Multi-theme system with semantic tokens + dark mode */\n:root {\n --color-bg: hsl(240 10% 5%);\n --color-bg-elevated: hsl(240 10% 8%);\n --color-text: hsl(0 0% 96%);\n --color-text-muted: hsl(0 0% 64%);\n --color-accent: hsl(330 81% 60%);\n --color-success: hsl(160 84% 39%);\n --color-error: hsl(0 84% 60%);\n --space-1: 4px; --space-2: 8px; --space-4: 16px;\n --radius-md: 12px; --radius-pill: 9999px;\n}\n[data-theme='light'] {\n --color-bg: hsl(0 0% 98%);\n --color-text: hsl(240 10% 10%);\n}\n/* Inline script in <head> to prevent FOUC */\n<script>document.documentElement.dataset.theme = localStorage.getItem('theme') || 'dark';</script>",
"tags": [
"css",
"tokens",
"theme",
"variables",
"design-system"
],
"difficulty": "Beginner",
"time_to_learn": "30 min",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties",
"compare_with": [
"Sass variables (compile-time, no runtime theme switch)",
"Tailwind theme (utility class explosion, harder to debug)"
]
},
{
"id": "1.3",
"title": "Vanilla JS Module Pattern (IIFE)",
"icon": "๐งฉ",
"description": "IIFE returning public API. Encapsulates private vars without bundler. ESM alternative for legacy browsers.",
"when_to_use": "single-file apps needing organization; legacy jQuery-era code; pre-ESM",
"when_not_to_use": "modern ESM project; multiple files; tree-shaking needed",
"pros": [
"Encapsulation without bundler โ private vars hidden from window scope",
"Familiar pattern from jQuery era โ readable for devs transitioning from jQuery plugins",
"Single-file deploy โ 1 HTTP request for all logic vs 50 for ESM modules"
],
"cons": [
"Static structure โ can't be reloaded or hot-swapped without state loss",
"No tree-shaking โ entire IIFE loads even if you use 1 method",
"Method names exposed on public API can't be minified to 1 letter (defeats size optimization)",
"Hard to test in isolation โ everything is one closure"
],
"gotchas": [
"Arrow function inherits lexical this โ inside IIFE, this === window in non-strict, undefined in strict",
"Private functions hidden from devtools โ hard to inspect state mid-debug",
"Use 'use strict' at top of IIFE to catch ReferenceError on assignment to undeclared vars",
"Closures hold references โ memory leaks if you store DOM nodes in long-lived IIFE",
"Can't use import/export โ use Revealing Module Pattern (assign all public methods to const obj = {...})",
"Destructuring the public API binds references: const { init } = MyApp; โ can't replace later"
],
"anti_patterns": [
"IIFE returning object literal when you need polymorphism โ use class",
"Storing DOM references in IIFE closure โ leaks when DOM removed; use WeakMap",
"Hiding async functions behind sync API โ caller doesn't know to await",
"Mixing CommonJS require() with IIFE โ module systems fight each other"
],
"prereqs": [
"1.1"
],
"next_steps": [
"3.10",
"4.1"
],
"examples": [
"stronghold-3d",
"flowboard",
"noodle-shop",
"sims-lite"
],
"snippet": "const MyApp = (() => { const secret = 'hidden'; return { init(opts) { /* */ }, version:'1.0' }; })(); MyApp.init();",
"snippet_full": "// Revealing Module Pattern with private state + public API\nconst GameState = (() => {\n 'use strict';\n let _state = { score: 0, level: 1 };\n const _listeners = new Set();\n function _emit() { _listeners.forEach(fn => fn(_state)); }\n return {\n get: () => ({ ..._state }),\n addScore: (n) => { _state.score += n; _emit(); },\n on: (fn) => _listeners.add(fn),\n reset: () => { _state = { score: 0, level: 1 }; _emit(); }\n };\n})();\nGameState.on(s => console.log('score:', s.score));\nGameState.addScore(10); // logs: score: 10",
"tags": [
"js",
"module",
"iife",
"closure",
"pattern"
],
"difficulty": "Intermediate",
"time_to_learn": "15 min",
"docs_url": "https://developer.mozilla.org/en-US/docs/Glossary/IIFE",
"compare_with": [
"ESM (modern, tree-shakable)",
"CommonJS (Node.js only)",
"AMD (deprecated)"
]
},
{
"id": "1.4",
"title": "localStorage Save/Load + Migration",
"icon": "๐พ",
"description": "JSON.stringify + version field. Fallback chain: v3 โ v2 โ v1. Foundation for offline-first apps.",
"when_to_use": "game saves; form drafts; user preferences; offline-first apps",
"when_not_to_use": "multi-device sync required; sensitive data; > 5MB; cross-tab real-time",
"pros": [
"Instant persistence โ no async, no network; perfect for game saves and form drafts",
"Zero latency reads โ synchronous getItem() < 1ms vs IndexedDB async",
"Free and abundant โ 5-10MB per origin (Chrome), no auth, no server cost",
"Survives reloads, browser restarts, OS updates โ even when offline"
],
"cons": [
"5-10MB limit per origin (Chrome) โ 5MB warning at 5MB, 0KB after QuotaExceededError",
"NOT encrypted โ user can open DevTools and read all saves; never store PII/passwords",
"Not synced across devices โ user on iPhone won't see iPad save",
"iOS Private Browsing = cleared when tab closes (no warning, silent data loss)",
"Synchronous โ blocks main thread; writing 1MB JSON freezes UI 50-200ms"
],
"gotchas": [
"ALWAYS try/catch JSON.parse โ corrupted data throws SyntaxError, uncaught = blank page",
"Map, Set, Date, undefined, NaN, Infinity don't survive JSON.stringify โ convert manually",
"iOS Safari localStorage cleared in Private mode silently; detect via try/catch on setItem",
"QuotaExceededError on save โ clear old versions, then save again with retry",
"Browser sync between tabs via 'storage' event โ useful but different origin policies apply",
"localStorage vs sessionStorage โ lastCleared on tab close, but shared across same-tab windows",
"Cache-Control headers don't affect localStorage โ only affects HTTP cache"
],
"anti_patterns": [
"Saving on every change without debounce โ 100ms of work per keystroke kills performance",
"Storing Date objects directly โ they serialize to strings, not Date instances on load",
"Catching SyntaxError and silently returning null โ user loses save without warning",
"Migration code that REQUIRES a specific old format โ if you skip v1, v3 loaders break"
],
"prereqs": [
"1.1"
],
"next_steps": [
"9.4",
"4.10"
],
"examples": [
"flowboard",
"stronghold-3d",
"noodle-shop",
"sims-lite",
"habit-streak"
],
"snippet": "const KEY = 'app_v3'; const save = s => localStorage.setItem(KEY, JSON.stringify({ver:3,data:s})); const load = () => JSON.parse(localStorage.getItem(KEY) || localStorage.getItem('app_v2') || 'null');",
"snippet_full": "// Production-grade localStorage with versioning, debounce, migration\nconst Store = (() => {\n const KEY = 'app_v3';\n const MIGRATIONS = {\n 1: (d) => ({ ...d, achievements: d.achievements || [] }),\n 2: (d) => ({ ...d, weather: d.weather || 'cloudy' })\n };\n let _timer = null;\n return {\n save(state) {\n clearTimeout(_timer);\n _timer = setTimeout(() => {\n try { localStorage.setItem(KEY, JSON.stringify({ ver: 3, data: state, ts: Date.now() })); }\n catch (e) { if (e.name === 'QuotaExceededError') console.warn('save failed: storage full'); }\n }, 300);\n },\n load() {\n for (let i = 3; i >= 1; i--) {\n const raw = localStorage.getItem(`app_v${i}`);\n if (!raw) continue;\n try {\n let obj = JSON.parse(raw);\n for (let v = obj.ver || 1; v <= 3; v++) if (MIGRATIONS[v]) obj.data = MIGRATIONS[v](obj.data);\n return obj.data;\n } catch (e) { console.warn(`v${i} corrupted`); }\n }\n return null;\n }\n };\n})();",
"tags": [
"storage",
"save",
"migration",
"offline",
"versioning"
],
"difficulty": "Intermediate",
"time_to_learn": "1 hour",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage",
"compare_with": [
"IndexedDB (async, MB-GB, structured queries)",
"sessionStorage (per-tab, ephemeral)",
"Cookies (HTTP-only, server-readable)"
]
},
{
"id": "1.5",
"title": "Web Audio API (Synthesized SFX)",
"icon": "๐",
"description": "OscillatorNode + GainNode for procedural SFX without audio files. Tiny code, no CDN, real-time pitch.",
"when_to_use": "เนเธเธธเนเธกเธเนเธญเธเธ์ถ SFX < 100 distinct; load time < 1s; prototype; license-clean assets",
"when_not_to_use": "music track needed; pro instruments; realistic sampled sounds",
"pros": [
"Zero file load โ sounds generated on-demand, instant playback, no CDN needed",
"Real-time pitch/rhythm control โ sweep, envelope, LFO without resampling",
"License-free โ no copyrighted sample packs, no .mp3 / .wav files in your bundle",
"Tiny code โ 50 lines replaces 10MB of .mp3 files; perfect for game jam prototypes"
],
"cons": [
"Synthetic = not realistic โ can't produce pro-quality instruments, voice, drums",
"iOS Safari AudioContext starts SUSPENDED โ must resume() inside a click/tap handler first",
"Polyphony capped at ~32 simultaneous voices on mobile โ explosion SFX stack up and clip",
"iOS audio plays through earpiece (not speaker) until user has a session of 7s+ โ silent phones!"
],
"gotchas": [
"AudioContext starts in 'suspended' state on iOS โ resume() MUST be in user gesture handler (click)",
"Always add envelope to prevent click/pop on note start: gain.linearRampToValueAtTime() from 0",
"A4 = 440Hz, A5 = 880Hz โ musical pitch uses A=440 reference; semitone = 2^(1/12) ratio",
"createOscillator().connect(gain).connect(ctx.destination) โ wrong connection = silent output",
"Pre-create oscillators when sound is needed, .start() on trigger โ don't create per-frame",
"Webkit prefix still needed for older Safari: webkitAudioContext fallback",
"Frequency below 20Hz is sub-bass; humans don't perceive direction โ use stereo panning via StereoPannerNode"
],
"anti_patterns": [
"Creating AudioContext per sound โ destroys Chrome's audio thread; one context per page",
"Using .start(0) without envelope โ causes loud click/pop on every note",
"Setting osc.frequency.value = 0 โ undefined behavior; use exponentialRampToValueAtTime",
"Looping sample-rate mismatch โ buffer source for sampled, oscillator for synth"
],
"prereqs": [
"1.1"
],
"next_steps": [
"6.1",
"6.6"
],
"examples": [
"stronghold-3d",
"street-fighter-thai",
"karaoke-studio",
"noodle-shop"
],
"snippet": "let actx; const beep = (f=440,d=0.1,v=0.3) => { if(!actx) actx = new AudioContext(); const o=actx.createOscillator(),g=actx.createGain(); o.connect(g).connect(actx.destination); o.frequency.value=f; g.gain.setValueAtTime(0,actx.currentTime); g.gain.linearRampToValueAtTime(v,actx.currentTime+0.005); o.start(); o.stop(actx.currentTime+d); };",
"snippet_full": "// Full game SFX system with envelope, sweep, polyphony, iOS unlock\nconst SFX = (() => {\n let ctx = null;\n const unlock = () => {\n if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)();\n if (ctx.state === 'suspended') ctx.resume();\n };\n const play = (freq = 440, dur = 0.1, type = 'square', vol = 0.2) => {\n if (!ctx) return;\n const o = ctx.createOscillator();\n const g = ctx.createGain();\n o.type = type; o.frequency.value = freq;\n o.connect(g).connect(ctx.destination);\n const t = ctx.currentTime;\n g.gain.setValueAtTime(0, t);\n g.gain.linearRampToValueAtTime(vol, t + 0.005);\n g.gain.exponentialRampToValueAtTime(0.001, t + dur);\n o.start(t); o.stop(t + dur);\n };\n // Preset SFX\n const jump = () => play(440, 0.15, 'square', 0.15);\n const hit = () => { play(150, 0.05, 'sawtooth', 0.3); play(80, 0.1, 'square', 0.2); };\n const coin = () => { play(880, 0.08); setTimeout(() => play(1320, 0.12), 80); };\n document.addEventListener('click', unlock, { once: true });\n return { play, jump, hit, coin, unlock };\n})();",
"tags": [
"audio",
"sfx",
"synth",
"game",
"oscillator"
],
"difficulty": "Intermediate",
"time_to_learn": "2 hours",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API",
"compare_with": [
"Tone.js (heavier, music library, 100KB)",
"Howler.js (file-based, 12KB)",
"HTMLAudioElement (file-based, limited)"
]
},
{
"id": "1.6",
"title": "Canvas 2D Rendering",
"icon": "๐งต",
"description": "Draw primitives + read pixels. ~60fps for ~10000 entities. Foundation for games, charts, image editors.",
"when_to_use": "custom visualization; image editor; simple game; chart library",
"when_not_to_use": "3D; > 10000 entities; rich text rendering; accessibility required",
"pros": [
"Native, no dependencies โ every browser ships Canvas 2D since IE 9",
"Pixel-level control โ read pixels (getImageData) for image processing, color picking",
"Image export โ toBlob() converts to PNG/JPEG; canvas.toDataURL() for inline img",
"60fps for 10k entities on desktop; 1k entities on mobile; predictable perf"
],
"cons": [
"clear+redraw every frame โ no scene graph; you manage state yourself",
"No native hit-testing โ implement spatial index (R-tree, grid) for click detection",
"Text rendering weak โ subpixel, kerning, RTL all janky vs DOM; use DOM overlays",
"Memory spike on large canvases โ 4K canvas = 64MB GPU RAM"
],
"gotchas": [
"devicePixelRatio โ canvas.width = displayWidth * dpr, then ctx.scale(dpr, dpr) for crispness on HiDPI",
"ctx.save()/restore() stack โ always pair them; leaking pushes is a common bug",
"globalCompositeOperation='lighter' for glow/particles โ add colors without darkening background",
"ctx.measureText() before fillText โ prevents layout thrash, lets you right-align text",
"Image smoothing off for pixel art: ctx.imageSmoothingEnabled = false",
"Touch events fired with same coordinates as mouse โ use pointer events for unified handling",
"OffscreenCanvas (2018+) lets you render in Web Worker โ main thread stays at 60fps for UI"
],
"anti_patterns": [
"Allocating new objects per frame (new Path2D(), new Image) โ garbage collection stutter",
"Calling fillText without ctx.font set โ default font is 10px sans-serif, often tiny",
"Using canvas as the only UI โ a11y nightmare, can't select text or use screen readers",
"Drawing at non-integer coordinates โ causes blurry text/edges; use Math.floor()"
],
"prereqs": [
"1.1"
],
"next_steps": [
"3.2",
"3.3",
"6.4",
"4.1"
],
"examples": [
"noodle-shop",
"karaoke-studio",
"palette",
"sj88cute-frame-extractor",
"fitcheck"
],
"snippet": "const ctx = canvas.getContext('2d'); const loop = () => { ctx.clearRect(0,0,w,h); particles.forEach(p => { ctx.fillStyle = `rgba(255,${p.life},0,${p.alpha})`; ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,Math.PI*2); ctx.fill(); }); requestAnimationFrame(loop); }; loop();",
"snippet_full": "// Production 2D game loop with HiDPI + fixed timestep + particle system\nconst Game = (() => {\n const c = document.getElementById('game');\n const ctx = c.getContext('2d', { alpha: false });\n const dpr = window.devicePixelRatio || 1;\n function resize() {\n c.width = innerWidth * dpr; c.height = innerHeight * dpr;\n c.style.width = innerWidth + 'px'; c.style.height = innerHeight + 'px';\n ctx.scale(dpr, dpr);\n }\n resize(); addEventListener('resize', resize);\n const particles = [];\n let last = performance.now(); const DT = 1/60;\n function update(dt) {\n for (let i = particles.length - 1; i >= 0; i--) {\n const p = particles[i];\n p.x += p.vx * dt; p.y += p.vy * dt; p.life -= dt * 50;\n if (p.life <= 0) particles.splice(i, 1);\n }\n }\n function render() {\n ctx.fillStyle = '#0a0a14'; ctx.fillRect(0, 0, innerWidth, innerHeight);\n particles.forEach(p => {\n ctx.fillStyle = `rgba(255, ${p.life * 2}, 0, ${p.life/100})`;\n ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill();\n });\n }\n function frame(now) {\n const dt = Math.min((now - last) / 1000, 0.25); last = now;\n update(dt); render();\n requestAnimationFrame(frame);\n }\n return { particles, start: () => requestAnimationFrame(frame) };\n})();\nGame.start();",
"tags": [
"canvas",
"2d",
"rendering",
"game",
"graphics"
],
"difficulty": "Intermediate",
"time_to_learn": "3 hours",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D",
"compare_with": [
"WebGL/WebGPU (3D, 10x more code)",
"SVG (DOM-based, scales, slow > 1000 elements)",
"PixiJS (WebGL wrapper, faster but +400KB)"
]
},
{
"id": "1.7",
"title": "Fetch API + JSON + Error Handling",
"icon": "๐ก",
"description": "Promise-based HTTP. Headers, body, params, 4xx/5xx, CORS, retry. Foundation for all REST APIs.",
"when_to_use": "every FE app talking to REST API; config; analytics events; file uploads",
"when_not_to_use": "WebSocket realtime; static content; one-time bundle load",
"pros": [
"Native โ no jQuery $.ajax needed; works in all modern browsers and Node 18+",
"Streaming via ReadableStream โ stream OpenAI SSE, large file downloads without memory spike",
"AbortController for cancellation โ user navigates away โ cancel pending requests",
"Auto JSON parsing with one .json() call; auto text with .text()"
],
"cons": [
"Does NOT throw on 4xx/5xx โ you must check res.ok and throw manually; common bug",
"Body can only be read once โ calling res.json() twice = error; cache the parsed result",
"No built-in retry โ implement exponential backoff yourself; many libs do this badly",
"No request timeout by default โ if server hangs, fetch() hangs forever; use AbortController"
],
"gotchas": [
"AbortController for cancellation โ setTimeout(ctrl.abort, 10000) for 10s timeout",
"CORS preflight (OPTIONS) on custom headers โ add 'Content-Type: application/json' triggers it",
"Cache default is 'default' not 'no-store' โ GET requests may return stale data; use cache: 'no-store'",
"credentials: 'include' for cross-origin cookies โ default is 'same-origin' (no cookies sent)",
"POST body is FormData/Blob/JSON.stringify/URLSearchParams โ each has different Content-Type",
"fetch() rejects on network error, NOT on HTTP 4xx/5xx โ always check res.ok first",
"Response.json() throws on non-JSON โ wrap in try/catch to fall back to .text()"
],
"anti_patterns": [
"fetch('/api/x').then(r => r.json()) without checking r.ok โ silently swallows 500 errors",
"Setting Content-Type manually for FormData โ browser sets it with boundary; manual = broken",
"Catching all errors and returning null โ user sees nothing; log + show error UI",
"Retry without backoff โ 1000 concurrent retries when server recovers = thundering herd"
],
"prereqs": [
"1.1"
],
"next_steps": [
"9.1",
"9.2",
"9.6",
"7.6"
],
"examples": [
"codehub",
"flowboard",
"memo-ai",
"mavis-assistant",
"ai-data-analyzer"
],
"snippet": "const fetchJSON = async (url, opts={}) => { const ctrl = new AbortController(); const tid = setTimeout(() => ctrl.abort(), opts.timeout || 10000); try { const r = await fetch(url, {...opts, signal:ctrl.signal}); if (!r.ok) throw new Error(`HTTP ${r.status}`); return await r.json(); } finally { clearTimeout(tid); } };",
"snippet_full": "// Production-grade fetch wrapper with retry, timeout, auth, error mapping\nconst api = (() => {\n const BASE = '';\n const TOKEN_KEY = 'auth_token';\n async function call(path, opts = {}) {\n const url = `${BASE}${path}`;\n const ctrl = new AbortController();\n const tid = setTimeout(() => ctrl.abort(), opts.timeout || 15000);\n const headers = { 'Content-Type': 'application/json', ...opts.headers };\n const token = localStorage.getItem(TOKEN_KEY);\n if (token) headers.Authorization = `Bearer ${token}`;\n let attempt = 0; let lastErr;\n while (attempt < (opts.retries || 3)) {\n try {\n const r = await fetch(url, { ...opts, headers, signal: ctrl.signal });\n if (r.status === 429 || r.status >= 500) throw new Error(`HTTP ${r.status}`);\n if (!r.ok) throw Object.assign(new Error(r.statusText), { status: r.status, body: await r.text() });\n return r.status === 204 ? null : await r.json();\n } catch (e) {\n lastErr = e; attempt++;\n if (attempt < (opts.retries || 3)) await new Promise(r => setTimeout(r, 200 * 2 ** attempt));\n } finally { clearTimeout(tid); }\n }\n throw lastErr;\n }\n return { get: (p) => call(p), post: (p, b) => call(p, { method: 'POST', body: JSON.stringify(b) }) };\n})();\nconst projects = await api.get('/api/projects?limit=20');",
"tags": [
"fetch",
"api",
"json",
"http",
"promise"
],
"difficulty": "Beginner",
"time_to_learn": "30 min",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API",
"compare_with": [
"Axios (40KB, interceptors, older API)",
"ky (3KB, modern, retry built-in)",
"XHR (legacy, sync mode, no streaming)"
]
},
{
"id": "1.8",
"title": "File API (Upload/Download/Drag)",
"icon": "๐",
"description": "Read file from input/drop, create Blob, download via invisible <a>. Foundation for editors, importers.",
"when_to_use": "image/video editor; file converter; CSV import; drag-drop upload; offline tools",
"when_not_to_use": "upload to server (use FormData + fetch); real-time sync (WebRTC)",
"pros": [
"No server needed โ process files 100% client-side; privacy guaranteed",
"Instant preview โ file -> blob URL -> <img> in 1ms, no upload roundtrip",
"Drag-drop UX โ native, no library; users expect it from desktop OS",
"Read as text/buffer/stream โ CSV.parse(text), binary parsing, streaming large files"
],
"cons": [
"RAM spike โ file.arrayBuffer() loads entire file into memory; 100MB image = 100MB+ heap",
"No chunk upload built-in โ must slice manually: file.slice(start, end) for resume uploads",
"Drag-drop on mobile = awkward โ no hover state, no file path, only camera roll",
"URL.createObjectURL leaks โ must revokeObjectURL when done or memory grows over time"
],
"gotchas": [
"URL.revokeObjectURL() after img.onload โ otherwise blob stays in memory until tab close",
"Drag-and-drop requires preventDefault() on BOTH dragover AND drop events โ missing one = broken",
"file.type is unreliable โ 'image/jpeg' on .jpg but empty for files without extension",
"file.arrayBuffer() vs file.stream() โ arrayBuffer for small files, stream for > 100MB",
"file.text() decodes as UTF-8 โ notepad-saved Thai files might be TIS-620, need TextDecoder",
"<input type='file' accept='.jpg,.png'> โ still allows ALL files via OS dialog; validate file.type",
"Download via <a download='filename'> + URL.createObjectURL(blob) โ don't append to body"
],
"anti_patterns": [
"Loading entire file as base64 dataURL โ 33% larger, blocks main thread, no streaming",
"Not revoking object URLs โ memory leak per drag-drop; user opens 10 files = 10 blobs in RAM",
"Trusting file.name for security โ user can name malware.exe to myimage.jpg; check file.type",
"Sync processing of large files โ use Web Worker + OffscreenCanvas for > 5MB images"
],
"prereqs": [
"1.1",
"1.7"
],
"next_steps": [
"6.4",
"6.10",
"6.3"
],
"examples": [
"karaoke-studio",
"fitcheck",
"sj88cute-frame-extractor",
"cutout-studio",
"pos-grocery"
],
"snippet": "const handleFile = async (file) => { if (file.type.startsWith('image/')) { const url = URL.createObjectURL(file); img.src = url; img.onload = () => URL.revokeObjectURL(url); } }; input.addEventListener('change', e => handleFile(e.target.files[0]));",
"snippet_full": "// Full drag-drop + click-to-upload + preview + download\nconst FileHandler = (() => {\n const dropZone = document.getElementById('drop');\n const input = document.getElementById('file');\n const preview = document.getElementById('preview');\n let currentFile = null;\n async function handle(file) {\n if (!file) return;\n if (file.size > 50 * 1024 * 1024) { alert('File too large (>50MB)'); return; }\n currentFile = file;\n const url = URL.createObjectURL(file);\n if (file.type.startsWith('image/')) {\n preview.src = url;\n preview.onload = () => URL.revokeObjectURL(url);\n } else if (file.type.startsWith('text/')) {\n preview.textContent = await file.text();\n URL.revokeObjectURL(url);\n }\n }\n // Drag-drop\n ['dragenter', 'dragover'].forEach(e => dropZone.addEventListener(e, ev => { ev.preventDefault(); dropZone.classList.add('hover'); }));\n ['dragleave', 'drop'].forEach(e => dropZone.addEventListener(e, ev => { ev.preventDefault(); dropZone.classList.remove('hover'); }));\n dropZone.addEventListener('drop', e => handle(e.dataTransfer.files[0]));\n input.addEventListener('change', e => handle(e.target.files[0]));\n function download() {\n if (!currentFile) return;\n const a = document.createElement('a');\n a.href = URL.createObjectURL(currentFile);\n a.download = currentFile.name;\n a.click();\n setTimeout(() => URL.revokeObjectURL(a.href), 100);\n }\n return { handle, download };\n})();",
"tags": [
"file",
"upload",
"drag",
"blob",
"download"
],
"difficulty": "Beginner",
"time_to_learn": "30 min",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/API/File_API",
"compare_with": [
"Dropzone.js (50KB, more features)",
"Uppy (200KB, resumable uploads)",
"FormData + server (when you need to upload)"
]
},
{
"id": "1.9",
"title": "MediaRecorder API",
"icon": "โบ",
"description": "Record MediaStream to webm blob. getUserMedia, getDisplayMedia, canvas.captureStream. Foundation for video editor + screen recorder.",
"when_to_use": "video editor; screen recorder; karaoke export; webcam capture; audio transcription",
"when_not_to_use": "real-time video call (WebRTC); long recording > 1h; production streaming (HLS/DASH)",
"pros": [
"Browser-native โ no ffmpeg.wasm for simple WebM recording; smaller bundle",
"GPU-accelerated โ canvas.captureStream uses compositor; 4K@60fps on modern GPUs",
"Multi-stream mix โ audio + video + screen capture in 1 MediaStream; karaoke-studio uses this",
"Codec choices โ vp8/vp9/h264, opus/vorbis, depending on browser; pick by mimeType"
],
"cons": [
"WebM only on most browsers โ Safari ships mp4 only since v14.1; mp4 is iOS-friendly",
"Audio/video sync drift โ mic + screen can drift 200ms+ over 5 min; resync in post",
"RAM spike during long recordings โ chunks pile up in JS array; stream to disk in worker",
"No pause/resume API โ must stop(), save, then start() a new recorder (joins are janky)"
],
"gotchas": [
"Safari 16+ supports video/mp4 mimeType โ use feature detection: MediaRecorder.isTypeSupported('video/mp4; codecs=avc1')",
"canvas.captureStream(30) for 30fps โ without param, captures only when canvas changes (bandwidth saver)",
"Multi-track via MediaStream โ add tracks together: new MediaStream([...video.getTracks(), ...audio.getTracks()])",
"dataavailable fires per timeslice โ use {timeslice: 1000} for 1s chunks; otherwise fires only on stop",
"Stop on last dataavailable โ recorder.requestData() then recorder.stop() ensures all chunks saved",
"getDisplayMedia({audio:true}) for system audio โ Chrome only; Firefox/Safari don't support yet",
"Camera LED always on โ cannot disable via JS (security); UX requires user consent prompt"
],
"anti_patterns": [
"Recording to single blob โ memory spike; use timeslice + chunks array",
"Setting videoBitsPerSecond too high โ 50Mbps on 720p = garbage quality; use 2-5 Mbps for screen",
"Not awaiting all dataavailable events โ stop() then immediately download() misses last chunk",
"Recording with mic + no mute check โ captures room noise; show visual feedback when recording"
],
"prereqs": [
"1.6",
"1.8"
],
"next_steps": [
"6.3",
"6.9",
"6.7"
],
"examples": [
"karaoke-studio",
"sj88-video-editor-pro",
"sj88cute-lut-preview",
"sj88cute-voice-isolator"
],
"snippet": "async function record(canvas, audioEl) { const vid = canvas.captureStream(30); const actx = new AudioContext(); const src = actx.createMediaElementSource(audioEl); const dest = actx.createMediaStreamDestination(); src.connect(dest); src.connect(actx.destination); const combined = new MediaStream([...vid.getVideoTracks(), ...dest.stream.getAudioTracks()]); return new MediaRecorder(combined, { mimeType: 'video/webm; codecs=vp9' }); }",
"snippet_full": "// Full screen recorder with system audio + mic + chunked storage\nconst Recorder = (() => {\n let rec = null, chunks = [];\n const pickCodec = () => {\n if (MediaRecorder.isTypeSupported('video/mp4;codecs=avc1')) return 'video/mp4;codecs=avc1';\n if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9')) return 'video/webm;codecs=vp9';\n return 'video/webm';\n };\n async function start() {\n chunks = [];\n const screen = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: 30 }, audio: true });\n const mic = await navigator.mediaDevices.getUserMedia({ audio: true });\n const audioTracks = [...screen.getAudioTracks(), ...mic.getAudioTracks()];\n const stream = new MediaStream([...screen.getVideoTracks(), ...audioTracks]);\n rec = new MediaRecorder(stream, { mimeType: pickCodec(), videoBitsPerSecond: 5_000_000 });\n rec.ondataavailable = e => e.data.size && chunks.push(e.data);\n rec.start(1000); // 1s chunks\n }\n function stop() {\n return new Promise(resolve => {\n rec.onstop = () => resolve(new Blob(chunks, { type: rec.mimeType }));\n rec.stop();\n rec.stream.getTracks().forEach(t => t.stop());\n });\n }\n function download(blob) {\n const a = document.createElement('a');\n a.href = URL.createObjectURL(blob);\n a.download = `recording-${Date.now()}.${blob.type.includes('mp4') ? 'mp4' : 'webm'}`;\n a.click();\n }\n return { start, stop, download };\n})();",
"tags": [
"recording",
"video",
"audio",
"stream",
"media"
],
"difficulty": "Advanced",
"time_to_learn": "3 hours",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder",
"compare_with": [
"WebCodecs API (low-level, 10x more code, more control)",
"WebRTC (peer-to-peer, real-time)",
"ffmpeg.wasm (cross-codec, +30MB bundle)"
]
},
{
"id": "1.10",
"title": "Service Workers (PWA)",
"icon": "๐ฑ",
"description": "Background script proxying network. Offline, installable, push notifications, background sync.",
"when_to_use": "every app user reuses; offline required; installable; push notifications",
"when_not_to_use": "one-time page; debug-difficulty important; legacy browser support critical",
"pros": [
"Offline โ cached resources work without network; perfect for travel/commute apps",
"Installable โ user can add to home screen; feels like native app, no app store",
"Push notifications โ re-engage users even when tab is closed (needs server-side push)",
"Background sync โ queue actions offline, replay when network returns"
],
"cons": [
"Dev experience tough โ cache invalidation is the hard part; one wrong strategy = stale forever",
"No DOM access โ fetch + postMessage only; can't update UI from SW directly",
"HTTPS required (except localhost) โ extra setup; certbot for production",
"First load requires network โ SW only intercepts on subsequent visits unless you precache"
],
"gotchas": [
"HTTPS required except localhost โ use http://localhost for dev or self-signed for testing",
"manifest.json + 192/512px icons required for installable โ missing icon = no install prompt",
"Change service-worker.js filename to force update โ e.g. sw-v2.js; browser byte-checks content",
"Cache-first for static assets, network-first for API, stale-while-revalidate for HTML โ hybrid",
"Update flow: skipWaiting() + clients.claim() to activate new SW immediately; otherwise old runs until all tabs close",
"iOS Safari PWA support limited โ no push, no background sync; cache works but no install badge",
"SW scope is path-based โ /sw.js only controls /pages/*; put SW at root for full control"
],
"anti_patterns": [
"Cache-first for API responses โ user sees stale data; use network-first with cache fallback",
"Versioning SW by content hash, not path โ hash changes force update but path-based is simpler",
"Precaching 100MB of assets โ first load takes 30s; precache only critical (< 5MB)",
"Using SW for non-network things (localStorage) โ no access; use IndexedDB or postMessage"
],
"prereqs": [
"1.7"
],
"next_steps": [
"8.2",
"1.4"
],
"examples": [
"flowboard",
"quick-note",
"habit-streak",
"memo-ai",
"knowledge-garden"
],
"snippet": "// /sw.js self.addEventListener('install', e => e.waitUntil(caches.open('v2').then(c => c.addAll(['/','/style.css','/app.js'])))); self.addEventListener('fetch', e => e.respondWith(caches.match(e.request).then(r => r || fetch(e.request))));",
"snippet_full": "// Production SW with cache strategies, update flow, push handler\n// sw.js\nconst VERSION = 'v2';\nconst STATIC = ['/', '/index.html', '/style.css', '/app.js', '/icon-192.png', '/icon-512.png'];\nconst CACHE_STATIC = `static-${VERSION}`;\nconst CACHE_API = `api-${VERSION}`;\nself.addEventListener('install', e => {\n e.waitUntil(caches.open(CACHE_STATIC).then(c => c.addAll(STATIC)));\n self.skipWaiting();\n});\nself.addEventListener('activate', e => {\n e.waitUntil(caches.keys().then(keys =>\n Promise.all(keys.filter(k => !k.endsWith(VERSION)).map(k => caches.delete(k)))\n ));\n return self.clients.claim();\n});\nself.addEventListener('fetch', e => {\n const url = new URL(e.request.url);\n if (url.pathname.startsWith('/api/')) {\n // Network-first for API with cache fallback\n e.respondWith(fetch(e.request).then(r => {\n const clone = r.clone();\n caches.open(CACHE_API).then(c => c.put(e.request, clone));\n return r;\n }).catch(() => caches.match(e.request)));\n } else {\n // Cache-first for static\n e.respondWith(caches.match(e.request).then(r => r || fetch(e.request)));\n }\n});\nself.addEventListener('push', e => {\n const data = e.data.json();\n self.registration.showNotification(data.title, { body: data.body, icon: '/icon-192.png' });\n});",
"tags": [
"pwa",
"service-worker",
"offline",
"push",
"cache"
],
"difficulty": "Advanced",
"time_to_learn": "1 day",
"docs_url": "https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API",
"compare_with": [
"AppCache (deprecated 2016)",
"Workbox (Google lib, 50KB, full toolbox)",
"Native apps (App Store, no web discoverability)"
]
}
]
}