Live demo: https://next-swr-quality-demo.vercel.app/
A small Next.js app that shows how to fetch data with safety and confidence. Cached responses return fast, background refresh keeps data fresh, and every error path is typed and tested. Clean domain boundaries, accessible UI states, and quality gates in CI.
Shows SWR caching, data validation, and accessible error handling.
On error, focus automatically moves to the alert (role="alert", aria-live="assertive").

- Contract-first data discipline: every API response is validated at the edge (Zod) and mapped into a single error shape (AppError) before it touches the UI.
- Predictable flow: domain boundary hides transport and caching; UI renders from a simple useQuery state machine (loading | success | empty | error).
- Pragmatic UX discipline: accessible errors (role=alert + focus), background revalidation for snappy reads, and TTL cache to avoid thrash.
- Quality gates you can trust: ESLint, typecheck, unit + E2E tests in CI, and Lighthouse budgets for perf/a11y/best-practices.
- Next.js 16 + React 19 + TypeScript (Node 22)
- Contracts at the edge (Zod), one error shape (AppError)
- Cache with TTL + SWR‑style revalidation (tiny
useQuery) - Tests: Vitest (unit), Playwright (e2e), MSW (mocks)
- A11y: Alert announces and auto‑focuses on error
- Perf: Lighthouse CI with budgets
- Storybook 8 (a11y addon) for component states
- CI: split jobs (check, e2e, lhci)
npm ci
npm run check # lint + typecheck + format:check + unit
npm run dev # http://localhost:3000
# Optional
npm run storybook # http://localhost:6006 (component states + a11y)
npx playwright install # first time only
npm run test:e2e # run e2e against dev server
npm run lhci # Lighthouse CI (requires a build + static serve)- dev: start Next.js in dev mode (port 3000)
- build: production build of the app
- start: start the built app
- lint: ESLint (Next core-web-vitals rules)
- typecheck: TypeScript project check
- format: Prettier write
- format:check: Prettier check only
- test:unit: Vitest unit tests (jsdom, MSW)
- test:e2e: Playwright tests
- storybook: Storybook dev server (port 6006)
- build-storybook: static Storybook build
- lhci: Lighthouse CI run (uses lighthouserc.json budgets)
- check: lint + typecheck + format:check + unit tests
- Domain:
src/domain/**- Hides HTTP + validation + cache from UI. Example:
src/domain/items/index.ts.
- Hides HTTP + validation + cache from UI. Example:
- Contracts:
src/lib/models.ts- Zod schemas,
ApiEnvelope,AppErrorfactory.
- Zod schemas,
- Transport:
src/lib/http.ts- Fetch wrapper with Abort, JSON/parse/HTTP/network →
AppErrormapping, optional schema validation.
- Fetch wrapper with Abort, JSON/parse/HTTP/network →
- Cache/SWR:
src/lib/cache.ts,src/hooks/useQuery.ts- In‑memory TTL store.
useQuery(key, fetcher, { ttl }|ttl)revalidates after paint.
- In‑memory TTL store.
- UI:
src/app/**, A11y:src/components/Alert.tsx- Pages import domain only. Alert uses role=alert, aria‑live, focus.
- Envelope:
ApiEnvelope(z.object(...))→{ data: ... }enforced per call. - AppError:
{ type: 'network'|'http'|'parse'|'validation'|'aborted'|'unknown', message, status? }. - Domain functions (e.g.,
listItems,getItem) return fully typed values or throwAppError.
- Unit (Vitest):
- HTTP normalization (network/HTTP/parse/validation/abort)
- Domain contract validation
- Cache TTL + SWR revalidation
useQuerystates (loading/success/empty/error) and abort on unmount- A11y: Alert role/live/focus
- E2E (Playwright): list → detail happy path with route‑mocked API
- Lint:
npm run lint(Next core‑web‑vitals) - Types:
npm run typecheck - Unit:
npm run test:unit - E2E:
npm run test:e2e - Format:
npm run format:check - Perf budgets:
npm run lhci(seelighthouserc.json) - All‑in:
npm run check
Budgets (targets): Performance ≥ 85, Accessibility ≥ 90, Best Practices ≥ 90.
- Framework: React + Vite
- Addons: essentials, a11y
- Command:
npm run storybook - Example:
src/components/Alert.stories.tsxcovers error/warning/info/success.
src/lib/models.ts— Zod schemas,AppErrorsrc/lib/http.ts— fetch wrapper + normalizationsrc/lib/cache.ts— TTL cache + SWR helpersrc/hooks/useQuery.ts— tiny query hook (TTL + revalidate)src/domain/items/index.ts— typed domain APIsrc/components/Alert.tsx— accessible error surfacetests/**— unit and setup;tests/e2e/**— Playwright
- Validate at edges for safety over micro‑perf
- Tiny utilities > heavy libs for clarity
- In‑memory cache for testability over persistence
- Route‑mocked E2E for speed; full backend integration tests are out of scope
- A11y baked into failure UI from day 1
Intentionally out of scope (to stay focused on the quality signals):
- No global state management (Redux/Zustand/RTK Query). Local state + small cache are enough here.
- No endpoint auth/security hardening. Mocked/demo API only; add auth, CSRF, rate‑limits in a real app.
- No bundle size tuning or production profiling. The goal is correctness + discipline, not bytes.
- No server/shared cache (Redis/CDN) or persistence layer. Only an in‑memory TTL for clarity.
- No mutations (create/update/delete) or optimistic UI; only read/query flows.
- No error reporting/observability wiring (Sentry/OpenTelemetry). Add when productized.
- No internationalization (i18n) or localization.
Next.js, React 19, TypeScript, Zod, MSW, Vitest, Playwright, Storybook, Tailwind, Lighthouse CI, ESLint, Prettier, GitHub Actions, A11y, SWR, AppError.
If you have 5 minutes, read src/lib/http.ts, src/lib/models.ts, and src/domain/items/index.ts for the core ideas in ~200 LOC.