@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,144 @@
1
+ # Audited mutation wrapper
2
+
3
+ Every state-changing API call in a BFSI app should emit an audit event (RBI Annexure I §8, SOC2 CC7.3). Without an `axiosBaseQuery`-style abstraction, we wrap `useMutation` with a custom hook.
4
+
5
+ ## The wrapper
6
+
7
+ ```ts
8
+ // src/services/useAuditedMutation.ts
9
+ import {
10
+ useMutation,
11
+ type UseMutationOptions,
12
+ type UseMutationResult,
13
+ } from '@tanstack/react-query';
14
+ import type { ApiError } from '@<scope>/core/http';
15
+ import { AuditClient, generateEventId } from '@<scope>/core/audit';
16
+ import { env } from '@/env';
17
+
18
+ const auditClient = new AuditClient({
19
+ endpoint: env.VITE_AUDIT_ENDPOINT,
20
+ batchSize: env.VITE_AUDIT_BATCH_SIZE,
21
+ flushIntervalMs: env.VITE_AUDIT_FLUSH_INTERVAL_MS,
22
+ });
23
+
24
+ export interface UseAuditedMutationOptions<TData, TVars>
25
+ extends UseMutationOptions<TData, ApiError, TVars> {
26
+ /** Audit event name — convention: <feature>.<entity>.<action> */
27
+ eventName: string;
28
+ /** Optional toast on success */
29
+ successMessage?: string;
30
+ /** Function to extract audit target from variables */
31
+ getTarget?: (vars: TVars) => { type: string; id?: string };
32
+ }
33
+
34
+ export function useAuditedMutation<TData, TVars>(
35
+ options: UseAuditedMutationOptions<TData, TVars>,
36
+ ): UseMutationResult<TData, ApiError, TVars> {
37
+ return useMutation({
38
+ ...options,
39
+ onMutate: async (vars) => {
40
+ // Record audit event (pending)
41
+ const eventId = generateEventId();
42
+ const target = options.getTarget?.(vars);
43
+ auditClient.record({
44
+ event_id: eventId,
45
+ event_name: options.eventName,
46
+ actor_id: getCurrentUserId(), // your auth context
47
+ target_type: target?.type,
48
+ target_id: target?.id,
49
+ timestamp: new Date().toISOString(),
50
+ outcome: 'pending',
51
+ });
52
+ // Pass eventId through context for outcome update on success/error
53
+ const inherited = await options.onMutate?.(vars);
54
+ return { eventId, inherited } as never;
55
+ },
56
+ onSuccess: (data, vars, ctx) => {
57
+ const eventId = (ctx as { eventId: string } | undefined)?.eventId;
58
+ if (eventId) {
59
+ auditClient.record({
60
+ event_id: eventId,
61
+ event_name: options.eventName,
62
+ actor_id: getCurrentUserId(),
63
+ timestamp: new Date().toISOString(),
64
+ outcome: 'success',
65
+ });
66
+ }
67
+ options.onSuccess?.(data, vars, ctx);
68
+ },
69
+ onError: (err, vars, ctx) => {
70
+ const eventId = (ctx as { eventId: string } | undefined)?.eventId;
71
+ if (eventId) {
72
+ auditClient.record({
73
+ event_id: eventId,
74
+ event_name: options.eventName,
75
+ actor_id: getCurrentUserId(),
76
+ timestamp: new Date().toISOString(),
77
+ outcome: 'failure',
78
+ });
79
+ }
80
+ options.onError?.(err, vars, ctx);
81
+ },
82
+ });
83
+ }
84
+
85
+ function getCurrentUserId(): string {
86
+ // Wire to your auth context
87
+ return 'TODO';
88
+ }
89
+ ```
90
+
91
+ ## Usage
92
+
93
+ ```tsx
94
+ const submit = useAuditedMutation({
95
+ mutationFn: submitKyc,
96
+ eventName: 'kyc.verification.submitted',
97
+ successMessage: 'KYC submitted successfully',
98
+ getTarget: (payload) => ({ type: 'kyc_record', id: undefined }), // id assigned on success
99
+ onSuccess: (record) => {
100
+ queryClient.invalidateQueries({ queryKey: kycKeys.all });
101
+ navigate(`/kyc/${record.id}`);
102
+ },
103
+ });
104
+ ```
105
+
106
+ ## Audit event naming
107
+
108
+ `<feature>.<entity>.<action>` — see `bfsi-feature` skill in the toolkit for full naming convention. Examples:
109
+
110
+ - `kyc.verification.submitted`
111
+ - `kyc.verification.approved`
112
+ - `transaction.transfer.initiated`
113
+ - `user.session.logged_in`
114
+ - `data.pan.revealed` (when PII is unmasked)
115
+
116
+ ## Why this wraps useMutation
117
+
118
+ In the RTK variant, audit events flow through the `axiosBaseQuery` because that's the single chokepoint every API call goes through. In TanStack, there's no equivalent — each service call is direct. The wrapper hook brings back the chokepoint for state-changing operations.
119
+
120
+ You CAN skip the wrapper for read-only queries (no audit needed for reads, by default). Reads via `useQuery` don't get the wrapper.
121
+
122
+ ## Reveal events (separate from mutations)
123
+
124
+ When a user clicks "reveal PAN" in `<PIIMaskedDisplay>`, you fire an audit event directly:
125
+
126
+ ```tsx
127
+ <PIIMaskedDisplay
128
+ type="pan"
129
+ value={user.pan}
130
+ onReveal={() =>
131
+ auditClient.record({
132
+ event_id: generateEventId(),
133
+ event_name: 'data.pan.revealed',
134
+ actor_id: getCurrentUserId(),
135
+ target_type: 'user',
136
+ target_id: user.id,
137
+ timestamp: new Date().toISOString(),
138
+ outcome: 'success',
139
+ })
140
+ }
141
+ />
142
+ ```
143
+
144
+ No mutation involved — just a direct audit call.
@@ -0,0 +1,102 @@
1
+ # Optimistic updates (TanStack)
2
+
3
+ Use sparingly. Optimistic updates risk showing stale state to the user; only use when success rate is very high AND latency is meaningful to the user.
4
+
5
+ ## When to use
6
+
7
+ ✅ Good fit:
8
+
9
+ - Toggling a boolean (favourited, archived, read/unread)
10
+ - Reordering a list (drag-and-drop)
11
+ - Inline editing of a single field
12
+
13
+ ❌ Bad fit:
14
+
15
+ - Financial transactions (NEVER show success before backend confirms)
16
+ - KYC submissions (regulatory + may be rejected by ML)
17
+ - Anything where failure is plausible
18
+
19
+ ## Pattern — `onMutate` + rollback
20
+
21
+ ```tsx
22
+ const queryClient = useQueryClient();
23
+
24
+ const toggleFavorite = useMutation({
25
+ mutationFn: async (args: { id: string; isFavorite: boolean }) => {
26
+ return PATCH<IKycRecord, { isFavorite: boolean }>(KYC_URLS.FAVORITE(args.id), {
27
+ isFavorite: args.isFavorite,
28
+ });
29
+ },
30
+ onMutate: async ({ id, isFavorite }) => {
31
+ // Cancel any outgoing refetches so they don't overwrite our optimistic update
32
+ await queryClient.cancelQueries({ queryKey: kycKeys.detail(id) });
33
+
34
+ // Snapshot the previous value
35
+ const previous = queryClient.getQueryData<IKycRecord>(kycKeys.detail(id));
36
+
37
+ // Optimistic update
38
+ if (previous) {
39
+ queryClient.setQueryData<IKycRecord>(kycKeys.detail(id), {
40
+ ...previous,
41
+ isFavorite,
42
+ });
43
+ }
44
+
45
+ // Return rollback context
46
+ return { previous };
47
+ },
48
+ onError: (_err, { id }, context) => {
49
+ if (context?.previous) {
50
+ queryClient.setQueryData(kycKeys.detail(id), context.previous);
51
+ }
52
+ },
53
+ onSettled: (_data, _err, { id }) => {
54
+ // Always refetch to reconcile with server truth
55
+ queryClient.invalidateQueries({ queryKey: kycKeys.detail(id) });
56
+ },
57
+ });
58
+ ```
59
+
60
+ ## Rules
61
+
62
+ 1. ALWAYS `cancelQueries` first — otherwise an in-flight fetch overwrites the optimistic update on settle.
63
+ 2. ALWAYS snapshot the previous value before patching — needed for rollback.
64
+ 3. ALWAYS rollback in `onError` using the context returned by `onMutate`.
65
+ 4. ALWAYS reconcile in `onSettled` (invalidate or refetch).
66
+ 5. NEVER toast "success" optimistically — wait for `onSuccess`.
67
+ 6. NEVER optimistically create new entries that need a server-assigned ID — wait for `onSuccess` to know the real ID.
68
+
69
+ ## Optimistic across multiple queries
70
+
71
+ If a mutation should optimistically update both detail AND a list:
72
+
73
+ ```ts
74
+ onMutate: async ({ id, isFavorite }) => {
75
+ await queryClient.cancelQueries({ queryKey: kycKeys.detail(id) });
76
+ await queryClient.cancelQueries({ queryKey: kycKeys.lists() });
77
+
78
+ const previousDetail = queryClient.getQueryData<IKycRecord>(kycKeys.detail(id));
79
+ const previousLists = queryClient.getQueriesData<IKycListResponse>({ queryKey: kycKeys.lists() });
80
+
81
+ // Patch detail
82
+ if (previousDetail) {
83
+ queryClient.setQueryData<IKycRecord>(kycKeys.detail(id), {
84
+ ...previousDetail,
85
+ isFavorite,
86
+ });
87
+ }
88
+
89
+ // Patch every cached list page
90
+ for (const [key, list] of previousLists) {
91
+ if (!list) continue;
92
+ queryClient.setQueryData<IKycListResponse>(key, {
93
+ ...list,
94
+ items: list.items.map((item) =>
95
+ item.id === id ? { ...item, isFavorite } : item,
96
+ ),
97
+ });
98
+ }
99
+
100
+ return { previousDetail, previousLists };
101
+ },
102
+ ```
@@ -0,0 +1,151 @@
1
+ # Service cookbook (TanStack)
2
+
3
+ Templates for common shapes. Copy + adapt.
4
+
5
+ ## List
6
+
7
+ ```ts
8
+ export const getKycList = async (): Promise<IKycListResponse> => {
9
+ const raw = await GET<unknown>(KYC_URLS.LIST);
10
+ return kycListResponseSchema.parse(raw);
11
+ };
12
+ ```
13
+
14
+ ## Paginated list
15
+
16
+ ```ts
17
+ export const getKycList = async (params: {
18
+ page: number;
19
+ pageSize: number;
20
+ status?: KycStatus;
21
+ }): Promise<IKycListResponse> => {
22
+ const raw = await GET<unknown, typeof params>(KYC_URLS.LIST, params);
23
+ return kycListResponseSchema.parse(raw);
24
+ };
25
+
26
+ // Component:
27
+ const { data } = useQuery({
28
+ queryKey: kycKeys.list({ page, pageSize, status }),
29
+ queryFn: () => getKycList({ page, pageSize, status }),
30
+ placeholderData: (prev) => prev, // smooth pagination — keep previous data visible
31
+ });
32
+ ```
33
+
34
+ ## Detail
35
+
36
+ ```ts
37
+ export const getKycDetail = async (id: string): Promise<IKycRecord> => {
38
+ const raw = await GET<unknown>(KYC_URLS.DETAIL(id));
39
+ return kycRecordSchema.parse(raw);
40
+ };
41
+ ```
42
+
43
+ ## Create
44
+
45
+ ```ts
46
+ export const submitKyc = async (payload: IKycSubmitRequest): Promise<IKycRecord> => {
47
+ const raw = await POST<unknown, IKycSubmitRequest>(KYC_URLS.SUBMIT, payload);
48
+ return kycRecordSchema.parse(raw);
49
+ };
50
+
51
+ // Component:
52
+ const submit = useMutation({
53
+ mutationFn: submitKyc,
54
+ onSuccess: (newRecord) => {
55
+ queryClient.invalidateQueries({ queryKey: kycKeys.lists() });
56
+ queryClient.setQueryData(kycKeys.detail(newRecord.id), newRecord);
57
+ },
58
+ });
59
+ ```
60
+
61
+ ## Update
62
+
63
+ ```ts
64
+ export const updateKyc = async (args: {
65
+ id: string;
66
+ body: IKycUpdateRequest;
67
+ }): Promise<IKycRecord> => {
68
+ const raw = await PUT<unknown, IKycUpdateRequest>(KYC_URLS.DETAIL(args.id), args.body);
69
+ return kycRecordSchema.parse(raw);
70
+ };
71
+
72
+ // Component:
73
+ const update = useMutation({
74
+ mutationFn: updateKyc,
75
+ onSuccess: (updated, { id }) => {
76
+ queryClient.invalidateQueries({ queryKey: kycKeys.lists() });
77
+ queryClient.setQueryData(kycKeys.detail(id), updated);
78
+ },
79
+ });
80
+ ```
81
+
82
+ Pattern: mutations that take both an id and a body wrap them in a single object. Keeps `mutationFn` typed as `(args: {...}) => Promise<...>` rather than overloaded.
83
+
84
+ ## Delete
85
+
86
+ ```ts
87
+ export const deleteKyc = (id: string): Promise<void> => DELETE<void>(KYC_URLS.DETAIL(id));
88
+
89
+ // Component:
90
+ const del = useMutation({
91
+ mutationFn: deleteKyc,
92
+ onSuccess: (_, id) => {
93
+ queryClient.invalidateQueries({ queryKey: kycKeys.lists() });
94
+ queryClient.removeQueries({ queryKey: kycKeys.detail(id) });
95
+ },
96
+ });
97
+ ```
98
+
99
+ ## Polling (e.g. async job status)
100
+
101
+ ```ts
102
+ export const getJobStatus = async (jobId: string): Promise<IJobStatus> => {
103
+ const raw = await GET<unknown>(JOB_URLS.STATUS(jobId));
104
+ return jobStatusSchema.parse(raw);
105
+ };
106
+
107
+ // Component:
108
+ const { data } = useQuery({
109
+ queryKey: jobKeys.status(jobId),
110
+ queryFn: () => getJobStatus(jobId),
111
+ refetchInterval: (query) => {
112
+ const status = query.state.data?.status;
113
+ return status === 'completed' || status === 'failed' ? false : 3000;
114
+ },
115
+ });
116
+ ```
117
+
118
+ ## File upload
119
+
120
+ ```ts
121
+ export const uploadDocument = async (file: File): Promise<IDocumentRecord> => {
122
+ const form = new FormData();
123
+ form.append('file', file);
124
+ const raw = await POST<unknown, FormData>(DOCS_URLS.UPLOAD, form, {
125
+ headers: { 'Content-Type': 'multipart/form-data' },
126
+ });
127
+ return documentRecordSchema.parse(raw);
128
+ };
129
+ ```
130
+
131
+ ## File download
132
+
133
+ ```ts
134
+ export const downloadStatement = (id: string): Promise<Blob> =>
135
+ GET<Blob, void>(STATEMENT_URLS.DOWNLOAD(id), undefined, { responseType: 'blob' });
136
+ ```
137
+
138
+ ## Parallel fetches in one query
139
+
140
+ ```ts
141
+ export const getDashboardData = async (userId: string): Promise<IDashboardData> => {
142
+ const [profile, accounts, recent] = await Promise.all([
143
+ GET<unknown>(USER_URLS.PROFILE(userId)),
144
+ GET<unknown>(ACCOUNT_URLS.LIST_FOR_USER(userId)),
145
+ GET<unknown>(TX_URLS.RECENT_FOR_USER(userId, { limit: 5 })),
146
+ ]);
147
+ return dashboardSchema.parse({ profile, accounts, recent });
148
+ };
149
+ ```
150
+
151
+ Components calling this get one queryKey, one loading state, one error — even though it makes 3 HTTP calls.
@@ -0,0 +1,63 @@
1
+ # Template: TanStack Query variant
2
+
3
+ Overlay applied on top of `templates/_shared/` when the user picks **TanStack Query**.
4
+
5
+ ## Adds
6
+
7
+ - `@tanstack/react-query` + devtools + `zustand` deps
8
+ - `src/api/axiosInstance.ts` — single shared axios instance from `@<projectName>/core/http`
9
+ - `src/api/http.ts` — typed `GET<TRes, TParams>`, `POST<TRes, TReq>`, `PUT`, `PATCH`, `DELETE` helpers
10
+ - `src/api/queryClient.ts` — `QueryClient` with BFSI defaults (30s stale, no focus refetch, no mutation retry)
11
+ - `src/services/example.ts` — reference service showing the `IRequest`/`IResponse` pattern
12
+ - `src/app/App.tsx` — overlays \_shared App with `<QueryClientProvider>`
13
+
14
+ ## Service-method pattern (vs RTK Query's dispatch style)
15
+
16
+ You call services directly — no hooks, no dispatch:
17
+
18
+ ```ts
19
+ // src/services/kyc.ts
20
+ import { POST, GET } from '@/api/http';
21
+
22
+ export interface IKycRequest {
23
+ pan: string;
24
+ aadhaar: string;
25
+ }
26
+ export interface IKycResponse {
27
+ id: string;
28
+ status: 'pending' | 'approved';
29
+ }
30
+
31
+ export const submitKyc = (payload: IKycRequest): Promise<IKycResponse> =>
32
+ POST<IKycResponse, IKycRequest>('/kyc', payload);
33
+
34
+ export const getKyc = (id: string): Promise<IKycResponse> => GET<IKycResponse>(`/kyc/${id}`);
35
+ ```
36
+
37
+ Inside components, wire with hooks:
38
+
39
+ ```tsx
40
+ import { useMutation, useQuery } from '@tanstack/react-query';
41
+ import { submitKyc, getKyc } from '@/services/kyc';
42
+
43
+ const submit = useMutation({ mutationFn: submitKyc });
44
+ const detail = useQuery({ queryKey: ['kyc', id], queryFn: () => getKyc(id) });
45
+ ```
46
+
47
+ ## Auth: set-once at login
48
+
49
+ The axios instance has no per-request token interceptor. Set the token once at login:
50
+
51
+ ```ts
52
+ import { setAuthToken } from '@react-vault/core/http';
53
+ import axiosInstance from '@/api/axiosInstance';
54
+
55
+ // inside login mutation onSuccess:
56
+ setAuthToken(axiosInstance, response.token);
57
+ ```
58
+
59
+ On 401, the instance's `onUnauthorized` callback clears the token and redirects to `/login`.
60
+
61
+ ## Why Zustand for client state
62
+
63
+ TanStack Query owns **server state** (fetches, caches, invalidation). Use Zustand for **client state** (UI state surviving route changes, form drafts, cross-component selections). Don't use Zustand for server data — that's what TanStack Query is for.
@@ -0,0 +1,8 @@
1
+ {
2
+ "_comment": "Partial package.json — CLI merges this with templates/_shared/package.json. Adds TanStack Query + Zustand deps.",
3
+ "dependencies": {
4
+ "@tanstack/react-query": "5.32.1",
5
+ "@tanstack/react-query-devtools": "5.32.1",
6
+ "zustand": "4.5.2"
7
+ }
8
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Single axios instance for the app. Auth token set ONCE at login via
3
+ * setAuthToken() — not per-request.
4
+ */
5
+ import { createAxios } from '@react-vault/core/http';
6
+ import { env } from '../env.js';
7
+
8
+ const axiosInstance = createAxios({
9
+ baseURL: env.VITE_API_BASE_URL,
10
+ timeoutMs: env.VITE_API_TIMEOUT_MS,
11
+ authHeaderName: env.VITE_AUTH_HEADER_NAME,
12
+ snakeCaseBackend: false,
13
+ onUnauthorized: () => {
14
+ if (typeof window !== 'undefined') {
15
+ window.location.href = '/login';
16
+ }
17
+ },
18
+ });
19
+
20
+ export default axiosInstance;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Typed HTTP helpers for TanStack Query feature services.
3
+ *
4
+ * Usage:
5
+ * import { GET, POST, PUT, PATCH, DELETE } from '@/api/http';
6
+ *
7
+ * interface ILoginRequest { email: string; password: string; }
8
+ * interface ILoginResponse { token: string; userId: string; }
9
+ *
10
+ * export const loginService = (payload: ILoginRequest) =>
11
+ * POST<ILoginResponse, ILoginRequest>('/auth/login', payload);
12
+ *
13
+ * Pair with useMutation / useQuery:
14
+ * const m = useMutation({ mutationFn: loginService });
15
+ * const q = useQuery({ queryKey: ['kyc', id], queryFn: () => getKyc(id) });
16
+ */
17
+ import type { AxiosRequestConfig } from 'axios';
18
+ import axiosInstance from './axiosInstance.js';
19
+
20
+ export async function GET<TResponse, TParams = void>(
21
+ url: string,
22
+ params?: TParams,
23
+ config?: AxiosRequestConfig,
24
+ ): Promise<TResponse> {
25
+ const { data } = await axiosInstance.get<TResponse>(url, { ...config, params });
26
+ return data;
27
+ }
28
+
29
+ export async function POST<TResponse, TRequest = void>(
30
+ url: string,
31
+ payload?: TRequest,
32
+ config?: AxiosRequestConfig,
33
+ ): Promise<TResponse> {
34
+ const { data } = await axiosInstance.post<TResponse>(url, payload, config);
35
+ return data;
36
+ }
37
+
38
+ export async function PUT<TResponse, TRequest = void>(
39
+ url: string,
40
+ payload?: TRequest,
41
+ config?: AxiosRequestConfig,
42
+ ): Promise<TResponse> {
43
+ const { data } = await axiosInstance.put<TResponse>(url, payload, config);
44
+ return data;
45
+ }
46
+
47
+ export async function PATCH<TResponse, TRequest = void>(
48
+ url: string,
49
+ payload?: TRequest,
50
+ config?: AxiosRequestConfig,
51
+ ): Promise<TResponse> {
52
+ const { data } = await axiosInstance.patch<TResponse>(url, payload, config);
53
+ return data;
54
+ }
55
+
56
+ export async function DELETE<TResponse = void>(
57
+ url: string,
58
+ config?: AxiosRequestConfig,
59
+ ): Promise<TResponse> {
60
+ const { data } = await axiosInstance.delete<TResponse>(url, config);
61
+ return data;
62
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * TanStack Query client with BFSI-friendly defaults:
3
+ * - 30s stale time (don't thrash refetching)
4
+ * - No refetch on focus (user's choice, not surprise)
5
+ * - Don't retry 4xx (except 408/429)
6
+ * - Mutations never auto-retry (use idempotency-key + explicit retry)
7
+ */
8
+ import { QueryClient } from '@tanstack/react-query';
9
+
10
+ export const queryClient = new QueryClient({
11
+ defaultOptions: {
12
+ queries: {
13
+ staleTime: 30_000,
14
+ gcTime: 5 * 60_000,
15
+ retry: (failureCount, error) => {
16
+ const status = (error as { status?: number })?.status;
17
+ if (status && status >= 400 && status < 500 && status !== 408 && status !== 429) {
18
+ return false;
19
+ }
20
+ return failureCount < 2;
21
+ },
22
+ refetchOnWindowFocus: false,
23
+ },
24
+ mutations: {
25
+ retry: false,
26
+ },
27
+ },
28
+ });
@@ -0,0 +1,20 @@
1
+ /**
2
+ * TanStack variant App.tsx — overlays the _shared App.tsx with QueryClientProvider.
3
+ */
4
+ import { QueryClientProvider } from '@tanstack/react-query';
5
+ import { BrowserRouter } from 'react-router-dom';
6
+ import { ErrorBoundary } from '../shared/ErrorBoundary.js';
7
+ import { AppRoutes } from '../routes/index.js';
8
+ import { queryClient } from '../api/queryClient.js';
9
+
10
+ export function App(): JSX.Element {
11
+ return (
12
+ <ErrorBoundary>
13
+ <QueryClientProvider client={queryClient}>
14
+ <BrowserRouter>
15
+ <AppRoutes />
16
+ </BrowserRouter>
17
+ </QueryClientProvider>
18
+ </ErrorBoundary>
19
+ );
20
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Example service. Pattern to copy for new features:
3
+ *
4
+ * 1. Define IRequest / IResponse interfaces
5
+ * 2. Export plain async functions that call GET/POST/PUT/PATCH/DELETE
6
+ * 3. Use them inside React components via useQuery / useMutation
7
+ *
8
+ * Keep services in src/services/<feature>.ts or src/features/<Feature>/service.ts —
9
+ * either works. Don't put TanStack hooks here; this layer is hook-free so
10
+ * services are easy to unit-test.
11
+ */
12
+ import { GET, POST } from '../api/http.js';
13
+
14
+ export interface IExampleListResponse {
15
+ items: Array<{ id: string; name: string }>;
16
+ total: number;
17
+ }
18
+
19
+ export interface IExampleCreateRequest {
20
+ name: string;
21
+ }
22
+
23
+ export interface IExampleCreateResponse {
24
+ id: string;
25
+ name: string;
26
+ }
27
+
28
+ export const getExamples = (): Promise<IExampleListResponse> =>
29
+ GET<IExampleListResponse>('/examples');
30
+
31
+ export const createExample = (payload: IExampleCreateRequest): Promise<IExampleCreateResponse> =>
32
+ POST<IExampleCreateResponse, IExampleCreateRequest>('/examples', payload);