Phase 1 of 10

๐Ÿ“ฆ Foundations

Stack backbone โ€” เธ•เน‰เธญเธ‡เธกเธตเธเนˆเธญเธ™เน€เธฃเธดเนˆเนˆเธกเธ—เธธเธเน‚เธ›เธฃเน€เธŠเธ„

10 sub-phases
40 pros
42 cons
70 gotchas
40 anti-patterns
10575 chars code
1.1

๐ŸŒ HTML5 Boilerplate

Beginnerโฑ 5 min

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 (5)
โ€ข 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 (5)
โ€ข 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 (8)
โ€ข 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
๐Ÿ”€ Alternatives
vs Vite + React (5x more deps, faster DX)
vs Next.js (SSR + routing overkill for 5-page apps)
โžก๏ธ Next: 1.2, 1.3, 1.7
๐Ÿ’ป Minimal (333 chars) โ–ถ Full example (516 chars)
<!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>
// Full SPA shell with module loading + global state + viewport meta
const App = (() => {
  const state = { user: null, theme: 'dark' };
  const listeners = new Set();
  const set = (k, v) => { state[k] = v; listeners.forEach(fn => fn(state)); };
  return {
    state,
    on: (fn) => listeners.add(fn),
    init: () => {
      document.documentElement.dataset.theme = state.theme;
      document.getElementById('app').innerHTML = '<h1>Hello</h1>';
    }
  };
})();
App.on(s => console.log('state:', s));
App.init();
htmlvanillaboilerplatespashell
1.2

๐ŸŽจ CSS Variables & Design Tokens

Beginnerโฑ 30 min

: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 (4)
โ€ข 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 (4)
โ€ข 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 (7)
โ€ข 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
๐Ÿ”€ Alternatives
vs Sass variables (compile-time, no runtime theme switch)
vs Tailwind theme (utility class explosion, harder to debug)
๐Ÿ“š Prereqs: 1.1
โžก๏ธ Next: 2.1, 2.2, 2.8
๐Ÿ’ป Minimal (136 chars) โ–ถ Full example (640 chars)
:root { --accent:#ec4899; --bg:#0a0a14; --text:#f5f5f7; --radius:12px; } .btn { background:var(--accent); border-radius:var(--radius); }
/* Multi-theme system with semantic tokens + dark mode */
:root {
  --color-bg: hsl(240 10% 5%);
  --color-bg-elevated: hsl(240 10% 8%);
  --color-text: hsl(0 0% 96%);
  --color-text-muted: hsl(0 0% 64%);
  --color-accent: hsl(330 81% 60%);
  --color-success: hsl(160 84% 39%);
  --color-error: hsl(0 84% 60%);
  --space-1: 4px; --space-2: 8px; --space-4: 16px;
  --radius-md: 12px; --radius-pill: 9999px;
}
[data-theme='light'] {
  --color-bg: hsl(0 0% 98%);
  --color-text: hsl(240 10% 10%);
}
/* Inline script in <head> to prevent FOUC */
<script>document.documentElement.dataset.theme = localStorage.getItem('theme') || 'dark';</script>
csstokensthemevariablesdesign-system
1.3

๐Ÿงฉ Vanilla JS Module Pattern (IIFE)

Intermediateโฑ 15 min

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 (3)
โ€ข 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 (4)
โ€ข 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 (6)
โ€ข 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
๐Ÿ”€ Alternatives
vs ESM (modern, tree-shakable)
vs CommonJS (Node.js only)
vs AMD (deprecated)
๐Ÿ“ฆ Used in (4)
๐Ÿ“š Prereqs: 1.1
โžก๏ธ Next: 3.10, 4.1
๐Ÿ’ป Minimal (115 chars) โ–ถ Full example (535 chars)
const MyApp = (() => { const secret = 'hidden'; return { init(opts) { /* */ }, version:'1.0' }; })(); MyApp.init();
// Revealing Module Pattern with private state + public API
const GameState = (() => {
  'use strict';
  let _state = { score: 0, level: 1 };
  const _listeners = new Set();
  function _emit() { _listeners.forEach(fn => fn(_state)); }
  return {
    get: () => ({ ..._state }),
    addScore: (n) => { _state.score += n; _emit(); },
    on: (fn) => _listeners.add(fn),
    reset: () => { _state = { score: 0, level: 1 }; _emit(); }
  };
})();
GameState.on(s => console.log('score:', s.score));
GameState.addScore(10); // logs: score: 10
jsmoduleiifeclosurepattern
1.4

๐Ÿ’พ localStorage Save/Load + Migration

Intermediateโฑ 1 hour

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 (4)
โ€ข 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)
โ€ข 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 (7)
โ€ข 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
๐Ÿ”€ Alternatives
vs IndexedDB (async, MB-GB, structured queries)
vs sessionStorage (per-tab, ephemeral)
vs Cookies (HTTP-only, server-readable)
๐Ÿ“š Prereqs: 1.1
โžก๏ธ Next: 9.4, 4.10
๐Ÿ’ป Minimal (200 chars) โ–ถ Full example (1016 chars)
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');
// Production-grade localStorage with versioning, debounce, migration
const Store = (() => {
  const KEY = 'app_v3';
  const MIGRATIONS = {
    1: (d) => ({ ...d, achievements: d.achievements || [] }),
    2: (d) => ({ ...d, weather: d.weather || 'cloudy' })
  };
  let _timer = null;
  return {
    save(state) {
      clearTimeout(_timer);
      _timer = setTimeout(() => {
        try { localStorage.setItem(KEY, JSON.stringify({ ver: 3, data: state, ts: Date.now() })); }
        catch (e) { if (e.name === 'QuotaExceededError') console.warn('save failed: storage full'); }
      }, 300);
    },
    load() {
      for (let i = 3; i >= 1; i--) {
        const raw = localStorage.getItem(`app_v${i}`);
        if (!raw) continue;
        try {
          let obj = JSON.parse(raw);
          for (let v = obj.ver || 1; v <= 3; v++) if (MIGRATIONS[v]) obj.data = MIGRATIONS[v](obj.data);
          return obj.data;
        } catch (e) { console.warn(`v${i} corrupted`); }
      }
      return null;
    }
  };
})();
storagesavemigrationofflineversioning
1.5

