The Craftsman standard for frontend application architecture — components, state management, data fetching, forms, routing, client/server boundaries, performance, error boundaries, and bundle size. Use this WHENEVER the work touches the application layer of a frontend: building components, wiring up API calls, deciding where state lives, handling loading/error/empty states, managing form validation, splitting bundles, or diagnosing slow pages. Trigger even when the user only says "wire up the API", "this page is slow", "manage this state", or "build the form" without naming a framework. Visual polish and design tokens → craft-ux; further handoffs in "Scope boundaries".
This skill encodes one engineer's standard for structuring frontend application code, applied the
same way across every repo. The method and opinions live here; the project specifics
(which framework, which data library, which router) are discovered from the repo — never hardcoded
or assumed.
Operating principle — discover before you build
Different repos already have different conventions. Before adding anything, spend two minutes
mapping what exists so you extend rather than duplicate:
package.json / lockfile → which framework (Next.js, React, Vue, Remix)? Which data layer
(@tanstack/react-query, swr, RTK Query)? Which form lib (react-hook-form, Formik)? Which
state approach (Zustand, Jotai, Context, URL-as-state)?
grep for an existing API client, query key factory, or base hook — wire into it, don't fork it.
Find the routing convention (file-based, config, nested layouts) and the client/server split
strategy before deciding where new code lives.
State what you found, then propose the smallest set of additions that closes the gap.
The frontend layers (build in this order)
Component architecture — decide the composition shape first: where are the boundaries,
what is server-rendered vs client-interactive, what gets co-located vs shared. Composition over
configuration: a component that accepts children and slots is more reusable than one with 20
props. See references/architecture.md.
State — start local (useState), lift only when two siblings genuinely share it, reach for
a global store only as a last resort. Server state (remote data) belongs in a query cache, not a
store. See references/state.md.
Data fetching — use a typed client that matches the repo's existing pattern. Every fetch
needs a loading state, an error state, and an empty state, all co-located with the component
that owns the data. Avoid waterfall fetches: parallelize or prefetch where the router allows.
See references/data-fetching.md.
Forms — validate with a schema library (Zod-style), couple it to the form hook, surface
errors accessibly next to the field that owns them. Optimistic updates are worth it when the
action is frequent and the rollback is cheap. See references/forms.md.
Performance — code-split at route boundaries by default, lazy-load heavy components, keep
the critical path lean. Reach for memoization only after measuring; premature optimization makes
code harder to read without a proven payoff. See references/performance.md.
Standing opinions (the non-negotiables)
These are the judgments that keep output consistent across repos — apply them unless the user
overrides:
Server state lives in a query cache, not a global store. A cache (React Query-style is the
opinion when applicable) gives you deduplication, background refresh, and stale-while-revalidate
for free — discover the repo's actual data layer first and extend it; don't bolt on React Query
beside an established SWR/RTK Query/server-loader pattern. A Zustand slice that manually mirrors
server data gives you bugs.
Forms are schema-validated with accessible errors. Define the shape once (Zod or equivalent),
derive both the TypeScript type and the runtime validation from it, and render error messages
adjacent to the field so screen readers find them.
No fetch waterfalls. If two data dependencies are independent, fetch them in parallel.
Co-locate the fetch with the component that renders it; don't hoist data to a parent just to
pass it down.
Every async UI has explicit loading, error, and empty states. A spinner that hides the rest
of the page is a loading state. A blank div when the list is empty is not an empty state — it is
a confusing silence.
Measure before optimizing. Lighthouse, bundle analysis (next build output, rollup-plugin- visualizer), and React DevTools profiler are the sources of truth. Gut feeling is a hypothesis,
not a reason to add useMemo.
Workflow
Discover the current state (framework, data layer, conventions) and report what exists.
Propose the implementation, ordered by the five layers above, smallest viable first.
Implement against the repo's existing patterns — its query client, its API conventions,
its form library, its routing approach.
Verify — run the app, exercise the happy path, the error path, and the empty path. A
component you haven't seen render in all three states is not done.
Scope boundaries
This skill covers the application-architecture layer — data, state, performance — and owns the
client side of the contract. Hand off at these lines:
Visual polish, design-system tokens, and layout decisions → craft-ux.
Server/API implementation and authentication → craft-backend.
Authorization policy and client-exposed secrets → craft-security.
Schema and query specifics behind the API → craft-db.
Cross-reference the neighbour skill whenever both concerns appear in the same task.
Reference index
Read the one matching the current task — they hold the concrete patterns, not this overview:
references/architecture.md — component boundaries, composition patterns, server vs client split
references/state.md — local state, lifting, query cache vs global store, URL-as-state
When craft-audit plans a frontend pass for a scope, it turns this checklist into the plan.md
todo list — the checklist is owned by this skill, not improvised by the orchestrator. Tailor to what
discovery found: skip a step that genuinely doesn't apply with a one-line reason; never silently drop
one. Emit findings using craft-audit workspace.md → "Canonical findings.md emission format"
(authority). Heading grammar (variables required — do not hardcode NNN/severity/status):
## <scopeLabel>-FE-<NNN> · severity <🔴|🟡|🟢> · status <open|fixed|wontfix (reason)|regressed|fixed (merged into <ID>)>
Example only: ## <scopeLabel>-FE-001 · severity 🔴 · status open
Required fields under each heading, in order, with these exact labels:
**What breaks (plain language):** · **Technical:** · **Fix:** · **Fingerprint:** ·
**Last-checked:** (optional **Confidence:** — verified | inferred | unverified-from-repo, absent
means verified — then optional **Fix-attempt:** only from craft-fix).
Assign sequential NNN per (scope, domain); judge severity with craft-audit prioritization.md.
Forbidden: ### headings; ## ID · 🔴 · open shorthand; severity/status as body bullets.
Map the repo's existing stack before judging anything — framework, data/cache lib, form lib,
state approach, router; flag new patterns bolted on beside an established convention → SKILL.md
(Operating principle)
Audit component boundaries — "use client" at the top of a large tree, data fetching tangled
into presentation components, god-components, boolean-prop piles, custom names shadowing native
props, index keys on reorderable lists → references/architecture.md
Check state placement — server data mirrored into a Zustand/Redux store, derived values stored
with a useState+useEffect sync, state over-lifted, filters/tab/sort lost on refresh
→ references/state.md (secrets/tokens in URL params is a security finding — flag it and
route to craft-security; references/state.md covers the state-placement side only)
Filters, pagination, tabs, and sort order are stored in URL params (not useState), so deep
links and back-nav work correctly → references/state.md (URL as state)
Review data fetching — raw inline fetch/unvalidated responses, ad-hoc scattered query keys,
a result-affecting variable missing from the key, sequential awaits or structural waterfalls,
staleTime left at 0 for stable data → references/data-fetching.md
Verify every async UI has explicit loading, error, AND empty states co-located at the fetch
site — a blank div for empty is a bug; one slow widget shouldn't blank the page → references/data-fetching.md
Inspect mutations — setQueryData with no onError rollback, optimistic updates on
destructive/irreversible actions, screens showing stale data because the touched key prefix was
never invalidated → references/data-fetching.md
Audit forms — hand-written types parallel to the schema, validation in handler ifs, errors
firing on every keystroke, submit disabled before any attempt or not disabled in-flight, server
trusting client-validated input → references/forms.md
Check form error accessibility — error text with no role="alert" region or
aria-describedby/aria-invalid wiring, first invalid field not focused on submit, server
errors swallowed to console → references/forms.md
Review performance against measurement — heavy leaf components not code-split at route
boundaries, >~20 KB deps on the critical path, prophylactic useMemo/memo with no profiled
cost, layout reads in render → references/performance.md
Run bundle analyzer (next build --analyze or source-map-explorer) and Lighthouse (throttled
network) — record an actual number for LCP and bundle size; pattern-checking without measurement
is not sufficient → references/performance.md
Component tests use renderWithClient and userEvent; API calls are mocked with MSW
→ references/testing.md
Tracking/analytics scripts (GA, Meta Pixel, PostHog, etc.) are gated on consent state, not
just the consent banner's visibility — verify with a fresh-load (incognito/cleared cookies)
Network tab check for tracker requests before any consent interaction → references/architecture.md
1---2name: craft-frontend3description: The Craftsman standard for frontend application architecture — components, state management, data fetching, forms, routing, client/server boundaries, performance, error boundaries, and bundle size. Use this WHENEVER the work touches the application layer of a frontend: building components, wiring up API calls, deciding where state lives, handling loading/error/empty states, managing form validation, splitting bundles, or diagnosing slow pages. Trigger even when the user only says "wire up the API", "this page is slow", "manage this state", or "build the form" without naming a framework. Visual polish and design tokens → craft-ux; further handoffs in "Scope boundaries".4---56# Frontend Craft78This skill encodes one engineer's standard for structuring frontend application code, applied the9same way across every repo. The **method and opinions** live here; the **project specifics**10(which framework, which data library, which router) are discovered from the repo — never hardcoded11or assumed.1213## Operating principle — discover before you build1415Different repos already have different conventions. Before adding anything, spend two minutes16mapping what exists so you extend rather than duplicate:1718- `package.json` / lockfile → which framework (Next.js, React, Vue, Remix)? Which data layer19 (`@tanstack/react-query`, `swr`, RTK Query)? Which form lib (`react-hook-form`, Formik)? Which20 state approach (Zustand, Jotai, Context, URL-as-state)?21- `grep` for an existing API client, query key factory, or base hook — wire into it, don't fork it.22- Find the routing convention (file-based, config, nested layouts) and the client/server split23 strategy before deciding where new code lives.2425State what you found, then propose the smallest set of additions that closes the gap.2627## The frontend layers (build in this order)28291. **Component architecture** — decide the composition shape first: where are the boundaries,30 what is server-rendered vs client-interactive, what gets co-located vs shared. Composition over31 configuration: a component that accepts children and slots is more reusable than one with 2032 props. See `references/architecture.md`.33342. **State** — start local (`useState`), lift only when two siblings genuinely share it, reach for35 a global store only as a last resort. Server state (remote data) belongs in a query cache, not a36 store. See `references/state.md`.37383. **Data fetching** — use a typed client that matches the repo's existing pattern. Every fetch39 needs a loading state, an error state, and an empty state, all co-located with the component40 that owns the data. Avoid waterfall fetches: parallelize or prefetch where the router allows.41 See `references/data-fetching.md`.42434. **Forms** — validate with a schema library (Zod-style), couple it to the form hook, surface44 errors accessibly next to the field that owns them. Optimistic updates are worth it when the45 action is frequent and the rollback is cheap. See `references/forms.md`.46475. **Performance** — code-split at route boundaries by default, lazy-load heavy components, keep48 the critical path lean. Reach for memoization only after measuring; premature optimization makes49 code harder to read without a proven payoff. See `references/performance.md`.5051## Standing opinions (the non-negotiables)5253These are the judgments that keep output consistent across repos — apply them unless the user54overrides:5556- **Server state lives in a query cache, not a global store.** A cache (React Query-style is the57 opinion when applicable) gives you deduplication, background refresh, and stale-while-revalidate58 for free — **discover the repo's actual data layer first** and extend it; don't bolt on React Query59 beside an established SWR/RTK Query/server-loader pattern. A Zustand slice that manually mirrors60 server data gives you bugs.61- **Forms are schema-validated with accessible errors.** Define the shape once (Zod or equivalent),62 derive both the TypeScript type and the runtime validation from it, and render error messages63 adjacent to the field so screen readers find them.64- **No fetch waterfalls.** If two data dependencies are independent, fetch them in parallel.65 Co-locate the fetch with the component that renders it; don't hoist data to a parent just to66 pass it down.67- **Every async UI has explicit loading, error, and empty states.** A spinner that hides the rest68 of the page is a loading state. A blank div when the list is empty is not an empty state — it is69 a confusing silence.70- **Measure before optimizing.** Lighthouse, bundle analysis (`next build` output, `rollup-plugin-71visualizer`), and React DevTools profiler are the sources of truth. Gut feeling is a hypothesis,72 not a reason to add `useMemo`.7374## Workflow75761. **Discover** the current state (framework, data layer, conventions) and report what exists.772. **Propose** the implementation, ordered by the five layers above, smallest viable first.783. **Implement** against the repo's existing patterns — its query client, its API conventions,79 its form library, its routing approach.804. **Verify** — run the app, exercise the happy path, the error path, and the empty path. A81 component you haven't seen render in all three states is not done.8283## Scope boundaries8485This skill covers the application-architecture layer — data, state, performance — and owns the86client side of the contract. Hand off at these lines:8788- **Visual polish, design-system tokens, and layout decisions** → `craft-ux`.89- **Server/API implementation and authentication** → `craft-backend`.90- **Authorization policy and client-exposed secrets** → `craft-security`.91- **Schema and query specifics behind the API** → `craft-db`.9293Cross-reference the neighbour skill whenever both concerns appear in the same task.9495## Reference index9697Read the one matching the current task — they hold the concrete patterns, not this overview:9899- `references/architecture.md` — component boundaries, composition patterns, server vs client split100- `references/state.md` — local state, lifting, query cache vs global store, URL-as-state101- `references/data-fetching.md` — typed clients, query key factories, parallel fetching, caching102- `references/forms.md` — schema validation, form hooks, accessible errors, optimistic updates103- `references/performance.md` — code splitting, lazy loading, bundle analysis, memoization rules104- `references/testing.md` — renderWithClient wrapper, user-event for RHF forms, MSW v2 mocking, co-location105106## Audit checklist (for craft-audit)107108When `craft-audit` plans a frontend pass for a scope, it turns this checklist into the `plan.md`109todo list — the checklist is owned by this skill, not improvised by the orchestrator. Tailor to what110discovery found: skip a step that genuinely doesn't apply with a one-line reason; never silently drop111one. Emit findings using craft-audit `workspace.md` → "Canonical findings.md emission format"112(authority). Heading grammar (variables required — do not hardcode NNN/severity/status):113114`## <scopeLabel>-FE-<NNN> · severity <🔴|🟡|🟢> · status <open|fixed|wontfix (reason)|regressed|fixed (merged into <ID>)>`115116Example only: `## <scopeLabel>-FE-001 · severity 🔴 · status open`117118Required fields under each heading, in order, with these exact labels:119`**What breaks (plain language):**` · `**Technical:**` · `**Fix:**` · `**Fingerprint:**` ·120`**Last-checked:**` (optional `**Confidence:**` — `verified | inferred | unverified-from-repo`, absent121means `verified` — then optional `**Fix-attempt:**` only from craft-fix).122Assign sequential NNN per (scope, domain); judge severity with craft-audit `prioritization.md`.123Forbidden: `###` headings; `## ID · 🔴 · open` shorthand; severity/status as body bullets.124125- [ ] Map the repo's existing stack before judging anything — framework, data/cache lib, form lib,126 state approach, router; flag new patterns bolted on beside an established convention → SKILL.md127 (Operating principle)128- [ ] Audit component boundaries — `"use client"` at the top of a large tree, data fetching tangled129 into presentation components, god-components, boolean-prop piles, custom names shadowing native130 props, index keys on reorderable lists → `references/architecture.md`131- [ ] Check state placement — server data mirrored into a Zustand/Redux store, derived values stored132 with a `useState`+`useEffect` sync, state over-lifted, filters/tab/sort lost on refresh133 → `references/state.md` (secrets/tokens in URL params is a security finding — flag it and134 route to craft-security; `references/state.md` covers the state-placement side only)135- [ ] Filters, pagination, tabs, and sort order are stored in URL params (not `useState`), so deep136 links and back-nav work correctly → `references/state.md` (URL as state)137- [ ] Review data fetching — raw inline `fetch`/unvalidated responses, ad-hoc scattered query keys,138 a result-affecting variable missing from the key, sequential `await`s or structural waterfalls,139 `staleTime` left at `0` for stable data → `references/data-fetching.md`140- [ ] Verify every async UI has explicit loading, error, AND empty states co-located at the fetch141 site — a blank div for empty is a bug; one slow widget shouldn't blank the page → `references/data-fetching.md`142- [ ] Inspect mutations — `setQueryData` with no `onError` rollback, optimistic updates on143 destructive/irreversible actions, screens showing stale data because the touched key prefix was144 never invalidated → `references/data-fetching.md`145- [ ] Audit forms — hand-written types parallel to the schema, validation in handler `if`s, errors146 firing on every keystroke, submit disabled before any attempt or not disabled in-flight, server147 trusting client-validated input → `references/forms.md`148- [ ] Check form error accessibility — error text with no `role="alert"` region or149 `aria-describedby`/`aria-invalid` wiring, first invalid field not focused on submit, server150 errors swallowed to console → `references/forms.md`151- [ ] Review performance against measurement — heavy leaf components not code-split at route152 boundaries, >~20 KB deps on the critical path, prophylactic `useMemo`/`memo` with no profiled153 cost, layout reads in render → `references/performance.md`154- [ ] Run bundle analyzer (`next build --analyze` or `source-map-explorer`) and Lighthouse (throttled155 network) — record an actual number for LCP and bundle size; pattern-checking without measurement156 is not sufficient → `references/performance.md`157- [ ] Component tests use `renderWithClient` and `userEvent`; API calls are mocked with MSW158 → `references/testing.md`159- [ ] Tracking/analytics scripts (GA, Meta Pixel, PostHog, etc.) are gated on consent state, not160 just the consent banner's visibility — verify with a fresh-load (incognito/cleared cookies)161 Network tab check for tracker requests before any consent interaction → `references/architecture.md`162
Run npx skillmds@latest add gul-labs/craft-frontend in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
The Craftsman standard for frontend application architecture — components, state management, data fetching, forms, routing, client/server boundaries, performance, error boundaries, and bundle size. Use this WHENEVER the work touches the application layer of a frontend: building components, wiring up API calls, deciding where state lives, handling loading/error/empty states, managing form validation, splitting bundles, or diagnosing slow pages. Trigger even when the user only says "wire up the API", "this page is slow", "manage this state", or "build the form" without naming a framework. Visual polish and design tokens → craft-ux; further handoffs in "Scope boundaries". It is listed under Web & Frontend on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
gul-labs (@gul-labs) published this skill. Their other Agent Skills are listed on their SkillMD profile.