feature-systems-pattern について
feature-systems-pattern は、TypeScript プロジェクトでドメイン単位のモジュールを構成する開発スキルです。API、クエリ層、フック、コンポーネント、公開エクスポートの設計と、依存関係やキャッシュの規則を案内します。
主な機能
- 01ドメイン別のシステムディレクトリ構成
- 02型付き API アダプターとエラー
- 03TanStack Query のキーとオプション
- 04フック、ミューテーション、楽観的更新
- 05Context と XState ストアのパターン
- 06依存方向とキャッシュ範囲の規則
活用シーン
- 01新しいドメインモジュールの構成
- 02クエリとミューテーションフックの追加
- 03楽観的キャッシュ更新の処理
- 04システム境界とエクスポートの確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marcioaltoe/ skills --skill feature-systems-patternインストール後、Claude Code で/feature-systems-patternを入力して読み込まれたか確認します。
同梱ファイルreferences/directory-layout.mdreferences/patterns.md
詳細
機能システムガイド
「システム」とは、1つのドメインに関連するすべての要素(API 呼び出し、クエリレイヤー、フック、コンポーネント、公開 API)を管理する、自己完結したドメイン駆動型のモジュールです。システムは systems/<domain>/ ディレクトリの下に配置します。
ディレクトリ構造と命名規則の全容については、references/directory-layout.md をお読みください。
各レイヤーの注釈付き実装パターンについては、references/patterns.md をお読みください。
クイックリファレンス
必須の併用スキル
システムは複数の技術領域にまたがるため、このスキルと併せて有効にしてください。
| 状況 | 有効にするスキル |
|---|---|
| フックまたはコンポーネント全般 | react + tanstack-query |
| データの取得/キャッシュ | tanstack-query |
| ミューテーション | tanstack-query |
| XState ストア | xstate |
| ユーティリティ関数 | es-toolkit |
| テストの作成/修正 | testing-boss + vitest |
| バグ修正 | systematic-debugging + no-workarounds |
システムディレクトリの概要
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
新しいシステムの作成手順
ステップ 1 — types.ts を定義する
- ドメインの型を明確にエクスポートし、生の API レスポンス形式は公開しないでください。
- 利用可能な場合は、プロジェクトの API コントラクト型から導出してください。
- 複雑な集約型には、導出ルールと不変条件を説明する JSDoc を記述してください。
ステップ 2 — API サービスレイヤーを構築する
adapters/<domain>-api.tsを作成します。- API 呼び出しには、プロジェクトの HTTP クライアントを使用します。
- 単一の名前空間オブジェクトをエクスポートします。例:
export const <domain>Api = { list, create, update, delete }。 - 型付きエラークラスをエクスポートします。例:
export class <Domain>ApiError extends Error { ... }。 - クエリのキャンセルに対応できるよう、すべての関数で
signal?: AbortSignalを受け取ります。 - エラーの抽出やレスポンスの正規化など、内部ヘルパーはすべてモジュール内に限定します。
ステップ 3 — 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,
};
- 細かな無効化ができるよう、階層構造のキーを使用します。
- キャッシュを正しく分離できる任意の識別子(userId、orgId など)でキーのスコープを設定します。
- 各キーのタプルには必ず
as constを指定します。
ステップ 4 — 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),
});
}
- 型安全性と再利用性を確保するため、
queryOptionsを使ってqueryKeyとqueryFnをまとめます。 - フック、プリフェッチ、ルートローダーで使用できるよう、各オプションファクトリーをエクスポートします。
- クエリコンテキストから受け取った
signalは、必ず API レイヤーに渡します。
ステップ 5 — フックを作成する
- クエリフック:
queryOptionsファクトリーでuseQueryをラップし、スコープ ID と任意の{ enabled? }を受け取ります。 - ミューテーションフック:適切な
onMutate/onError/onSettledコールバックを設定したuseMutationを使い、楽観的更新を実装します。 - ビューモデルフック:複数のフックを組み合わせてページ/シェルコンポーネント向けの処理を行い、フラットなオブジェクトを返します。
- テストは
hooks/__tests__/に配置するか、use-xxx.test.tsxとして対象の近くに配置します。
ミューテーションと楽観的更新の完全なパターンについては、references/patterns.md をお読みください。
ステップ 6 — (任意)コンテキストを追加する
クエリデータや組み合わせた状態を、プロパティのバケツリレーなしでコンポーネントツリー内で共有する必要がある場合は、contexts/<domain>-context.tsx を作成します。
// Always nullable context — consumer hook throws if used outside provider
export const <Domain>Context = createContext<<Domain>ContextValue | null>(null);
- コンテキスト、プロバイダーコンポーネント、再エクスポートするコンシューマーフックを同じファイルからエクスポートします。
- パフォーマンスが重要なツリーでは、Core/UI/Operations のサブコンテキストに分割します。
ステップ 7 — (任意)XState ストアを追加する
複雑な非同期ステートマシン(複数ステップのフロー、ポーリング、イベント発行)には、stores/<domain>-store.ts を作成します。
export const <domain>Store = createStore({
context: { ... } as <Domain>Context,
emits: { ... },
on: {
someEvent: (context, event, enqueue) => {
enqueue.effect(async () => { ... });
return { ...context, isLoading: true };
},
},
});
ステップ 8 — index.ts を接続する
ラベル付きのセクションと明示的な名前付きエクスポートを使って、バレルを整理します。
// 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";
重要なルール
- 同じ場所にまとめるには
queryOptionsを使用します。 再利用可能なオプションファクトリーにqueryKeyとqueryFnをまとめます。同じクエリーキーを複数のファイルに分散させないでください。 - 依存関係は一方向に流します。
adapters -> lib -> hooks -> components。アダプターからフックやコンポーネントをインポートしてはいけません。 - クエリーキーのスコープを設定します。 認証済みスコープ(ユーザー、組織、テナント)に依存するクエリーには、そのスコープ ID をキーに含め、スコープをまたいで古いデータが使われないようにします。
- API レイヤーでは型付きエラーを使用します。 アダプターから生のエラーをスローしてはいけません。コンシューマーがエラーメッセージの文字列を調べなくてもエラーの種類を判別できるよう、型付きエラークラスを使用します。
- AbortSignal を伝播させます。 クエリーを適切にキャンセルできるよう、
queryFnのコンテキストから渡されるsignalをすべての API 呼び出しに渡します。 - ミューテーション後は必ず無効化します。 サーバーとの結果整合性を確保するため、
onSettledでqueryClient.invalidateQueriesを使用します。 - 楽観的更新にはロールバックが必要です。 キャッシュベースの楽観的更新を行う場合は、
onMutateで以前のデータをスナップショットとして保存し、onErrorで復元します。 - 楽観的更新の前に、実行中のクエリーをキャンセルします。 再フェッチによって楽観的な状態が上書きされないよう、
onMutateでqueryClient.cancelQueriesを呼び出します。 - Zod スキーマは
lib/に配置します。 API 境界で実行時検証を行うため、すべての Zod スキーマをlib/<domain>-schemas.tsに配置します。
エラー処理
- API レイヤーが型付きエラーをスローします:TanStack Query がエラーを捕捉し、
query.errorを通じて公開します。 - 楽観的更新を伴うミューテーションが失敗します:
onErrorコールバックが、onMutateで取得したスナップショットを使ってキャッシュをロールバックします。その後、onSettledがキャッシュを無効化し、最新のデータを再取得します。 - スコープをまたいで古いデータが使われます:スコープ ID をクエリーキーに追加し、
enabledの条件でBoolean(scopeId)が確認されていることを確かめます。 - アンマウント時にクエリーがキャンセルされます:コンポーネントがアンマウントされると、TanStack Query は
signalを使って実行中のクエリーを自動的にキャンセルします。signalが API レイヤーまで伝播されていることを確認してください。
詳細なリファレンス
references/directory-layout.md— ディレクトリー構成、ファイル命名規則、バレルに関する規約の全容references/patterns.md— API レイヤー、クエリーオプション、フック、ミューテーション、楽観的更新、コンテキスト、ストアに関するコードパターンの解説