๐Ÿ”Š Web Audio API (Synthesized SFX)

Intermediateโฑ 2 hours

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 (4)
โ€ข 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 (4)
โ€ข 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 (7)
โ€ข 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
๐Ÿ”€ Alternatives
vs Tone.js (heavier, music library, 100KB)
vs Howler.js (file-based, 12KB)
vs HTMLAudioElement (file-based, limited)
๐Ÿ“š Prereqs: 1.1
โžก๏ธ Next: 6.1, 6.6
๐Ÿ’ป Minimal (341 chars) โ–ถ Full example (1080 chars)
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); };
// Full game SFX system with envelope, sweep, polyphony, iOS unlock
const SFX = (() => {
  let ctx = null;
  const unlock = () => {
    if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)();
    if (ctx.state === 'suspended') ctx.resume();
  };
  const play = (freq = 440, dur = 0.1, type = 'square', vol = 0.2) => {
    if (!ctx) return;
    const o = ctx.createOscillator();
    const g = ctx.createGain();
    o.type = type; o.frequency.value = freq;
    o.connect(g).connect(ctx.destination);
    const t = ctx.currentTime;
    g.gain.setValueAtTime(0, t);
    g.gain.linearRampToValueAtTime(vol, t + 0.005);
    g.gain.exponentialRampToValueAtTime(0.001, t + dur);
    o.start(t); o.stop(t + dur);
  };
  // Preset SFX
  const jump = () => play(440, 0.15, 'square', 0.15);
  const hit = () => { play(150, 0.05, 'sawtooth', 0.3); play(80, 0.1, 'square', 0.2); };
  const coin = () => { play(880, 0.08); setTimeout(() => play(1320, 0.12), 80); };
  document.addEventListener('click', unlock, { once: true });
  return { play, jump, hit, coin, unlock };
})();
audiosfxsynthgameoscillator
1.6

๐Ÿงต Canvas 2D Rendering

Intermediateโฑ 3 hours

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 (4)
โ€ข 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 (4)
โ€ข 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 (7)
โ€ข 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()
๐Ÿ”€ Alternatives
vs WebGL/WebGPU (3D, 10x more code)
vs SVG (DOM-based, scales, slow > 1000 elements)
vs PixiJS (WebGL wrapper, faster but +400KB)
๐Ÿ“š Prereqs: 1.1
โžก๏ธ Next: 3.2, 3.3, 6.4, 4.1
๐Ÿ’ป Minimal (265 chars) โ–ถ Full example (1292 chars)
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();
// Production 2D game loop with HiDPI + fixed timestep + particle system
const Game = (() => {
  const c = document.getElementById('game');
  const ctx = c.getContext('2d', { alpha: false });
  const dpr = window.devicePixelRatio || 1;
  function resize() {
    c.width = innerWidth * dpr; c.height = innerHeight * dpr;
    c.style.width = innerWidth + 'px'; c.style.height = innerHeight + 'px';
    ctx.scale(dpr, dpr);
  }
  resize(); addEventListener('resize', resize);
  const particles = [];
  let last = performance.now(); const DT = 1/60;
  function update(dt) {
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx * dt; p.y += p.vy * dt; p.life -= dt * 50;
      if (p.life <= 0) particles.splice(i, 1);
    }
  }
  function render() {
    ctx.fillStyle = '#0a0a14'; ctx.fillRect(0, 0, innerWidth, innerHeight);
    particles.forEach(p => {
      ctx.fillStyle = `rgba(255, ${p.life * 2}, 0, ${p.life/100})`;
      ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill();
    });
  }
  function frame(now) {
    const dt = Math.min((now - last) / 1000, 0.25); last = now;
    update(dt); render();
    requestAnimationFrame(frame);
  }
  return { particles, start: () => requestAnimationFrame(frame) };
})();
Game.start();
canvas2drenderinggamegraphics
1.7

