@react-vault/create-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/LICENSE +12 -0
  2. package/README.md +16 -0
  3. package/bin/create-app.js +8 -0
  4. package/claude-toolkit/README.md +131 -0
  5. package/claude-toolkit/agents/bfsi-accessibility-auditor.md +132 -0
  6. package/claude-toolkit/agents/bfsi-architect.md +156 -0
  7. package/claude-toolkit/agents/bfsi-code-reviewer.md +137 -0
  8. package/claude-toolkit/agents/bfsi-compliance-auditor.md +161 -0
  9. package/claude-toolkit/agents/bfsi-pii-scanner.md +142 -0
  10. package/claude-toolkit/agents/bfsi-pr-reviewer.md +114 -0
  11. package/claude-toolkit/agents/bfsi-security-reviewer.md +136 -0
  12. package/claude-toolkit/commands/bfsi-audit.md +46 -0
  13. package/claude-toolkit/commands/bfsi-doctor.md +97 -0
  14. package/claude-toolkit/commands/bfsi-review.md +46 -0
  15. package/claude-toolkit/commands/bfsi-scaffold.md +47 -0
  16. package/claude-toolkit/hooks/hooks.json +181 -0
  17. package/claude-toolkit/hooks/scripts/a11y-check.sh +63 -0
  18. package/claude-toolkit/hooks/scripts/audit-prompt.sh +36 -0
  19. package/claude-toolkit/hooks/scripts/block-destructive.sh +41 -0
  20. package/claude-toolkit/hooks/scripts/block-force-push.sh +30 -0
  21. package/claude-toolkit/hooks/scripts/format.sh +42 -0
  22. package/claude-toolkit/hooks/scripts/inject-context.sh +44 -0
  23. package/claude-toolkit/hooks/scripts/lint.sh +45 -0
  24. package/claude-toolkit/hooks/scripts/protect-files.sh +53 -0
  25. package/claude-toolkit/hooks/scripts/save-compliance-context.sh +35 -0
  26. package/claude-toolkit/hooks/scripts/scan-pii.sh +87 -0
  27. package/claude-toolkit/hooks/scripts/scan-secrets.sh +67 -0
  28. package/claude-toolkit/hooks/scripts/verify-clean.sh +50 -0
  29. package/claude-toolkit/package.json +22 -0
  30. package/claude-toolkit/plugin.json +31 -0
  31. package/claude-toolkit/skills/bfsi-api-endpoint/SKILL.md +105 -0
  32. package/claude-toolkit/skills/bfsi-commit/SKILL.md +102 -0
  33. package/claude-toolkit/skills/bfsi-compliance-check/SKILL.md +107 -0
  34. package/claude-toolkit/skills/bfsi-encrypt-helper/SKILL.md +127 -0
  35. package/claude-toolkit/skills/bfsi-error-message/SKILL.md +162 -0
  36. package/claude-toolkit/skills/bfsi-feature/SKILL.md +120 -0
  37. package/claude-toolkit/skills/bfsi-feature/references/architecture.md +69 -0
  38. package/claude-toolkit/skills/bfsi-feature/references/audit-events.md +70 -0
  39. package/claude-toolkit/skills/bfsi-feature/scripts/scaffold.mjs +136 -0
  40. package/claude-toolkit/skills/bfsi-form/SKILL.md +73 -0
  41. package/claude-toolkit/skills/bfsi-form/references/validation-regex.md +50 -0
  42. package/claude-toolkit/skills/bfsi-onboarding/SKILL.md +110 -0
  43. package/claude-toolkit/skills/bfsi-pii-field/SKILL.md +90 -0
  44. package/claude-toolkit/skills/bfsi-test-pattern/SKILL.md +179 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +339 -0
  48. package/dist/index.js.map +1 -0
  49. package/package.json +69 -0
  50. package/templates/_shared/.claude/settings.json +31 -0
  51. package/templates/_shared/.env.local.sample +25 -0
  52. package/templates/_shared/.github/workflows/ci.yml +49 -0
  53. package/templates/_shared/CLAUDE.md +89 -0
  54. package/templates/_shared/README.md +50 -0
  55. package/templates/_shared/index.html +16 -0
  56. package/templates/_shared/package.json +73 -0
  57. package/templates/_shared/postcss.config.cjs +6 -0
  58. package/templates/_shared/src/app/App.tsx +13 -0
  59. package/templates/_shared/src/app/globals.css +64 -0
  60. package/templates/_shared/src/env.ts +33 -0
  61. package/templates/_shared/src/i18n/i18n.ts +18 -0
  62. package/templates/_shared/src/i18n/translations/en.json +54 -0
  63. package/templates/_shared/src/i18n/translations/hi.json +30 -0
  64. package/templates/_shared/src/main.tsx +16 -0
  65. package/templates/_shared/src/routes/ProtectedRoute.tsx +28 -0
  66. package/templates/_shared/src/routes/index.tsx +67 -0
  67. package/templates/_shared/src/shared/ErrorBoundary.tsx +60 -0
  68. package/templates/_shared/tailwind.config.ts +68 -0
  69. package/templates/_shared/tests/setup.ts +7 -0
  70. package/templates/_shared/tsconfig.json +33 -0
  71. package/templates/_shared/tsconfig.node.json +13 -0
  72. package/templates/_shared/vite.config.ts +47 -0
  73. package/templates/rtk-query/.claude/skills/axios-auth/SKILL.md +103 -0
  74. package/templates/rtk-query/.claude/skills/axios-auth/references/error-shape.md +84 -0
  75. package/templates/rtk-query/.claude/skills/axios-auth/references/full-code-walkthrough.md +146 -0
  76. package/templates/rtk-query/.claude/skills/axios-auth/references/notification-wiring.md +141 -0
  77. package/templates/rtk-query/.claude/skills/constants-organization/SKILL.md +112 -0
  78. package/templates/rtk-query/.claude/skills/constants-organization/references/example-files.md +134 -0
  79. package/templates/rtk-query/.claude/skills/constants-organization/references/tag-types-catalog.md +53 -0
  80. package/templates/rtk-query/.claude/skills/redux-store-integration/SKILL.md +159 -0
  81. package/templates/rtk-query/.claude/skills/redux-store-integration/references/localStorage-persistence.md +70 -0
  82. package/templates/rtk-query/.claude/skills/redux-store-integration/references/middleware-patterns.md +82 -0
  83. package/templates/rtk-query/.claude/skills/rtk-query-api/SKILL.md +148 -0
  84. package/templates/rtk-query/.claude/skills/rtk-query-api/references/cache-strategies.md +96 -0
  85. package/templates/rtk-query/.claude/skills/rtk-query-api/references/endpoint-cookbook.md +145 -0
  86. package/templates/rtk-query/.claude/skills/rtk-query-api/references/optimistic-update.md +53 -0
  87. package/templates/rtk-query/README.md +84 -0
  88. package/templates/rtk-query/package.partial.json +7 -0
  89. package/templates/rtk-query/src/app/App.tsx +23 -0
  90. package/templates/rtk-query/src/axiosconfig/axiosInstance.ts +26 -0
  91. package/templates/rtk-query/src/axiosconfig/baseQuery.ts +72 -0
  92. package/templates/rtk-query/src/axiosconfig/interceptor.ts +42 -0
  93. package/templates/rtk-query/src/redux/invalidateCacheMiddleware.ts +20 -0
  94. package/templates/rtk-query/src/redux/reduxHooks.ts +10 -0
  95. package/templates/rtk-query/src/redux/rootReducer.ts +18 -0
  96. package/templates/rtk-query/src/redux/store.ts +36 -0
  97. package/templates/tanstack-query/.claude/skills/axios-auth/SKILL.md +109 -0
  98. package/templates/tanstack-query/.claude/skills/axios-auth/references/error-shape.md +89 -0
  99. package/templates/tanstack-query/.claude/skills/axios-auth/references/full-code-walkthrough.md +121 -0
  100. package/templates/tanstack-query/.claude/skills/axios-auth/references/notification-pattern.md +109 -0
  101. package/templates/tanstack-query/.claude/skills/constants-organization/SKILL.md +144 -0
  102. package/templates/tanstack-query/.claude/skills/constants-organization/references/example-files.md +111 -0
  103. package/templates/tanstack-query/.claude/skills/constants-organization/references/query-key-factories.md +129 -0
  104. package/templates/tanstack-query/.claude/skills/query-client-setup/SKILL.md +165 -0
  105. package/templates/tanstack-query/.claude/skills/query-client-setup/references/devtools.md +67 -0
  106. package/templates/tanstack-query/.claude/skills/query-client-setup/references/global-handlers.md +94 -0
  107. package/templates/tanstack-query/.claude/skills/tanstack-services/SKILL.md +142 -0
  108. package/templates/tanstack-query/.claude/skills/tanstack-services/references/audited-mutation.md +144 -0
  109. package/templates/tanstack-query/.claude/skills/tanstack-services/references/optimistic-update.md +102 -0
  110. package/templates/tanstack-query/.claude/skills/tanstack-services/references/service-cookbook.md +151 -0
  111. package/templates/tanstack-query/README.md +63 -0
  112. package/templates/tanstack-query/package.partial.json +8 -0
  113. package/templates/tanstack-query/src/api/axiosInstance.ts +20 -0
  114. package/templates/tanstack-query/src/api/http.ts +62 -0
  115. package/templates/tanstack-query/src/api/queryClient.ts +28 -0
  116. package/templates/tanstack-query/src/app/App.tsx +20 -0
  117. package/templates/tanstack-query/src/services/example.ts +32 -0
