Feature Systems Guide
SkillDev toolsGuides your agent in building and editing domain feature systems with set directory layout, hooks, and query patterns.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Feature Systems Guide skill
About this capability
Create, extend, or debug frontend domain modules organized under systems/. Covers adapters, Query hooks, local state, and public exports; excludes generic components and backend implementation.
What this skill tells your AI
The instructions your AI receives, as published by compozy/compozy in .agents/skills/app-renderer-systems/SKILL.md and read by ahel’s review.
A "system" is a self-contained, domain-driven module that owns everything related to one domain: its API calls, query layer, hooks, components, and public API. Systems live under a systems/<domain>/ directory.
Read references/directory-layout.md for the full directory structure and naming conventions.
Read references/patterns.md for annotated implementation patterns per layer.
Quick Reference
Mandatory Companion Skills
Activate alongside this skill — systems span multiple technical domains:
| Situation | Activate |
|---|---|
| Any hook or component | react + tanstack |
| Data fetching/caching | eng-data-boundaries + tanstack |
| Mutations | tanstack |
| XState store | xstate |
| Utility functions | es-toolkit |
| Writing/fixing tests | test-antipatterns + vitest |
| Bug fix | systematic-debugging + no-workarounds |
System Directory at a Glance
systems/<domain>/
├── index.ts # Public API barrel — required for every system
├── types.ts # TypeScript types for this domain
├── adapters/ # API service layer (HTTP calls, error types)
│ └── <domain>-api.ts
├── lib/ # Pure utilities, schemas, constants, query keys
│ ├── query-keys.ts # TanStack Query key factory
│ ├── query-options.ts # Reusable queryOptions / mutationOptions
│ ├── <domain>-schemas.ts
│ └── constants.ts
├── hooks/ # React hooks (queries, mutations, view-models)
│ ├── __tests__/
│ ├── use-<action>.ts # Query hooks
│ ├── use-create-<entity>.ts # Mutation hooks
│ ├── use-update-<entity>.ts
│ ├── use-delete-<entity>.ts
│ └── use-<domain>-view-model.ts
├── contexts/ # React contexts + providers
│ └── <domain>-context.tsx
├── stores/ # XState stores (complex async state machines)
│ └── <domain>-store.ts
├── components/ # React UI components
│ ├── stories/
│ └── index.ts
└── guards/ # Route guards / access checks
Step-by-Step: Creating a New System
Step 1 — Define types.ts
- Export clean component view-model types; preserve API page envelopes in adapters and query caches, then flatten at the hook/view-model read boundary.
- Derive from the project's API contract types when available.
- Document complex aggregated types with JSDoc explaining derivation rules and invariants.
Step 2 — Build the API service layer
- Create
adapters/<domain>-api.ts. - Use the project's HTTP client for API calls.
- Export a single namespace object:
export const <domain>Api = { list, create, update, delete }. - Export a typed error class:
export class <Domain>ApiError extends Error { ... }. - Accept
signal?: AbortSignalon every function to support query cancellation. - Keep all internal helpers (error extraction, response normalization) private to the module.
Step 3 — Add lib/query-keys.ts
export const <domain>Keys = {
all: ["<domain>"] as const,
lists: () => [...<domain>Keys.all, "list"] as const,
list: (scopeId: string | null) => [...<domain>Keys.lists(), scopeId] as const,
details: () => [...<domain>Keys.all, "detail"] as const,
detail: (id: string) => [...<domain>Keys.details(), id] as const,
};
- Use hierarchical key structure for granular invalidation.
- Scope keys with any identifier (userId, orgId, etc.) that isolates the cache correctly.
- Use
as conston every key tuple.
Step 4 — Add lib/query-options.ts
import { queryOptions } from "@tanstack/react-query";
import { <domain>Api } from "../adapters/<domain>-api";
import { <domain>Keys } from "./query-keys";
export function <domain>ListOptions(scopeId: string | null) {
return queryOptions({
queryKey: <domain>Keys.list(scopeId),
queryFn: ({ signal }) => <domain>Api.list(scopeId!, signal),
staleTime: 60_000,
enabled: Boolean(scopeId),
});
}
export function <domain>DetailOptions(id: string) {
return queryOptions({
queryKey: <domain>Keys.detail(id),
queryFn: ({ signal }) => <domain>Api.get(id, signal),
enabled: Boolean(id),
});
}
- Co-locate
queryKeyandqueryFnviaqueryOptionsfor type safety and reuse. - Export each option factory for hooks, prefetching, route loaders, mutations, and stream writers.
- Keep stable scope/filters in the base key and continuation cursors only in
pageParam. - Always pass
signalfrom the query context through to the API layer.
Step 5 — Write hooks
- Query hooks: Wrap
useQuerywith thequeryOptionsfactories; accept a scope ID + optional{ enabled? }. - Mutation hooks: Use
useMutationwith properonMutate/onError/onSettledcallbacks for optimistic updates. - View-model hooks: Compose multiple hooks for a page/shell component; return a flat object.
- Place tests in
hooks/__tests__/or co-locate asuse-xxx.test.tsx.
Read references/patterns.md for complete mutation and optimistic update patterns.
Step 6 — (Optional) Add context
Create contexts/<domain>-context.tsx when query data or combined state must be shared across a component subtree without prop-drilling.
// Always nullable context — consumer hook throws if used outside provider
export const <Domain>Context = createContext<<Domain>ContextValue | null>(null);
- Export the context, provider component, and re-export consumer hooks from the same file.
- For performance-sensitive trees, split into Core / UI / Operations sub-contexts.
Step 7 — (Optional) Add an XState store
Create stores/<domain>-store.ts for complex async state machines (multi-step flows, polling, event emission).
export const <domain>Store = createStore({
context: { ... } as <Domain>Context,
emits: { ... },
on: {
someEvent: (context, event, enqueue) => {
enqueue.effect(async () => { ... });
return { ...context, isLoading: true };
},
},
});
Step 8 — Wire up index.ts
Organize the barrel with labeled sections and explicit named exports:
// Types
export type { <Domain>Type } from "./types";
// Hooks
export { use<Domain>List, use<Domain>Detail } from "./hooks";
export { useCreate<Domain>, useUpdate<Domain>, useDelete<Domain> } from "./hooks";
// Components
export { <Domain>Component } from "./components";
// Utilities
export { <domain>HelperFn } from "./lib/<domain>-utils";
// Query Keys & Options
export { <domain>Keys } from "./lib/query-keys";
export { <domain>ListOptions, <domain>DetailOptions } from "./lib/query-options";
// API
export { <domain>Api, <Domain>ApiError } from "./adapters/<domain>-api";
Critical Rules
- Use
queryOptionsfor co-location. Reuse option/key factories across loaders, hooks, mutations, and streams; never redeclare server-query identity at a consumer. - Unidirectional dependency flow.
adapters -> lib -> hooks -> components. Adapters never import from hooks or components. - Scope query keys. Include every stable scope/filter in the base key and keep pagination cursors in
pageParamto prevent cross-scope data and cache fragmentation. - Typed errors in the API layer. Never throw raw errors from adapters. Use a typed error class so consumers can distinguish error types without inspecting message strings.
- AbortSignal propagation. Pass
signalfrom thequeryFncontext through to every API call for proper query cancellation. - Follow the owner's mutation contract. Reconcile through canonical keys; invalidate on settle only when the read model requires a server reread.
- Optimistic updates require rollback. When using cache-based optimistic updates, snapshot previous data in
onMutateand restore inonError. - Cancel outgoing queries before optimistic updates. Call
queryClient.cancelQueriesinonMutateto prevent refetches from overwriting optimistic state. - Zod schemas in lib/. Place all Zod schemas in
lib/<domain>-schemas.tsfor runtime validation at API boundaries.
Error Handling
- API layer throws typed error: TanStack Query catches and exposes it via
query.error. - Mutation fails with optimistic update:
onErrorrestores the complete cache snapshot;onSettledrereads only when the owner contract requires it. - Stale cross-scope data: Add the scope ID to the query key and verify that
enabledguards checkBoolean(scopeId). - Query cancellation on unmount: TanStack Query automatically cancels in-flight queries via the
signalwhen a component unmounts — ensuresignalis propagated to the API layer.
Detailed References
references/directory-layout.md— Full directory structure, file naming, and barrel conventionsreferences/patterns.md— Annotated code patterns for the API layer, query options, hooks, mutations, optimistic updates, contexts, and stores
Signals
- GitHub stars
- 3k
- Forks
- 177
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
app-renderer-systems- Source
- github.com/compozy/compozy