๐Ÿ“ก Fetch API + JSON + Error Handling

Beginnerโฑ 30 min

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 (4)
โ€ข 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 (4)
โ€ข 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 (7)
โ€ข 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
๐Ÿ”€ Alternatives
vs Axios (40KB, interceptors, older API)
vs ky (3KB, modern, retry built-in)
vs XHR (legacy, sync mode, no streaming)
๐Ÿ“š Prereqs: 1.1
โžก๏ธ Next: 9.1, 9.2, 9.6, 7.6
๐Ÿ’ป Minimal (318 chars) โ–ถ Full example (1331 chars)
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); } };
// Production-grade fetch wrapper with retry, timeout, auth, error mapping
const api = (() => {
  const BASE = '';
  const TOKEN_KEY = 'auth_token';
  async function call(path, opts = {}) {
    const url = `${BASE}${path}`;
    const ctrl = new AbortController();
    const tid = setTimeout(() => ctrl.abort(), opts.timeout || 15000);
    const headers = { 'Content-Type': 'application/json', ...opts.headers };
    const token = localStorage.getItem(TOKEN_KEY);
    if (token) headers.Authorization = `Bearer ${token}`;
    let attempt = 0; let lastErr;
    while (attempt < (opts.retries || 3)) {
      try {
        const r = await fetch(url, { ...opts, headers, signal: ctrl.signal });
        if (r.status === 429 || r.status >= 500) throw new Error(`HTTP ${r.status}`);
        if (!r.ok) throw Object.assign(new Error(r.statusText), { status: r.status, body: await r.text() });
        return r.status === 204 ? null : await r.json();
      } catch (e) {
        lastErr = e; attempt++;
        if (attempt < (opts.retries || 3)) await new Promise(r => setTimeout(r, 200 * 2 ** attempt));
      } finally { clearTimeout(tid); }
    }
    throw lastErr;
  }
  return { get: (p) => call(p), post: (p, b) => call(p, { method: 'POST', body: JSON.stringify(b) }) };
})();
const projects = await api.get('/api/projects?limit=20');
fetchapijsonhttppromise
1.8

๐Ÿ“ File API (Upload/Download/Drag)

Beginnerโฑ 30 min

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 (4)
โ€ข 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 (4)
โ€ข 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 (7)
โ€ข 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
๐Ÿ”€ Alternatives
vs Dropzone.js (50KB, more features)
vs Uppy (200KB, resumable uploads)
vs FormData + server (when you need to upload)
๐Ÿ“š Prereqs: 1.1, 1.7
โžก๏ธ Next: 6.4, 6.10, 6.3
๐Ÿ’ป Minimal (248 chars) โ–ถ Full example (1452 chars)
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]));
// Full drag-drop + click-to-upload + preview + download
const FileHandler = (() => {
  const dropZone = document.getElementById('drop');
  const input = document.getElementById('file');
  const preview = document.getElementById('preview');
  let currentFile = null;
  async function handle(file) {
    if (!file) return;
    if (file.size > 50 * 1024 * 1024) { alert('File too large (>50MB)'); return; }
    currentFile = file;
    const url = URL.createObjectURL(file);
    if (file.type.startsWith('image/')) {
      preview.src = url;
      preview.onload = () => URL.revokeObjectURL(url);
    } else if (file.type.startsWith('text/')) {
      preview.textContent = await file.text();
      URL.revokeObjectURL(url);
    }
  }
  // Drag-drop
  ['dragenter', 'dragover'].forEach(e => dropZone.addEventListener(e, ev => { ev.preventDefault(); dropZone.classList.add('hover'); }));
  ['dragleave', 'drop'].forEach(e => dropZone.addEventListener(e, ev => { ev.preventDefault(); dropZone.classList.remove('hover'); }));
  dropZone.addEventListener('drop', e => handle(e.dataTransfer.files[0]));
  input.addEventListener('change', e => handle(e.target.files[0]));
  function download() {
    if (!currentFile) return;
    const a = document.createElement('a');
    a.href = URL.createObjectURL(currentFile);
    a.download = currentFile.name;
    a.click();
    setTimeout(() => URL.revokeObjectURL(a.href), 100);
  }
  return { handle, download };
})();
fileuploaddragblobdownload
1.9