@@ -0,0 +1,129 @@
1
+ # queryKey factory pattern
2
+
3
+ QueryKeys are TanStack Query's primary cache identifier. They MUST be:
4
+
5
+ - Stable: same logical query → same key shape
6
+ - Discoverable: cache invalidation should be easy to write
7
+ - Type-safe: keys derived from a factory, not hand-typed
8
+
9
+ ## Anti-pattern: inline arrays
10
+
11
+ ```tsx
12
+ // ❌ Don't do this:
13
+ useQuery({ queryKey: ['kyc', id], queryFn: () => getKyc(id) });
14
+
15
+ // ❌ Or this:
16
+ queryClient.invalidateQueries({ queryKey: ['kyc'] });
17
+ ```
18
+
19
+ Problems:
20
+
21
+ - Renaming the feature requires grep across the whole codebase
22
+ - Easy to typo the queryKey, causing silent cache misses
23
+ - Filter shapes embedded in keys aren't reused
24
+
25
+ ## Pattern: factory objects
26
+
27
+ ```ts
28
+ // src/utils/constants/queryKeys.ts
29
+ export const kycKeys = {
30
+ all: ['kyc'] as const,
31
+ lists: () => [...kycKeys.all, 'list'] as const,
32
+ list: (filters?: KycFilters) => [...kycKeys.lists(), filters] as const,
33
+ details: () => [...kycKeys.all, 'detail'] as const,
34
+ detail: (id: string) => [...kycKeys.details(), id] as const,
35
+ };
36
+ ```
37
+
38
+ Usage:
39
+
40
+ ```tsx
41
+ useQuery({
42
+ queryKey: kycKeys.detail(id),
43
+ queryFn: () => getKyc(id),
44
+ });
45
+ ```
46
+
47
+ ## Invalidation matrix
48
+
49
+ | Goal | Code |
50
+ | ------------------------------------- | ---------------------------------------------------------------------------------- |
51
+ | Invalidate everything for the feature | `queryClient.invalidateQueries({ queryKey: kycKeys.all })` |
52
+ | Invalidate all lists (any filter) | `queryClient.invalidateQueries({ queryKey: kycKeys.lists() })` |
53
+ | Invalidate one specific list | `queryClient.invalidateQueries({ queryKey: kycKeys.list({ status: 'pending' }) })` |
54
+ | Invalidate all details | `queryClient.invalidateQueries({ queryKey: kycKeys.details() })` |
55
+ | Invalidate one record | `queryClient.invalidateQueries({ queryKey: kycKeys.detail(id) })` |
56
+
57
+ TanStack Query matches by **prefix** — `kycKeys.all` invalidates everything that starts with `['kyc']`.
58
+
59
+ ## Pattern: after a mutation
60
+
61
+ ```tsx
62
+ const queryClient = useQueryClient();
63
+
64
+ const submit = useMutation({
65
+ mutationFn: submitKyc,
66
+ onSuccess: (newRecord) => {
67
+ // Drop the lists cache so the next list query refetches
68
+ queryClient.invalidateQueries({ queryKey: kycKeys.lists() });
69
+ // Optionally pre-fill the detail cache so navigating to it is instant
70
+ queryClient.setQueryData(kycKeys.detail(newRecord.id), newRecord);
71
+ },
72
+ });
73
+ ```
74
+
75
+ ## Pattern: cross-feature invalidation
76
+
77
+ ```tsx
78
+ const submit = useMutation({
79
+ mutationFn: submitKyc,
80
+ onSuccess: () => {
81
+ queryClient.invalidateQueries({ queryKey: kycKeys.all });
82
+ queryClient.invalidateQueries({ queryKey: userKeys.detail(userId) }); // KYC changes affect user profile
83
+ },
84
+ });
85
+ ```
86
+
87
+ For complex graphs, consider a small middleware-like wrapper:
88
+
89
+ ```ts
90
+ // src/services/invalidations.ts
91
+ export const invalidationsAfter = {
92
+ kycSubmit: (qc: QueryClient, userId: string) => {
93
+ qc.invalidateQueries({ queryKey: kycKeys.all });
94
+ qc.invalidateQueries({ queryKey: userKeys.detail(userId) });
95
+ },
96
+ // ... other cross-feature invalidation recipes
97
+ };
98
+
99
+ // Then in components:
100
+ const submit = useMutation({
101
+ mutationFn: submitKyc,
102
+ onSuccess: () => invalidationsAfter.kycSubmit(queryClient, currentUserId),
103
+ });
104
+ ```
105
+
106
+ ## Anti-pattern: keys in the wrong order
107
+
108
+ The order matters — TanStack Query treats the array as a prefix. Always put the most general element first:
109
+
110
+ ```ts
111
+ // ✅ Good
112
+ list: (filters) => ['kyc', 'list', filters] as const;
113
+
114
+ // ❌ Bad — can't invalidate "all kyc" without listing every filter
115
+ list: (filters) => [filters, 'kyc', 'list'] as const;
116
+ ```
117
+
118
+ ## Logout: nuke the cache
119
+
120
+ ```ts
121
+ import { kycKeys } from '@/utils/constants/queryKeys';
122
+
123
+ function logout() {
124
+ // ... clear auth
125
+ queryClient.clear(); // wipes EVERYTHING (preferred on logout)
126
+ // alternative — selective:
127
+ // queryClient.removeQueries({ queryKey: kycKeys.all });
128
+ }
129
+ ```
@@ -0,0 +1,165 @@
1
+ ---
2
+ name: query-client-setup
3
+ description: Configure the TanStack QueryClient — defaults, retry policy, refetch behaviour, optional global error handlers, devtools. Use when setting up the QueryClient for the first time, adjusting retry / staleTime / refetch behaviour, adding a global error handler, or wiring devtools.
4
+ ---
5
+
6
+ # QueryClient Setup
7
+
8
+ `src/api/queryClient.ts` exports a single `QueryClient` instance with BFSI-friendly defaults. It's mounted via `<QueryClientProvider>` in `src/app/App.tsx`.
9
+
10
+ ## File map
11
+
12
+ ```
13
+ src/api/
14
+ ├── axiosInstance.ts single axios for all services
15
+ ├── http.ts typed GET/POST/PUT/PATCH/DELETE helpers
16
+ └── queryClient.ts QueryClient with defaultOptions
17
+ src/app/
18
+ └── App.tsx wraps in <QueryClientProvider client={queryClient}>
19
+ ```
20
+
21
+ ## Default config (what the scaffolder ships)
22
+
23
+ ```ts
24
+ import { QueryClient } from '@tanstack/react-query';
25
+
26
+ export const queryClient = new QueryClient({
27
+ defaultOptions: {
28
+ queries: {
29
+ staleTime: 30_000, // 30s — minimise thrash, refetch when stale
30
+ gcTime: 5 * 60_000, // 5min — keep in cache after unsubscribed
31
+ retry: (failureCount, error) => {
32
+ const status = (error as { status?: number })?.status;
33
+ // Don't retry 4xx (except 408/429)
34
+ if (status && status >= 400 && status < 500 && status !== 408 && status !== 429) {
35
+ return false;
36
+ }
37
+ return failureCount < 2;
38
+ },
39
+ refetchOnWindowFocus: false, // BFSI: don't refetch on tab switch
40
+ },
41
+ mutations: {
42
+ retry: false, // Never auto-retry mutations
43
+ },
44
+ },
45
+ });
46
+ ```
47
+
48
+ ## Why these defaults
49
+
50
+ | Setting | Choice | Reason |
51
+ | ----------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------- |
52
+ | `staleTime: 30_000` | 30 seconds | Prevents thrash from rapid component remounts; long enough that two users on the same screen don't both hit the API |
53
+ | `gcTime: 5 * 60_000` | 5 minutes | Keeps caches around when user navigates back; cleared on logout via `queryClient.clear()` |
54
+ | `retry: 4xx → no` | 4xx → no, 5xx → 2 | 4xx is a contract issue — retrying won't help. 5xx might be transient. |
55
+ | `refetchOnWindowFocus: false` | off | BFSI users tab away and back; surprise refetches lose draft state |
56
+ | `mutations.retry: false` | never retry | Mutations have side effects. Use idempotency-key + explicit user retry. |
57
+
58
+ ## Workflow — adjusting defaults
59
+
60
+ Don't override these in component-level `useQuery` calls unless you have a specific reason. Override the default once in `queryClient.ts`:
61
+
62
+ ```ts
63
+ defaultOptions: {
64
+ queries: {
65
+ staleTime: 60_000, // change to 60s
66
+ // ...
67
+ },
68
+ },
69
+ ```
70
+
71
+ For a per-feature override:
72
+
73
+ ```tsx
74
+ const { data } = useQuery({
75
+ queryKey: kycKeys.list(filters),
76
+ queryFn: () => getKycList(filters),
77
+ staleTime: 0, // always refetch on mount (audit-critical list)
78
+ });
79
+ ```
80
+
81
+ ## Wiring into the app
82
+
83
+ `src/app/App.tsx` (variant overlay):
84
+
85
+ ```tsx
86
+ import { QueryClientProvider } from '@tanstack/react-query';
87
+ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
88
+ import { BrowserRouter } from 'react-router-dom';
89
+ import { ErrorBoundary } from '../shared/ErrorBoundary';
90
+ import { AppRoutes } from '../routes';
91
+ import { queryClient } from '../api/queryClient';
92
+
93
+ export function App(): JSX.Element {
94
+ return (
95
+ <ErrorBoundary>
96
+ <QueryClientProvider client={queryClient}>
97
+ <BrowserRouter>
98
+ <AppRoutes />
99
+ </BrowserRouter>
100
+ {import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
101
+ </QueryClientProvider>
102
+ </ErrorBoundary>
103
+ );
104
+ }
105
+ ```
106
+
107
+ Devtools render only in dev (gated by `import.meta.env.DEV`).
108
+
109
+ ## Global error handlers (optional)
110
+
111
+ For a "catch all unhandled mutation errors" toast, use a `MutationCache`:
112
+
113
+ ```ts
114
+ import { QueryClient, MutationCache } from '@tanstack/react-query';
115
+ import { toast } from 'sonner';
116
+ import { toSafeView } from '@<scope>/core/compliance';
117
+ import type { ApiError } from '@<scope>/core/http';
118
+ import i18n from '@/i18n/i18n';
119
+
120
+ export const queryClient = new QueryClient({
121
+ mutationCache: new MutationCache({
122
+ onError: (error, _vars, _ctx, mutation) => {
123
+ if (mutation.options.onError) return; // skip if per-mutation handler set
124
+ const view = toSafeView(error as ApiError, i18n.t);
125
+ toast.error(view.title, { description: view.description });
126
+ },
127
+ }),
128
+ defaultOptions: {
129
+ /* ... */
130
+ },
131
+ });
132
+ ```
133
+
134
+ See [`references/global-handlers.md`](references/global-handlers.md) for QueryCache + MutationCache patterns.
135
+
136
+ ## Conventions enforced
137
+
138
+ - ❌ NEVER create multiple `QueryClient` instances — there must be exactly one for the app.
139
+ - ❌ NEVER set `refetchOnWindowFocus: true` globally (BFSI default is off).
140
+ - ❌ NEVER enable `mutations.retry > 0` globally — mutations have side effects.
141
+ - ❌ NEVER render `<ReactQueryDevtools>` in production builds — gate on `import.meta.env.DEV`.
142
+ - ✅ `<QueryClientProvider>` wraps the WHOLE app (above the router, below ErrorBoundary).
143
+ - ✅ Per-query overrides go in the `useQuery` call, not the QueryClient config.
144
+ - ✅ On logout: `queryClient.clear()` after `clearAuthToken()`.
145
+
146
+ ## Logout sequence
147
+
148
+ ```ts
149
+ import { clearAuthToken } from '@<scope>/core/http';
150
+ import { queryClient } from '@/api/queryClient';
151
+ import axiosInstance from '@/api/axiosInstance';
152
+
153
+ export function logout() {
154
+ clearAuthToken(axiosInstance); // drop the auth header
155
+ queryClient.clear(); // wipe all cached server data
156
+ navigate('/login');
157
+ }
158
+ ```
159
+
160
+ `queryClient.clear()` removes all cached queries + in-flight fetches. Without it, a re-login on the same browser sees stale data for a flash before refetch.
161
+
162
+ ## References
163
+
164
+ - [`references/global-handlers.md`](references/global-handlers.md) — QueryCache.onError + MutationCache.onError patterns
165
+ - [`references/devtools.md`](references/devtools.md) — using ReactQueryDevtools effectively
@@ -0,0 +1,67 @@
1
+ # Using ReactQueryDevtools effectively
2
+
3
+ The devtools panel is the single most useful tool for debugging TanStack Query behaviour. Render it conditionally so it never reaches production bundles.
4
+
5
+ ## Setup
6
+
7
+ ```tsx
8
+ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
9
+
10
+ <QueryClientProvider client={queryClient}>
11
+ {/* app */}
12
+ {import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
13
+ </QueryClientProvider>;
14
+ ```
15
+
16
+ `@tanstack/react-query-devtools` is in `devDependencies` and Vite tree-shakes it out of the production bundle as long as the import is gated behind `import.meta.env.DEV`. Confirm with `pnpm build && grep -r 'ReactQueryDevtools' dist/`.
17
+
18
+ ## Debugging workflow
19
+
20
+ When something feels wrong with caching/refetching:
21
+
22
+ 1. **Open devtools** (icon in bottom-right corner).
23
+ 2. **Find the queryKey** — search the list for your feature (e.g. `kyc`).
24
+ 3. **Inspect the state**:
25
+ - `fetchStatus: idle/fetching/paused` — is it actually fetching?
26
+ - `status: pending/success/error` — what's the last result?
27
+ - `data` — the parsed response (or `undefined`)
28
+ - `error` — the ApiError instance
29
+ 4. **Check observers** — which components are subscribed? If 0, the query won't refetch on stale.
30
+ 5. **Manual invalidate** — click "Invalidate" to force-refetch and see if the issue is invalidation logic vs the network call.
31
+ 6. **Time travel** — switch query state to "Fresh" or "Stale" to test edge cases.
32
+
33
+ ## Common issues + how the devtools reveal them
34
+
35
+ | Symptom | What devtools shows | Fix |
36
+ | ------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------- |
37
+ | List doesn't refetch after a mutation | List query is "Fresh" not "Stale" after the mutation | Missing `invalidateQueries` in mutation's `onSuccess` |
38
+ | Query won't fire | `fetchStatus: paused`, `enabled: false` | Some upstream variable is undefined; check the `enabled:` flag |
39
+ | Two components show different data | They have different queryKey shapes | Normalise queryKey via a factory |
40
+ | Cache size grows forever | Many entries with `observers: 0` | `gcTime` too long; lower it |
41
+ | Mutation seems to retry | Multiple "pending" entries | Check `mutations.retry` is `false` |
42
+
43
+ ## Don't ship devtools to prod
44
+
45
+ Risk: devtools expose query payloads, including PII. Even though they only render `import.meta.env.DEV`, double-check:
46
+
47
+ ```bash
48
+ pnpm build
49
+ grep -r 'ReactQueryDevtools' dist/ # should be empty
50
+ grep -r 'TanStack Query' dist/ # should be empty
51
+ ```
52
+
53
+ If you see hits, the gating is wrong — ReactQueryDevtools is in the production bundle.
54
+
55
+ ## Alternative for staging
56
+
57
+ If you need devtools-like inspection in staging (not full prod), add a feature flag:
58
+
59
+ ```tsx
60
+ {
61
+ (import.meta.env.DEV || env.VITE_ENABLE_QUERY_DEVTOOLS === 'true') && (
62
+ <ReactQueryDevtools initialIsOpen={false} />
63
+ );
64
+ }
65
+ ```
66
+
67
+ NEVER set `VITE_ENABLE_QUERY_DEVTOOLS=true` in production — it ships the devtools.
@@ -0,0 +1,94 @@
1
+ # Global QueryCache / MutationCache handlers
2
+
3
+ For cross-cutting concerns (every-error toast, every-success audit), use the cache-level callbacks instead of repeating `onError` in every component.
4
+
5
+ ## When to use cache-level vs per-call
6
+
7
+ | Concern | Where | Why |
8
+ | -------------------------------- | ----------------------------------------- | ----------------------------------------------------------------------- |
9
+ | Default error toast | `MutationCache.onError` | DRY — every component without its own onError gets it |
10
+ | Per-mutation audit event | `useAuditedMutation` wrapper | Audit needs event name, target, etc. — too granular for cache-level |
11
+ | Global "session expired" handler | `axios.onUnauthorized` (in `createAxios`) | 401 is handled before the error reaches React; no cache callback needed |
12
+ | Logging all query failures | `QueryCache.onError` | Observability — fire-and-forget telemetry |
13
+ | Per-query field error mapping | Per-mutation `onError` | Field errors are component-specific (which RHF form to set them on) |
14
+
15
+ ## MutationCache pattern
16
+
17
+ ```ts
18
+ import { QueryClient, MutationCache } from '@tanstack/react-query';
19
+ import { toast } from 'sonner';
20
+ import { toSafeView } from '@<scope>/core/compliance';
21
+ import type { ApiError } from '@<scope>/core/http';
22
+ import i18n from '@/i18n/i18n';
23
+
24
+ export const queryClient = new QueryClient({
25
+ mutationCache: new MutationCache({
26
+ onError: (error, _vars, _ctx, mutation) => {
27
+ // Skip if mutation defined its own onError
28
+ if (mutation.options.onError) return;
29
+
30
+ const apiErr = error as ApiError;
31
+ // Skip 401 (handled by axios.onUnauthorized)
32
+ if (apiErr.kind === 'unauthorized') return;
33
+
34
+ const view = toSafeView(apiErr, i18n.t);
35
+ toast.error(view.title, { description: view.description });
36
+ },
37
+ onSuccess: (_data, _vars, _ctx, mutation) => {
38
+ const successMessage = (mutation.options as { successMessage?: string }).successMessage;
39
+ if (successMessage) toast.success(successMessage);
40
+ },
41
+ }),
42
+ // ... defaultOptions
43
+ });
44
+ ```
45
+
46
+ Note: `mutation.options.onError` being set means the component has opted-in to handle the error itself. Skip the global toast to avoid double-toasting.
47
+
48
+ ## QueryCache pattern (rarer)
49
+
50
+ ```ts
51
+ import { QueryCache } from '@tanstack/react-query';
52
+ import * as Sentry from '@sentry/react';
53
+
54
+ queryCache: new QueryCache({
55
+ onError: (error, query) => {
56
+ // Don't toast on query errors (they may fire in background)
57
+ // Just log to telemetry
58
+ Sentry.captureException(error, {
59
+ tags: { queryKey: JSON.stringify(query.queryKey) },
60
+ });
61
+ },
62
+ }),
63
+ ```
64
+
65
+ Don't surface query errors to UI via global toast — they fire in background and may be for unmounted components. Let the component handle its own error state via `{ error, isError } = useQuery(...)`.
66
+
67
+ ## Anti-patterns
68
+
69
+ ❌ `MutationCache.onError` that always fires even when the component has its own:
70
+
71
+ ```ts
72
+ // WRONG — double-toasts
73
+ onError: (error) => {
74
+ toast.error(safeMessage(error)); // fires even if component handled it
75
+ },
76
+ ```
77
+
78
+ ❌ `QueryCache.onError` that toasts:
79
+
80
+ ```ts
81
+ // WRONG — toasts for background refetches the user didn't trigger
82
+ onError: (error) => toast.error(safeMessage(error)),
83
+ ```
84
+
85
+ ❌ Per-mutation `onError` that re-throws (TanStack catches and routes via cache):
86
+
87
+ ```ts
88
+ // WRONG — onError shouldn't throw
89
+ useMutation({
90
+ onError: (err) => {
91
+ throw err;
92
+ }, // breaks rollback chain
93
+ });
94
+ ```
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: tanstack-services
3
+ description: Create the service layer for a feature — typed async functions that wrap the HTTP helpers (GET/POST/PUT/PATCH/DELETE) and return parsed responses. Services are hook-free and consumed by useQuery / useMutation in components. Use when adding a new feature's services, adding a new service to an existing feature, or wiring API endpoints.
4
+ ---
5
+
6
+ # TanStack Services
7
+
8
+ Every feature has a service file at `src/services/<feature>.ts` (or `src/features/<Feature>/service.ts`). Services are plain async functions: typed inputs in, typed parsed output out. No hooks. No React.
9
+
10
+ ## File map (typical feature)
11
+
12
+ ```
13
+ src/features/<Feature>/
14
+ ├── service.ts typed async functions
15
+ ├── schema.ts Zod schemas (request + response)
16
+ ├── types.ts inferred types from Zod
17
+ ├── containers/... React components that call services via useQuery/useMutation
18
+ ```
19
+
20
+ ## Workflow — new feature service
21
+
22
+ ### Step 1 — Define schemas
23
+
24
+ `src/features/Kyc/schema.ts`:
25
+
26
+ ```ts
27
+ import { z } from 'zod';
28
+ import { PAN_REGEX, AADHAAR_REGEX } from '@/utils/constants/regexConstants';
29
+
30
+ export const kycRecordSchema = z.object({
31
+ id: z.string(),
32
+ pan: z.string().regex(PAN_REGEX),
33
+ aadhaar: z.string().regex(AADHAAR_REGEX),
34
+ status: z.enum(['pending', 'approved', 'rejected']),
35
+ createdAt: z.coerce.date(),
36
+ });
37
+
38
+ export const kycListResponseSchema = z.object({
39
+ items: z.array(kycRecordSchema),
40
+ total: z.number().int().nonnegative(),
41
+ });
42
+
43
+ export const kycSubmitRequestSchema = z.object({
44
+ pan: z.string().regex(PAN_REGEX),
45
+ aadhaar: z.string().regex(AADHAAR_REGEX),
46
+ });
47
+ ```
48
+
49
+ Types are inferred — never hand-write:
50
+
51
+ ```ts
52
+ // types.ts
53
+ import { z } from 'zod';
54
+ import type { kycRecordSchema, kycListResponseSchema, kycSubmitRequestSchema } from './schema';
55
+
56
+ export type IKycRecord = z.infer<typeof kycRecordSchema>;
57
+ export type IKycListResponse = z.infer<typeof kycListResponseSchema>;
58
+ export type IKycSubmitRequest = z.infer<typeof kycSubmitRequestSchema>;
59
+ ```
60
+
61
+ ### Step 2 — Write services
62
+
63
+ `src/features/Kyc/service.ts`:
64
+
65
+ ```ts
66
+ import { GET, POST } from '@/api/http';
67
+ import { KYC_URLS } from '@/utils/constants/urlConstants';
68
+ import { kycRecordSchema, kycListResponseSchema } from './schema';
69
+ import type { IKycRecord, IKycListResponse, IKycSubmitRequest } from './types';
70
+
71
+ export const getKycList = async (): Promise<IKycListResponse> => {
72
+ const raw = await GET<unknown>(KYC_URLS.LIST);
73
+ return kycListResponseSchema.parse(raw);
74
+ };
75
+
76
+ export const getKycDetail = async (id: string): Promise<IKycRecord> => {
77
+ const raw = await GET<unknown>(KYC_URLS.DETAIL(id));
78
+ return kycRecordSchema.parse(raw);
79
+ };
80
+
81
+ export const submitKyc = async (payload: IKycSubmitRequest): Promise<IKycRecord> => {
82
+ const raw = await POST<unknown, IKycSubmitRequest>(KYC_URLS.SUBMIT, payload);
83
+ return kycRecordSchema.parse(raw);
84
+ };
85
+ ```
86
+
87
+ ### Step 3 — Use in components
88
+
89
+ ```tsx
90
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
91
+ import { kycKeys } from '@/utils/constants/queryKeys';
92
+ import { getKycList, getKycDetail, submitKyc } from './service';
93
+
94
+ function KycList() {
95
+ const { data, isLoading, error } = useQuery({
96
+ queryKey: kycKeys.lists(),
97
+ queryFn: getKycList,
98
+ });
99
+ // ...
100
+ }
101
+
102
+ function KycForm() {
103
+ const queryClient = useQueryClient();
104
+ const submit = useMutation({
105
+ mutationFn: submitKyc,
106
+ onSuccess: () => {
107
+ queryClient.invalidateQueries({ queryKey: kycKeys.all });
108
+ },
109
+ });
110
+ // ...
111
+ }
112
+ ```
113
+
114
+ ## Conventions enforced
115
+
116
+ - ❌ NEVER use `axios.<method>` directly — use the typed `GET<TRes, TParams>` / `POST<TRes, TReq>` helpers from `@/api/http`.
117
+ - ❌ NEVER skip the `.parse()` step — every response goes through Zod validation at runtime.
118
+ - ❌ NEVER inline a URL string — use `urlConstants.ts`.
119
+ - ❌ NEVER call services without typing the response — the generic on `GET<IResponse>` is required.
120
+ - ❌ NEVER put React hooks in service files — services are pure async functions.
121
+ - ✅ One service file per feature; export named async functions.
122
+ - ✅ Request types prefixed `I` (`IKycSubmitRequest`) to match service interface convention.
123
+ - ✅ Responses parsed by Zod schemas from the same feature's `schema.ts`.
124
+ - ✅ Use `getX` / `submitX` / `updateX` / `deleteX` naming for service functions.
125
+
126
+ ## Service vs hook
127
+
128
+ A service is a plain function: `getKyc(id): Promise<IKycRecord>`.
129
+
130
+ A hook wraps it with TanStack Query: `useQuery({ queryKey: kycKeys.detail(id), queryFn: () => getKyc(id) })`.
131
+
132
+ Don't pre-bundle the hook into the service. Keep services hook-free so:
133
+
134
+ - They're trivial to unit test (call them directly with mocked axios)
135
+ - The component decides cache behaviour (staleTime, refetch, enabled, etc.)
136
+ - The same service can power useQuery, useMutation, OR a direct call outside a component (rare but useful — e.g. in a logout flow)
137
+
138
+ ## References
139
+
140
+ - [`references/service-cookbook.md`](references/service-cookbook.md) — list, detail, paginated, file-upload, file-download, polling examples
141
+ - [`references/optimistic-update.md`](references/optimistic-update.md) — optimistic mutations with cache patches
142
+ - [`references/audited-mutation.md`](references/audited-mutation.md) — wrapping useMutation to fire audit events