@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,109 @@
1
+ ---
2
+ name: axios-auth
3
+ description: Configure the project's axios instance and set the auth token ONCE at login via setAuthToken() — not per-request. Used by TanStack Query service methods. Covers response interceptor for notifications + 401 redirect. Use when the user mentions axios, API client, set token, login flow, 401 handling, response interceptor, error notification.
4
+ ---
5
+
6
+ # Axios + Auth Pattern (TanStack variant)
7
+
8
+ The HTTP layer is two files under `src/api/` plus the auth-token helper from `@<scope>/core/http`. Tokens are set ONCE at login on the axios instance — not injected per-request via interceptor.
9
+
10
+ ## File map
11
+
12
+ | File | Role |
13
+ | -------------------------- | ---------------------------------------------------------------------------------- |
14
+ | `src/api/axiosInstance.ts` | Single shared `AxiosInstance` from `createAxios()` |
15
+ | `src/api/http.ts` | Typed `GET<TRes,TParams>` / `POST<TRes,TReq>` / `PUT` / `PATCH` / `DELETE` helpers |
16
+ | `@<scope>/core/http` | Exports `createAxios`, `setAuthToken`, `clearAuthToken`, `ApiError` |
17
+
18
+ ## Workflow
19
+
20
+ ### Step 1 — Set token at login (the canonical place)
21
+
22
+ Inside your login service's success path:
23
+
24
+ ```ts
25
+ import { useMutation } from '@tanstack/react-query';
26
+ import { setAuthToken } from '@<scope>/core/http';
27
+ import axiosInstance from '@/api/axiosInstance';
28
+ import { loginService } from '@/services/auth';
29
+
30
+ const loginMutation = useMutation({
31
+ mutationFn: loginService,
32
+ onSuccess: (response) => {
33
+ setAuthToken(axiosInstance, response.token);
34
+ // navigate to dashboard, etc.
35
+ },
36
+ });
37
+ ```
38
+
39
+ `setAuthToken` writes to `instance.defaults.headers.common.Authorization`. Every subsequent request carries the header automatically.
40
+
41
+ ### Step 2 — Clear on logout / 401
42
+
43
+ `createAxios()` wires this: when a response is 401, it auto-calls `clearAuthToken(instance)` then invokes the `onUnauthorized` callback (which redirects to `/login` in the scaffolded template).
44
+
45
+ Manual logout flow:
46
+
47
+ ```ts
48
+ import { clearAuthToken } from '@<scope>/core/http';
49
+ import axiosInstance from '@/api/axiosInstance';
50
+ import { useQueryClient } from '@tanstack/react-query';
51
+
52
+ const queryClient = useQueryClient();
53
+
54
+ function logout() {
55
+ clearAuthToken(axiosInstance);
56
+ queryClient.clear(); // wipe all cached server data
57
+ navigate('/login');
58
+ }
59
+ ```
60
+
61
+ `queryClient.clear()` matters on logout — without it, a re-login sees stale data for a flash before refetch.
62
+
63
+ ### Step 3 — Use the typed HTTP helpers in services
64
+
65
+ ```ts
66
+ // src/services/kyc.ts
67
+ import { GET, POST } from '@/api/http';
68
+
69
+ export interface IKycRequest {
70
+ pan: string;
71
+ aadhaar: string;
72
+ }
73
+ export interface IKycResponse {
74
+ id: string;
75
+ status: 'pending' | 'approved' | 'rejected';
76
+ }
77
+
78
+ export const submitKyc = (payload: IKycRequest): Promise<IKycResponse> =>
79
+ POST<IKycResponse, IKycRequest>('/kyc', payload);
80
+
81
+ export const getKyc = (id: string): Promise<IKycResponse> => GET<IKycResponse>(`/kyc/${id}`);
82
+ ```
83
+
84
+ Services are pure async functions — no hooks, no React. That makes them trivially unit-testable.
85
+
86
+ ## Conventions enforced
87
+
88
+ - ❌ NEVER inject token via a `request.use` interceptor — use `setAuthToken` at login.
89
+ - ❌ NEVER read token from `localStorage` on every request.
90
+ - ❌ NEVER hardcode `Authorization: Bearer ...` anywhere.
91
+ - ❌ NEVER use plain `axios.get(...)` in service files — use the typed `GET<TRes,TParams>` helper.
92
+ - ✅ One axios instance per app — exported as the default from `axiosInstance.ts`.
93
+ - ✅ All services use typed helpers from `src/api/http.ts`.
94
+ - ✅ On logout: `clearAuthToken()` + `queryClient.clear()`.
95
+
96
+ ## Quick reference checklist
97
+
98
+ When adding API auth:
99
+
100
+ - [ ] Login mutation calls `setAuthToken(axiosInstance, token)` in `onSuccess`
101
+ - [ ] Logout calls `clearAuthToken(axiosInstance)` AND `queryClient.clear()`
102
+ - [ ] Token never appears in `localStorage` (memory only)
103
+ - [ ] `onUnauthorized` callback in `axiosInstance.ts` redirects to `/login`
104
+
105
+ ## References
106
+
107
+ - [`references/full-code-walkthrough.md`](references/full-code-walkthrough.md) — annotated walk through axiosInstance + http helpers
108
+ - [`references/error-shape.md`](references/error-shape.md) — `ApiError` contract + safe surfacing inside `useMutation`/`useQuery`
109
+ - [`references/notification-pattern.md`](references/notification-pattern.md) — toast notifications from mutation `onSuccess`/`onError` callbacks
@@ -0,0 +1,89 @@
1
+ # Error shape contract (TanStack variant)
2
+
3
+ ## `ApiError` (from `@<scope>/core/http`)
4
+
5
+ ```ts
6
+ class ApiError extends Error {
7
+ readonly kind: ApiErrorKind;
8
+ readonly status?: number;
9
+ readonly ref?: string;
10
+ readonly fieldErrors?: Record<string, string>;
11
+ }
12
+
13
+ type ApiErrorKind =
14
+ | 'network'
15
+ | 'timeout'
16
+ | 'unauthorized'
17
+ | 'forbidden'
18
+ | 'not_found'
19
+ | 'conflict'
20
+ | 'validation'
21
+ | 'rate_limited'
22
+ | 'server_error'
23
+ | 'cancelled'
24
+ | 'unknown';
25
+ ```
26
+
27
+ `createAxios` converts every axios error into `ApiError`. Services THROW it (since they return promises). TanStack Query catches and exposes via `useQuery({ error })` / `useMutation({ onError, error })`.
28
+
29
+ ## Backend error envelope
30
+
31
+ ```json
32
+ {
33
+ "errors": [{ "detail": "Email is already taken" }]
34
+ }
35
+ ```
36
+
37
+ Or for field-level (422):
38
+
39
+ ```json
40
+ {
41
+ "errors": {
42
+ "email": ["is already taken"],
43
+ "password": ["is too short"]
44
+ }
45
+ }
46
+ ```
47
+
48
+ `ApiError.fieldErrors` is populated for `kind === 'validation'`.
49
+
50
+ ## Surfacing errors in components
51
+
52
+ ```tsx
53
+ import { useMutation } from '@tanstack/react-query';
54
+ import type { ApiError } from '@<scope>/core/http';
55
+ import { submitKyc } from '@/services/kyc';
56
+
57
+ const mutation = useMutation({
58
+ mutationFn: submitKyc,
59
+ onError: (error: ApiError) => {
60
+ if (error.kind === 'validation' && error.fieldErrors) {
61
+ // Set RHF field errors:
62
+ for (const [field, msg] of Object.entries(error.fieldErrors)) {
63
+ form.setError(field as keyof FormValues, { message: msg });
64
+ }
65
+ } else if (error.kind === 'unauthorized') {
66
+ // Already handled by axios's onUnauthorized — nothing to do.
67
+ } else {
68
+ toast.error(safeMessage(error));
69
+ }
70
+ },
71
+ });
72
+ ```
73
+
74
+ ## NEVER expose to UI
75
+
76
+ - ❌ `error.message` from raw axios — leaks "Network Error" / stack info
77
+ - ❌ `error.response.data.errors` raw — may contain SQL fragments, DB IDs
78
+ - ❌ HTTP status codes as user copy ("Error 500")
79
+
80
+ Use `toSafeView(error, t)` from `@<scope>/core/compliance` to convert `ApiError.kind` into a user-facing toast title + description + ref code.
81
+
82
+ ## Retry semantics
83
+
84
+ Default `queryClient` config (in `src/api/queryClient.ts`):
85
+
86
+ - Queries: retry up to 2x on 5xx and 408/429; never retry 4xx (except 408/429); no retry on network errors that aren't transient
87
+ - Mutations: NEVER auto-retry. Use idempotency-key + explicit user-triggered retry
88
+
89
+ This matters in BFSI: silently retrying a payment mutation on an ambiguous 5xx could double-charge.
@@ -0,0 +1,121 @@
1
+ # Axios + Auth — full code walkthrough (TanStack)
2
+
3
+ ## `src/api/axiosInstance.ts`
4
+
5
+ ```ts
6
+ import { createAxios } from '@<scope>/core/http';
7
+ import { env } from '../env.js';
8
+
9
+ const axiosInstance = createAxios({
10
+ baseURL: env.VITE_API_BASE_URL,
11
+ timeoutMs: env.VITE_API_TIMEOUT_MS,
12
+ authHeaderName: env.VITE_AUTH_HEADER_NAME,
13
+ snakeCaseBackend: false,
14
+ onUnauthorized: () => {
15
+ if (typeof window !== 'undefined') {
16
+ window.location.href = '/login';
17
+ }
18
+ },
19
+ });
20
+
21
+ export default axiosInstance;
22
+ ```
23
+
24
+ Key points:
25
+
26
+ - `createAxios` attaches BFSI-grade interceptors: request-IDs, idempotency keys, error mapping to typed `ApiError`.
27
+ - `onUnauthorized` fires AFTER `clearAuthToken` has wiped the token off the instance.
28
+ - `snakeCaseBackend: true` enables automatic snake↔camel transformation.
29
+
30
+ Unlike the RTK variant, there is NO separate `interceptor.ts` file — the response side is handled by `createAxios` (error mapping) and by mutation/query callbacks (notifications). No side-effect import needed.
31
+
32
+ ## `src/api/http.ts`
33
+
34
+ ```ts
35
+ import type { AxiosRequestConfig } from 'axios';
36
+ import axiosInstance from './axiosInstance.js';
37
+
38
+ export async function GET<TResponse, TParams = void>(
39
+ url: string,
40
+ params?: TParams,
41
+ config?: AxiosRequestConfig,
42
+ ): Promise<TResponse> {
43
+ const { data } = await axiosInstance.get<TResponse>(url, { ...config, params });
44
+ return data;
45
+ }
46
+
47
+ export async function POST<TResponse, TRequest = void>(
48
+ url: string,
49
+ payload?: TRequest,
50
+ config?: AxiosRequestConfig,
51
+ ): Promise<TResponse> {
52
+ const { data } = await axiosInstance.post<TResponse>(url, payload, config);
53
+ return data;
54
+ }
55
+
56
+ // PUT, PATCH, DELETE follow the same pattern
57
+ ```
58
+
59
+ Key points:
60
+
61
+ - All helpers unwrap `response.data` — services return the body, not the AxiosResponse envelope.
62
+ - Generic `<TResponse, TRequest>` keeps the call-site type-safe without forcing the caller to cast.
63
+ - Optional `config` lets you pass `headers`, `responseType`, `signal` (AbortController), etc. per-call.
64
+
65
+ ## Why no `baseQuery` here
66
+
67
+ TanStack Query doesn't have RTK Query's "baseQuery" abstraction — service functions ARE the baseQuery equivalent. Each service is a tiny async function that throws on failure. `useQuery`/`useMutation` calls them and handles their promise.
68
+
69
+ ## File upload pattern
70
+
71
+ ```ts
72
+ export const uploadDocument = (file: File): Promise<IDocumentRecord> => {
73
+ const form = new FormData();
74
+ form.append('file', file);
75
+ return POST<IDocumentRecord, FormData>('/docs/upload', form, {
76
+ headers: { 'Content-Type': 'multipart/form-data' },
77
+ });
78
+ };
79
+ ```
80
+
81
+ ## File download pattern
82
+
83
+ ```ts
84
+ export const downloadStatement = (id: string): Promise<Blob> =>
85
+ GET<Blob, void>(`/statements/${id}`, undefined, { responseType: 'blob' });
86
+ ```
87
+
88
+ Then in the component:
89
+
90
+ ```tsx
91
+ const { data: blob } = useQuery({
92
+ queryKey: ['statement', id],
93
+ queryFn: () => downloadStatement(id),
94
+ enabled: shouldDownload,
95
+ });
96
+
97
+ useEffect(() => {
98
+ if (!blob) return;
99
+ const url = URL.createObjectURL(blob);
100
+ const a = document.createElement('a');
101
+ a.href = url;
102
+ a.download = `statement-${id}.pdf`;
103
+ a.click();
104
+ URL.revokeObjectURL(url);
105
+ }, [blob, id]);
106
+ ```
107
+
108
+ ## Cancellation pattern
109
+
110
+ ```ts
111
+ // In the component:
112
+ const controller = new AbortController();
113
+ const { data } = useQuery({
114
+ queryKey: ['search', term],
115
+ queryFn: () => GET<ISearchResult>('/search', { q: term }, { signal: controller.signal }),
116
+ });
117
+ // On unmount or term change:
118
+ useEffect(() => () => controller.abort(), []);
119
+ ```
120
+
121
+ TanStack Query also auto-cancels stale in-flight queries when the queryKey changes — usually you don't need explicit AbortController.
@@ -0,0 +1,109 @@
1
+ # Notification pattern (TanStack variant)
2
+
3
+ Unlike the RTK variant (notifications dispatched from baseQuery via Redux slice), TanStack lets you dispatch notifications directly from mutation callbacks. No global slice needed.
4
+
5
+ ## Simple — at the call site
6
+
7
+ ```tsx
8
+ import { useMutation } from '@tanstack/react-query';
9
+ import { toast } from 'sonner';
10
+ import { submitKyc } from '@/services/kyc';
11
+ import type { ApiError } from '@<scope>/core/http';
12
+
13
+ const mutation = useMutation({
14
+ mutationFn: submitKyc,
15
+ onSuccess: () => toast.success('KYC submitted'),
16
+ onError: (err: ApiError) => toast.error(safeMessage(err)),
17
+ });
18
+ ```
19
+
20
+ ## DRY — wrap mutation creation
21
+
22
+ When most mutations want the same toast behaviour:
23
+
24
+ ```ts
25
+ // src/services/useAuditedMutation.ts
26
+ import { useMutation, type UseMutationOptions } from '@tanstack/react-query';
27
+ import { toast } from 'sonner';
28
+ import type { ApiError } from '@<scope>/core/http';
29
+
30
+ export function useAuditedMutation<TData, TVars>(
31
+ options: UseMutationOptions<TData, ApiError, TVars> & {
32
+ successMessage?: string;
33
+ eventName: string; // audit event (e.g. 'kyc.submission.attempted')
34
+ },
35
+ ) {
36
+ return useMutation({
37
+ ...options,
38
+ onMutate: async (vars) => {
39
+ // emit audit event (see bfsi-audit-action skill for full pattern)
40
+ await options.onMutate?.(vars);
41
+ },
42
+ onSuccess: (data, vars, ctx) => {
43
+ if (options.successMessage) toast.success(options.successMessage);
44
+ options.onSuccess?.(data, vars, ctx);
45
+ },
46
+ onError: (err, vars, ctx) => {
47
+ toast.error(safeMessage(err));
48
+ options.onError?.(err, vars, ctx);
49
+ },
50
+ });
51
+ }
52
+ ```
53
+
54
+ Then use it everywhere:
55
+
56
+ ```tsx
57
+ const submit = useAuditedMutation({
58
+ mutationFn: submitKyc,
59
+ successMessage: 'KYC submitted',
60
+ eventName: 'kyc.submission.attempted',
61
+ onSuccess: () => navigate('/kyc/success'),
62
+ });
63
+ ```
64
+
65
+ ## Global error handler
66
+
67
+ For a true catch-all, set a `QueryCache` / `MutationCache` error handler in the `QueryClient` config:
68
+
69
+ ```ts
70
+ import { QueryClient, QueryCache, MutationCache } from '@tanstack/react-query';
71
+ import { toast } from 'sonner';
72
+
73
+ export const queryClient = new QueryClient({
74
+ queryCache: new QueryCache({
75
+ onError: (error) => {
76
+ // ONLY fire for unhandled errors — usually you want per-component handling
77
+ // toast.error(safeMessage(error as ApiError));
78
+ },
79
+ }),
80
+ mutationCache: new MutationCache({
81
+ onError: (error, _vars, _ctx, mutation) => {
82
+ // Skip if the mutation has its own onError
83
+ if (mutation.options.onError) return;
84
+ toast.error(safeMessage(error as ApiError));
85
+ },
86
+ }),
87
+ // ... defaultOptions
88
+ });
89
+ ```
90
+
91
+ The `MutationCache.onError` pattern is useful: every mutation that doesn't define its own `onError` gets the default toast.
92
+
93
+ ## Avoid: toast spam
94
+
95
+ ❌ Don't toast on every query error — components are often mounted in the background or with stale flags, and you'd toast for non-user-initiated fetches.
96
+
97
+ ✅ DO toast on every mutation error — mutations are user-initiated, so an error means the user's action failed.
98
+
99
+ ## Avoid: notifications for 401 / cancelled
100
+
101
+ The 401 path already redirects to login (handled by `createAxios.onUnauthorized`). Don't double-toast. Filter inside `safeMessage`:
102
+
103
+ ```ts
104
+ function safeMessage(error: ApiError): string {
105
+ if (error.kind === 'unauthorized') return ''; // empty = no toast (sonner handles)
106
+ if (error.kind === 'cancelled') return '';
107
+ return toSafeView(error, t).description;
108
+ }
109
+ ```
@@ -0,0 +1,144 @@
1
+ ---
2
+ name: constants-organization
3
+ description: Where each kind of constant lives in the project and how to add a new one. Covers URL endpoint constants, HTTP method usage, route paths, queryKey conventions, validation regex patterns, and app-wide constants. Use when adding a new endpoint URL, route path, queryKey, validation regex, or any other shared constant.
4
+ ---
5
+
6
+ # Constants Organisation (TanStack variant)
7
+
8
+ Constants are split across files by purpose. Each kind has one home — don't scatter.
9
+
10
+ ## File map
11
+
12
+ ```
13
+ src/utils/constants/
14
+ ├── appConstants.ts App-wide enums (ERROR / SUCCESS, storage keys, etc.)
15
+ ├── urlConstants.ts Every API endpoint URL string
16
+ ├── routeConstants.ts Every router path
17
+ ├── queryKeys.ts TanStack Query queryKey factories
18
+ └── regexConstants.ts Reusable validation regex
19
+ ```
20
+
21
+ ## When to use which
22
+
23
+ | Adding … | Goes in | Naming convention |
24
+ | ---------------------------- | ------------------- | --------------------------------------------------- |
25
+ | A backend endpoint URL | `urlConstants.ts` | `<FEATURE>_URLS.<ACTION>` or `<FEATURE>_URL` |
26
+ | A frontend route path | `routeConstants.ts` | `ROUTES.<feature>.<view>` |
27
+ | A TanStack queryKey | `queryKeys.ts` | `<feature>Keys.list()` / `<feature>Keys.detail(id)` |
28
+ | A validation regex | `regexConstants.ts` | `<FIELD>_REGEX` |
29
+ | A storage key | `appConstants.ts` | `<PURPOSE>_KEY` |
30
+ | An enum value used > 1 place | `appConstants.ts` | `<NAME>` (UPPER_SNAKE) |
31
+
32
+ ## Workflow — adding a new endpoint
33
+
34
+ 1. Open `src/utils/constants/urlConstants.ts`.
35
+ 2. Add to the appropriate feature block (or create one):
36
+ ```ts
37
+ export const KYC_URLS = {
38
+ LIST: '/kyc',
39
+ DETAIL: (id: string) => `/kyc/${id}`,
40
+ SUBMIT: '/kyc/submit',
41
+ } as const;
42
+ ```
43
+ 3. Reference from the feature's service:
44
+
45
+ ```ts
46
+ import { KYC_URLS } from '@/utils/constants/urlConstants';
47
+ import { GET } from '@/api/http';
48
+
49
+ export const getKycList = () => GET<IKycListResponse>(KYC_URLS.LIST);
50
+ ```
51
+
52
+ ## Workflow — adding a queryKey
53
+
54
+ TanStack Query queryKeys are arrays that uniquely identify cached data. Always go through a queryKey factory — never inline the array. This keeps invalidation patterns DRY and refactor-safe.
55
+
56
+ `src/utils/constants/queryKeys.ts`:
57
+
58
+ ```ts
59
+ export const kycKeys = {
60
+ all: ['kyc'] as const,
61
+ lists: () => [...kycKeys.all, 'list'] as const,
62
+ list: (filters?: KycFilters) => [...kycKeys.lists(), filters] as const,
63
+ details: () => [...kycKeys.all, 'detail'] as const,
64
+ detail: (id: string) => [...kycKeys.details(), id] as const,
65
+ };
66
+ ```
67
+
68
+ Usage:
69
+
70
+ ```tsx
71
+ // Component
72
+ const { data } = useQuery({
73
+ queryKey: kycKeys.list(filters),
74
+ queryFn: () => getKycList(filters),
75
+ });
76
+
77
+ // Invalidation
78
+ queryClient.invalidateQueries({ queryKey: kycKeys.all }); // all KYC
79
+ queryClient.invalidateQueries({ queryKey: kycKeys.lists() }); // all lists
80
+ queryClient.invalidateQueries({ queryKey: kycKeys.detail(id) }); // one record
81
+ ```
82
+
83
+ See [`references/query-key-factories.md`](references/query-key-factories.md) for the full pattern + invalidation matrix.
84
+
85
+ ## Workflow — adding a route
86
+
87
+ 1. Open `src/utils/constants/routeConstants.ts`.
88
+ 2. Add the path:
89
+ ```ts
90
+ export const ROUTES = {
91
+ kyc: {
92
+ list: '/kyc',
93
+ detail: '/kyc/:id',
94
+ submit: '/kyc/submit',
95
+ },
96
+ } as const;
97
+ ```
98
+ 3. Wire it in `src/routes/index.tsx`:
99
+ ```tsx
100
+ <Route
101
+ path={ROUTES.kyc.list}
102
+ element={
103
+ <ProtectedRoute permission="kyc.view">
104
+ <KycList />
105
+ </ProtectedRoute>
106
+ }
107
+ />
108
+ ```
109
+
110
+ ## Workflow — adding a validation regex
111
+
112
+ Centralise — never inline.
113
+
114
+ ```ts
115
+ // src/utils/constants/regexConstants.ts
116
+ export { PII_PATTERNS } from '@<scope>/core/pii';
117
+ import { PII_PATTERNS } from '@<scope>/core/pii';
118
+
119
+ export const PAN_REGEX = PII_PATTERNS.pan;
120
+ export const AADHAAR_REGEX = PII_PATTERNS.aadhaar;
121
+ export const MOBILE_REGEX = PII_PATTERNS.mobileIndia;
122
+ export const IFSC_REGEX = PII_PATTERNS.ifsc;
123
+ ```
124
+
125
+ Use in Zod:
126
+
127
+ ```ts
128
+ import { PAN_REGEX } from '@/utils/constants/regexConstants';
129
+ const schema = z.object({ pan: z.string().regex(PAN_REGEX) });
130
+ ```
131
+
132
+ ## Conventions enforced
133
+
134
+ - ❌ NEVER hardcode a URL string in a service — always reference `urlConstants.ts`.
135
+ - ❌ NEVER inline a queryKey array — always use a key factory from `queryKeys.ts`.
136
+ - ❌ NEVER inline a regex in component code or schema — always via `regexConstants.ts`.
137
+ - ✅ Group by feature within each constants file (e.g. `KYC_URLS`, `LOAN_URLS`; `kycKeys`, `loanKeys`).
138
+ - ✅ Use `as const` on the exported objects so TypeScript infers literal types.
139
+ - ✅ Functions for dynamic paths (`DETAIL: (id) => ...`), strings for static ones.
140
+
141
+ ## References
142
+
143
+ - [`references/example-files.md`](references/example-files.md) — full templates for each constants file
144
+ - [`references/query-key-factories.md`](references/query-key-factories.md) — queryKey factory pattern + invalidation matrix
@@ -0,0 +1,111 @@
1
+ # Constants files — templates (TanStack)
2
+
3
+ ## `src/utils/constants/appConstants.ts`
4
+
5
+ ```ts
6
+ // Storage keys
7
+ export const SELECTED_LOCALE = 'selectedLocale';
8
+ export const THEME_PREFERENCE = 'themePreference';
9
+
10
+ // Notification types
11
+ export const SUCCESS = 'success' as const;
12
+ export const ERROR = 'error' as const;
13
+ export const INFO = 'info' as const;
14
+ export const WARNING = 'warning' as const;
15
+
16
+ // Status enums
17
+ export const KYC_STATUS = {
18
+ PENDING: 'pending',
19
+ APPROVED: 'approved',
20
+ REJECTED: 'rejected',
21
+ REVIEW: 'review',
22
+ } as const;
23
+ export type KycStatus = (typeof KYC_STATUS)[keyof typeof KYC_STATUS];
24
+ ```
25
+
26
+ ## `src/utils/constants/urlConstants.ts`
27
+
28
+ ```ts
29
+ export const AUTH_URLS = {
30
+ LOGIN: '/auth/login',
31
+ LOGOUT: '/auth/logout',
32
+ REFRESH: '/auth/refresh',
33
+ PROFILE: '/auth/profile',
34
+ } as const;
35
+
36
+ export const KYC_URLS = {
37
+ LIST: '/kyc',
38
+ DETAIL: (id: string) => `/kyc/${id}`,
39
+ SUBMIT: '/kyc/submit',
40
+ APPROVE: (id: string) => `/kyc/${id}/approve`,
41
+ } as const;
42
+ ```
43
+
44
+ ## `src/utils/constants/routeConstants.ts`
45
+
46
+ ```ts
47
+ export const ROUTES = {
48
+ home: '/',
49
+ login: '/login',
50
+ dashboard: '/dashboard',
51
+ kyc: { list: '/kyc', detail: '/kyc/:id', submit: '/kyc/submit' },
52
+ notFound: '*',
53
+ } as const;
54
+
55
+ export const kycDetailPath = (id: string): string => `/kyc/${id}`;
56
+ ```
57
+
58
+ ## `src/utils/constants/queryKeys.ts`
59
+
60
+ ```ts
61
+ // One factory per feature. Each factory exports a const object with:
62
+ // - `all`: top-level scope
63
+ // - `lists()` / `list(filters)`: list queries (filters in key for cache isolation)
64
+ // - `details()` / `detail(id)`: detail queries (id in key)
65
+ // - Other entity-specific keys
66
+
67
+ export const kycKeys = {
68
+ all: ['kyc'] as const,
69
+ lists: () => [...kycKeys.all, 'list'] as const,
70
+ list: (filters?: KycFilters) => [...kycKeys.lists(), filters] as const,
71
+ details: () => [...kycKeys.all, 'detail'] as const,
72
+ detail: (id: string) => [...kycKeys.details(), id] as const,
73
+ };
74
+
75
+ export const transactionKeys = {
76
+ all: ['transactions'] as const,
77
+ lists: () => [...transactionKeys.all, 'list'] as const,
78
+ list: (filters?: TxFilters) => [...transactionKeys.lists(), filters] as const,
79
+ details: () => [...transactionKeys.all, 'detail'] as const,
80
+ detail: (id: string) => [...transactionKeys.details(), id] as const,
81
+ byAccount: (accountId: string) => [...transactionKeys.all, 'byAccount', accountId] as const,
82
+ };
83
+
84
+ interface KycFilters {
85
+ status?: string;
86
+ page?: number;
87
+ }
88
+ interface TxFilters {
89
+ dateFrom?: string;
90
+ dateTo?: string;
91
+ type?: string;
92
+ }
93
+ ```
94
+
95
+ ## `src/utils/constants/regexConstants.ts`
96
+
97
+ ```ts
98
+ export { PII_PATTERNS } from '@<scope>/core/pii';
99
+ import { PII_PATTERNS } from '@<scope>/core/pii';
100
+
101
+ export const PAN_REGEX = PII_PATTERNS.pan;
102
+ export const AADHAAR_REGEX = PII_PATTERNS.aadhaar;
103
+ export const MOBILE_REGEX = PII_PATTERNS.mobileIndia;
104
+ export const IFSC_REGEX = PII_PATTERNS.ifsc;
105
+ export const PINCODE_REGEX = PII_PATTERNS.pincodeIndia;
106
+ export const EMAIL_REGEX = PII_PATTERNS.email;
107
+
108
+ // App-specific patterns:
109
+ export const REFERENCE_CODE_REGEX = /^ERR-[A-Z0-9]{4}$/;
110
+ export const TRANSACTION_ID_REGEX = /^TXN[0-9]{12}$/;
111
+ ```