โบ MediaRecorder API

Advancedโฑ 3 hours

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 (4)
โ€ข 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 (4)
โ€ข 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 (7)
โ€ข 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
๐Ÿ”€ Alternatives
vs WebCodecs API (low-level, 10x more code, more control)
vs WebRTC (peer-to-peer, real-time)
vs ffmpeg.wasm (cross-codec, +30MB bundle)
๐Ÿ“š Prereqs: 1.6, 1.8
โžก๏ธ Next: 6.3, 6.9, 6.7
๐Ÿ’ป Minimal (435 chars) โ–ถ Full example (1438 chars)
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' }); }
// Full screen recorder with system audio + mic + chunked storage
const Recorder = (() => {
  let rec = null, chunks = [];
  const pickCodec = () => {
    if (MediaRecorder.isTypeSupported('video/mp4;codecs=avc1')) return 'video/mp4;codecs=avc1';
    if (MediaRecorder.isTypeSupported('video/webm;codecs=vp9')) return 'video/webm;codecs=vp9';
    return 'video/webm';
  };
  async function start() {
    chunks = [];
    const screen = await navigator.mediaDevices.getDisplayMedia({ video: { frameRate: 30 }, audio: true });
    const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
    const audioTracks = [...screen.getAudioTracks(), ...mic.getAudioTracks()];
    const stream = new MediaStream([...screen.getVideoTracks(), ...audioTracks]);
    rec = new MediaRecorder(stream, { mimeType: pickCodec(), videoBitsPerSecond: 5_000_000 });
    rec.ondataavailable = e => e.data.size && chunks.push(e.data);
    rec.start(1000); // 1s chunks
  }
  function stop() {
    return new Promise(resolve => {
      rec.onstop = () => resolve(new Blob(chunks, { type: rec.mimeType }));
      rec.stop();
      rec.stream.getTracks().forEach(t => t.stop());
    });
  }
  function download(blob) {
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = `recording-${Date.now()}.${blob.type.includes('mp4') ? 'mp4' : 'webm'}`;
    a.click();
  }
  return { start, stop, download };
})();
recordingvideoaudiostreammedia
1.10

๐Ÿ“ฑ Service Workers (PWA)

Advancedโฑ 1 day

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 (4)
โ€ข 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 (4)
โ€ข 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 (7)
โ€ข 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
๐Ÿ”€ Alternatives
vs AppCache (deprecated 2016)
vs Workbox (Google lib, 50KB, full toolbox)
vs Native apps (App Store, no web discoverability)
๐Ÿ“š Prereqs: 1.7
โžก๏ธ Next: 8.2, 1.4
๐Ÿ’ป Minimal (240 chars) โ–ถ Full example (1275 chars)
// /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))));
// Production SW with cache strategies, update flow, push handler
// sw.js
const VERSION = 'v2';
const STATIC = ['/', '/index.html', '/style.css', '/app.js', '/icon-192.png', '/icon-512.png'];
const CACHE_STATIC = `static-${VERSION}`;
const CACHE_API = `api-${VERSION}`;
self.addEventListener('install', e => {
  e.waitUntil(caches.open(CACHE_STATIC).then(c => c.addAll(STATIC)));
  self.skipWaiting();
});
self.addEventListener('activate', e => {
  e.waitUntil(caches.keys().then(keys =>
    Promise.all(keys.filter(k => !k.endsWith(VERSION)).map(k => caches.delete(k)))
  ));
  return self.clients.claim();
});
self.addEventListener('fetch', e => {
  const url = new URL(e.request.url);
  if (url.pathname.startsWith('/api/')) {
    // Network-first for API with cache fallback
    e.respondWith(fetch(e.request).then(r => {
      const clone = r.clone();
      caches.open(CACHE_API).then(c => c.put(e.request, clone));
      return r;
    }).catch(() => caches.match(e.request)));
  } else {
    // Cache-first for static
    e.respondWith(caches.match(e.request).then(r => r || fetch(e.request)));
  }
});
self.addEventListener('push', e => {
  const data = e.data.json();
  self.registration.showNotification(data.title, { body: data.body, icon: '/icon-192.png' });
});
pwaservice-workerofflinepushcache

๐Ÿค– 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)"
      ]
    }
  ]
}
๐Ÿ”— /api/skill (all 100) ๐Ÿ’พ Download foundations.json