@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.
- package/LICENSE +12 -0
- package/README.md +16 -0
- package/bin/create-app.js +8 -0
- package/claude-toolkit/README.md +131 -0
- package/claude-toolkit/agents/bfsi-accessibility-auditor.md +132 -0
- package/claude-toolkit/agents/bfsi-architect.md +156 -0
- package/claude-toolkit/agents/bfsi-code-reviewer.md +137 -0
- package/claude-toolkit/agents/bfsi-compliance-auditor.md +161 -0
- package/claude-toolkit/agents/bfsi-pii-scanner.md +142 -0
- package/claude-toolkit/agents/bfsi-pr-reviewer.md +114 -0
- package/claude-toolkit/agents/bfsi-security-reviewer.md +136 -0
- package/claude-toolkit/commands/bfsi-audit.md +46 -0
- package/claude-toolkit/commands/bfsi-doctor.md +97 -0
- package/claude-toolkit/commands/bfsi-review.md +46 -0
- package/claude-toolkit/commands/bfsi-scaffold.md +47 -0
- package/claude-toolkit/hooks/hooks.json +181 -0
- package/claude-toolkit/hooks/scripts/a11y-check.sh +63 -0
- package/claude-toolkit/hooks/scripts/audit-prompt.sh +36 -0
- package/claude-toolkit/hooks/scripts/block-destructive.sh +41 -0
- package/claude-toolkit/hooks/scripts/block-force-push.sh +30 -0
- package/claude-toolkit/hooks/scripts/format.sh +42 -0
- package/claude-toolkit/hooks/scripts/inject-context.sh +44 -0
- package/claude-toolkit/hooks/scripts/lint.sh +45 -0
- package/claude-toolkit/hooks/scripts/protect-files.sh +53 -0
- package/claude-toolkit/hooks/scripts/save-compliance-context.sh +35 -0
- package/claude-toolkit/hooks/scripts/scan-pii.sh +87 -0
- package/claude-toolkit/hooks/scripts/scan-secrets.sh +67 -0
- package/claude-toolkit/hooks/scripts/verify-clean.sh +50 -0
- package/claude-toolkit/package.json +22 -0
- package/claude-toolkit/plugin.json +31 -0
- package/claude-toolkit/skills/bfsi-api-endpoint/SKILL.md +105 -0
- package/claude-toolkit/skills/bfsi-commit/SKILL.md +102 -0
- package/claude-toolkit/skills/bfsi-compliance-check/SKILL.md +107 -0
- package/claude-toolkit/skills/bfsi-encrypt-helper/SKILL.md +127 -0
- package/claude-toolkit/skills/bfsi-error-message/SKILL.md +162 -0
- package/claude-toolkit/skills/bfsi-feature/SKILL.md +120 -0
- package/claude-toolkit/skills/bfsi-feature/references/architecture.md +69 -0
- package/claude-toolkit/skills/bfsi-feature/references/audit-events.md +70 -0
- package/claude-toolkit/skills/bfsi-feature/scripts/scaffold.mjs +136 -0
- package/claude-toolkit/skills/bfsi-form/SKILL.md +73 -0
- package/claude-toolkit/skills/bfsi-form/references/validation-regex.md +50 -0
- package/claude-toolkit/skills/bfsi-onboarding/SKILL.md +110 -0
- package/claude-toolkit/skills/bfsi-pii-field/SKILL.md +90 -0
- package/claude-toolkit/skills/bfsi-test-pattern/SKILL.md +179 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +339 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/templates/_shared/.claude/settings.json +31 -0
- package/templates/_shared/.env.local.sample +25 -0
- package/templates/_shared/.github/workflows/ci.yml +49 -0
- package/templates/_shared/CLAUDE.md +89 -0
- package/templates/_shared/README.md +50 -0
- package/templates/_shared/index.html +16 -0
- package/templates/_shared/package.json +73 -0
- package/templates/_shared/postcss.config.cjs +6 -0
- package/templates/_shared/src/app/App.tsx +13 -0
- package/templates/_shared/src/app/globals.css +64 -0
- package/templates/_shared/src/env.ts +33 -0
- package/templates/_shared/src/i18n/i18n.ts +18 -0
- package/templates/_shared/src/i18n/translations/en.json +54 -0
- package/templates/_shared/src/i18n/translations/hi.json +30 -0
- package/templates/_shared/src/main.tsx +16 -0
- package/templates/_shared/src/routes/ProtectedRoute.tsx +28 -0
- package/templates/_shared/src/routes/index.tsx +67 -0
- package/templates/_shared/src/shared/ErrorBoundary.tsx +60 -0
- package/templates/_shared/tailwind.config.ts +68 -0
- package/templates/_shared/tests/setup.ts +7 -0
- package/templates/_shared/tsconfig.json +33 -0
- package/templates/_shared/tsconfig.node.json +13 -0
- package/templates/_shared/vite.config.ts +47 -0
- package/templates/rtk-query/.claude/skills/axios-auth/SKILL.md +103 -0
- package/templates/rtk-query/.claude/skills/axios-auth/references/error-shape.md +84 -0
- package/templates/rtk-query/.claude/skills/axios-auth/references/full-code-walkthrough.md +146 -0
- package/templates/rtk-query/.claude/skills/axios-auth/references/notification-wiring.md +141 -0
- package/templates/rtk-query/.claude/skills/constants-organization/SKILL.md +112 -0
- package/templates/rtk-query/.claude/skills/constants-organization/references/example-files.md +134 -0
- package/templates/rtk-query/.claude/skills/constants-organization/references/tag-types-catalog.md +53 -0
- package/templates/rtk-query/.claude/skills/redux-store-integration/SKILL.md +159 -0
- package/templates/rtk-query/.claude/skills/redux-store-integration/references/localStorage-persistence.md +70 -0
- package/templates/rtk-query/.claude/skills/redux-store-integration/references/middleware-patterns.md +82 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/SKILL.md +148 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/references/cache-strategies.md +96 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/references/endpoint-cookbook.md +145 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/references/optimistic-update.md +53 -0
- package/templates/rtk-query/README.md +84 -0
- package/templates/rtk-query/package.partial.json +7 -0
- package/templates/rtk-query/src/app/App.tsx +23 -0
- package/templates/rtk-query/src/axiosconfig/axiosInstance.ts +26 -0
- package/templates/rtk-query/src/axiosconfig/baseQuery.ts +72 -0
- package/templates/rtk-query/src/axiosconfig/interceptor.ts +42 -0
- package/templates/rtk-query/src/redux/invalidateCacheMiddleware.ts +20 -0
- package/templates/rtk-query/src/redux/reduxHooks.ts +10 -0
- package/templates/rtk-query/src/redux/rootReducer.ts +18 -0
- package/templates/rtk-query/src/redux/store.ts +36 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/SKILL.md +109 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/references/error-shape.md +89 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/references/full-code-walkthrough.md +121 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/references/notification-pattern.md +109 -0
- package/templates/tanstack-query/.claude/skills/constants-organization/SKILL.md +144 -0
- package/templates/tanstack-query/.claude/skills/constants-organization/references/example-files.md +111 -0
- package/templates/tanstack-query/.claude/skills/constants-organization/references/query-key-factories.md +129 -0
- package/templates/tanstack-query/.claude/skills/query-client-setup/SKILL.md +165 -0
- package/templates/tanstack-query/.claude/skills/query-client-setup/references/devtools.md +67 -0
- package/templates/tanstack-query/.claude/skills/query-client-setup/references/global-handlers.md +94 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/SKILL.md +142 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/references/audited-mutation.md +144 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/references/optimistic-update.md +102 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/references/service-cookbook.md +151 -0
- package/templates/tanstack-query/README.md +63 -0
- package/templates/tanstack-query/package.partial.json +8 -0
- package/templates/tanstack-query/src/api/axiosInstance.ts +20 -0
- package/templates/tanstack-query/src/api/http.ts +62 -0
- package/templates/tanstack-query/src/api/queryClient.ts +28 -0
- package/templates/tanstack-query/src/app/App.tsx +20 -0
- package/templates/tanstack-query/src/services/example.ts +32 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react-swc';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
// Vite config for BFSI starter. Notable:
|
|
6
|
+
// - SWC for fast transpile
|
|
7
|
+
// - Path alias '@/' → 'src/'
|
|
8
|
+
// - Server hardening headers (CSP set at deploy edge; dev runs without)
|
|
9
|
+
// - HMR exposed only on localhost
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
plugins: [react()],
|
|
12
|
+
resolve: {
|
|
13
|
+
alias: {
|
|
14
|
+
'@': path.resolve(__dirname, './src'),
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
server: {
|
|
18
|
+
port: 5173,
|
|
19
|
+
strictPort: true,
|
|
20
|
+
host: 'localhost',
|
|
21
|
+
headers: {
|
|
22
|
+
'X-Content-Type-Options': 'nosniff',
|
|
23
|
+
'X-Frame-Options': 'DENY',
|
|
24
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
25
|
+
'Permissions-Policy': 'geolocation=(), camera=(), microphone=()',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
build: {
|
|
29
|
+
target: 'es2022',
|
|
30
|
+
sourcemap: true,
|
|
31
|
+
rollupOptions: {
|
|
32
|
+
output: {
|
|
33
|
+
manualChunks: {
|
|
34
|
+
react: ['react', 'react-dom', 'react-router-dom'],
|
|
35
|
+
forms: ['react-hook-form', 'zod', '@hookform/resolvers'],
|
|
36
|
+
i18n: ['react-i18next', 'i18next'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
test: {
|
|
42
|
+
environment: 'jsdom',
|
|
43
|
+
setupFiles: ['./tests/setup.ts'],
|
|
44
|
+
globals: true,
|
|
45
|
+
include: ['src/**/*.test.{ts,tsx}', 'tests/**/*.test.{ts,tsx}'],
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: axios-auth
|
|
3
|
+
description: Configure the project's axios instance, response interceptor, and RTK Query baseQuery. Sets the auth token ONCE at login via setAuthToken() (not per-request), with response interceptor for notifications + 401 redirect. Use when the user mentions axios, API client, baseQuery, set token, login flow, 401 handling, response interceptor, error notification.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Axios + Auth Pattern
|
|
7
|
+
|
|
8
|
+
The HTTP layer is three files under `src/axiosconfig/` 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/axiosconfig/axiosInstance.ts` | Single shared `AxiosInstance` from `createAxios()` |
|
|
15
|
+
| `src/axiosconfig/interceptor.ts` | Response interceptor: notifications + 401 redirect. Side-effect import from `axiosInstance.ts`. |
|
|
16
|
+
| `src/axiosconfig/baseQuery.ts` | `axiosBaseQuery()` for RTK Query — wraps the instance |
|
|
17
|
+
| `@<scope>/core/http` | Exports `createAxios`, `setAuthToken`, `clearAuthToken`, `ApiError` |
|
|
18
|
+
|
|
19
|
+
## Workflow
|
|
20
|
+
|
|
21
|
+
### Step 1 — Set token at login (the canonical place)
|
|
22
|
+
|
|
23
|
+
Inside your login mutation's success path:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { setAuthToken } from '@<scope>/core/http';
|
|
27
|
+
import axiosInstance from '@/axiosconfig/axiosInstance';
|
|
28
|
+
|
|
29
|
+
// After login API returns { token, ... }:
|
|
30
|
+
setAuthToken(axiosInstance, response.token);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`setAuthToken` writes to `instance.defaults.headers.common.Authorization` (or whatever `authHeaderName` was set). Every subsequent request carries the header automatically — no per-request interceptor needed.
|
|
34
|
+
|
|
35
|
+
### Step 2 — Clear on logout / 401
|
|
36
|
+
|
|
37
|
+
`createAxios()` already 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).
|
|
38
|
+
|
|
39
|
+
Manual logout flow:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { clearAuthToken } from '@<scope>/core/http';
|
|
43
|
+
import axiosInstance from '@/axiosconfig/axiosInstance';
|
|
44
|
+
|
|
45
|
+
clearAuthToken(axiosInstance);
|
|
46
|
+
// then dispatch any session-cleanup action and navigate
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Step 3 — Wire notification dispatch in the interceptor (optional)
|
|
50
|
+
|
|
51
|
+
The scaffolded `interceptor.ts` has commented-out placeholders for notification dispatch. When you set up a Notification slice, uncomment + adapt:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import store from '@/redux/store';
|
|
55
|
+
import { setNotification } from '@/shared/Notification/slice';
|
|
56
|
+
|
|
57
|
+
// inside the response error handler:
|
|
58
|
+
store.dispatch(setNotification({ type: 'error', message: extractMessage(err) }));
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
See [`references/notification-wiring.md`](references/notification-wiring.md) for the full pattern.
|
|
62
|
+
|
|
63
|
+
### Step 4 — Use `axiosBaseQuery` in feature APIs
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { createApi } from '@reduxjs/toolkit/query/react';
|
|
67
|
+
import axiosBaseQuery from '@/axiosconfig/baseQuery';
|
|
68
|
+
|
|
69
|
+
export const fooApi = createApi({
|
|
70
|
+
reducerPath: 'fooApi',
|
|
71
|
+
baseQuery: axiosBaseQuery(),
|
|
72
|
+
tagTypes: ['Foo'],
|
|
73
|
+
endpoints: (builder) => ({
|
|
74
|
+
/* ... */
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Each endpoint can opt-in to success/error notifications via `showSuccessNotification` / `showFailureNotification` flags on the query object.
|
|
80
|
+
|
|
81
|
+
## Conventions enforced
|
|
82
|
+
|
|
83
|
+
- ❌ NEVER inject token via a `request.use` interceptor — use `setAuthToken` at login.
|
|
84
|
+
- ❌ NEVER read token from `localStorage` on every request.
|
|
85
|
+
- ❌ NEVER hardcode `Authorization: Bearer ...` anywhere.
|
|
86
|
+
- ✅ One axios instance per app — exported as the default from `axiosInstance.ts`.
|
|
87
|
+
- ✅ Side-effect import of `'./interceptor.js'` in `axiosInstance.ts` so response interceptors are always registered.
|
|
88
|
+
- ✅ All RTK Query APIs use `axiosBaseQuery()` (never the default `fetchBaseQuery`).
|
|
89
|
+
|
|
90
|
+
## Quick reference checklist
|
|
91
|
+
|
|
92
|
+
When adding API auth:
|
|
93
|
+
|
|
94
|
+
- [ ] Login mutation calls `setAuthToken(axiosInstance, token)` on success
|
|
95
|
+
- [ ] Logout / 401 path calls `clearAuthToken(axiosInstance)`
|
|
96
|
+
- [ ] Token never appears in `localStorage` (memory only, set on the instance)
|
|
97
|
+
- [ ] `onUnauthorized` callback in `axiosInstance.ts` redirects to `/login`
|
|
98
|
+
|
|
99
|
+
## References
|
|
100
|
+
|
|
101
|
+
- [`references/full-code-walkthrough.md`](references/full-code-walkthrough.md) — annotated walk through all three files in the scaffolded project
|
|
102
|
+
- [`references/notification-wiring.md`](references/notification-wiring.md) — how to wire `showSuccessNotification` / `showFailureNotification` flags to a Notification slice
|
|
103
|
+
- [`references/error-shape.md`](references/error-shape.md) — backend error contract (`ApiError` kinds, field-error format)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Error shape contract
|
|
2
|
+
|
|
3
|
+
The HTTP layer normalises errors so feature code can pattern-match instead of digging through raw axios errors.
|
|
4
|
+
|
|
5
|
+
## `ApiError` (from `@<scope>/core/http`)
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
class ApiError extends Error {
|
|
9
|
+
readonly kind: ApiErrorKind;
|
|
10
|
+
readonly status?: number;
|
|
11
|
+
readonly ref?: string;
|
|
12
|
+
readonly fieldErrors?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type ApiErrorKind =
|
|
16
|
+
| 'network' // no response, offline, connection refused
|
|
17
|
+
| 'timeout' // request exceeded timeout
|
|
18
|
+
| 'unauthorized' // 401
|
|
19
|
+
| 'forbidden' // 403
|
|
20
|
+
| 'not_found' // 404
|
|
21
|
+
| 'conflict' // 409
|
|
22
|
+
| 'validation' // 422 — fieldErrors populated
|
|
23
|
+
| 'rate_limited' // 429
|
|
24
|
+
| 'server_error' // 5xx
|
|
25
|
+
| 'cancelled' // user cancelled / abort signal
|
|
26
|
+
| 'unknown';
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`createAxios` attaches an error-mapping interceptor that converts every axios error into `ApiError`. Inside `axiosBaseQuery`, you'll catch this — re-throw is fine; RTK Query stores it in `endpoint.error`.
|
|
30
|
+
|
|
31
|
+
## Backend error envelope
|
|
32
|
+
|
|
33
|
+
The starter's `axiosBaseQuery` expects backends to return errors as:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"errors": [{ "detail": "Email is already taken" }]
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or for field-level (422):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"errors": {
|
|
46
|
+
"email": ["is already taken"],
|
|
47
|
+
"password": ["is too short"]
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
If your backend uses a different shape, override `extractMessage()` in `interceptor.ts` and adjust `axiosBaseQuery`'s error branch.
|
|
53
|
+
|
|
54
|
+
## Surfacing errors in containers
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import type { ApiError } from '@<scope>/core/http';
|
|
58
|
+
|
|
59
|
+
const [createFoo, { error, isError }] = useCreateFooMutation();
|
|
60
|
+
|
|
61
|
+
if (isError && error) {
|
|
62
|
+
const e = error as ApiError;
|
|
63
|
+
if (e.kind === 'validation' && e.fieldErrors) {
|
|
64
|
+
// Set field-level errors on RHF:
|
|
65
|
+
for (const [field, msg] of Object.entries(e.fieldErrors)) {
|
|
66
|
+
form.setError(field as keyof FormValues, { message: msg });
|
|
67
|
+
}
|
|
68
|
+
} else if (e.kind === 'unauthorized') {
|
|
69
|
+
// Already handled by axios's onUnauthorized — no action needed.
|
|
70
|
+
} else {
|
|
71
|
+
// Generic toast — interceptor handles this if showFailureNotification was true.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## NEVER expose to UI
|
|
77
|
+
|
|
78
|
+
Per the `bfsi-error-message` reference skill in the toolkit:
|
|
79
|
+
|
|
80
|
+
- ❌ `error.message` from raw axios — leaks "Network Error" / stack info
|
|
81
|
+
- ❌ `error.response.data.errors` raw — may contain SQL fragments, DB IDs
|
|
82
|
+
- ❌ HTTP status codes as user copy ("Error 500")
|
|
83
|
+
|
|
84
|
+
Use the safe-error mapping (`toSafeView` from `@<scope>/core/compliance`) to convert `ApiError.kind` into a user-facing toast title + description + ref code.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Axios + Auth — full code walkthrough
|
|
2
|
+
|
|
3
|
+
Annotated tour of the three files the scaffolder lays down.
|
|
4
|
+
|
|
5
|
+
## `src/axiosconfig/axiosInstance.ts`
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createAxios } from '@<scope>/core/http';
|
|
9
|
+
import { env } from '../env.js';
|
|
10
|
+
import './interceptor.js'; // side-effect: registers response interceptor
|
|
11
|
+
|
|
12
|
+
const axiosInstance = createAxios({
|
|
13
|
+
baseURL: env.VITE_API_BASE_URL,
|
|
14
|
+
timeoutMs: env.VITE_API_TIMEOUT_MS,
|
|
15
|
+
authHeaderName: env.VITE_AUTH_HEADER_NAME, // default 'Authorization'
|
|
16
|
+
snakeCaseBackend: false, // flip true for Rails/Python
|
|
17
|
+
onUnauthorized: () => {
|
|
18
|
+
if (typeof window !== 'undefined') {
|
|
19
|
+
window.location.href = '/login';
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export default axiosInstance;
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Key points:
|
|
28
|
+
|
|
29
|
+
- `createAxios` (from core) attaches the BFSI-grade interceptors: request-IDs, idempotency keys, error mapping to typed `ApiError`.
|
|
30
|
+
- The `onUnauthorized` callback fires AFTER `clearAuthToken` has already wiped the token off the instance.
|
|
31
|
+
- The side-effect `import './interceptor.js'` is critical — it registers the response interceptor when `axiosInstance` is first imported.
|
|
32
|
+
- `snakeCaseBackend: true` enables automatic snake↔camel transformation on bodies and responses.
|
|
33
|
+
|
|
34
|
+
## `src/axiosconfig/interceptor.ts`
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import type { AxiosError, AxiosResponse } from 'axios';
|
|
38
|
+
import axiosInstance from './axiosInstance.js';
|
|
39
|
+
// import store from '../redux/store.js';
|
|
40
|
+
// import { setNotification } from '../shared/Notification/slice.js';
|
|
41
|
+
|
|
42
|
+
export interface ApiErrorShape {
|
|
43
|
+
config?: { url?: string };
|
|
44
|
+
response?: {
|
|
45
|
+
status?: number;
|
|
46
|
+
data?: {
|
|
47
|
+
errors?: Array<{ detail?: string; details?: string }> | Record<string, string[]>;
|
|
48
|
+
message?: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
axiosInstance.interceptors.response.use(
|
|
54
|
+
(response: AxiosResponse) => response,
|
|
55
|
+
(error: AxiosError) => {
|
|
56
|
+
const err = error as unknown as ApiErrorShape;
|
|
57
|
+
const status = err.response?.status;
|
|
58
|
+
|
|
59
|
+
// Wire to your Notification slice when ready:
|
|
60
|
+
// store.dispatch(setNotification({ type: 'error', message: extractMessage(err) }));
|
|
61
|
+
|
|
62
|
+
if (status === 401) {
|
|
63
|
+
// createAxios's onUnauthorized has already fired (cleared token + navigated).
|
|
64
|
+
// Add any additional auth-cleanup here.
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return Promise.reject(err);
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Notes:
|
|
73
|
+
|
|
74
|
+
- This file ONLY adds the response side. Request-side concerns (auth header, X-Request-Id, Idempotency-Key) are owned by `createAxios`'s built-in interceptors.
|
|
75
|
+
- 401 is double-handled: once in `createAxios` (clears token) and once here (extra cleanup if needed).
|
|
76
|
+
- The notification dispatch is commented-out scaffolding; wire it when you create the Notification slice.
|
|
77
|
+
|
|
78
|
+
## `src/axiosconfig/baseQuery.ts`
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import type { AxiosRequestConfig, AxiosResponse, ResponseType } from 'axios';
|
|
82
|
+
import type { BaseQueryFn } from '@reduxjs/toolkit/query';
|
|
83
|
+
import axiosInstance from './axiosInstance.js';
|
|
84
|
+
import type { ApiErrorShape } from './interceptor.js';
|
|
85
|
+
|
|
86
|
+
const GET = 'GET' as const;
|
|
87
|
+
|
|
88
|
+
const axiosBaseQuery =
|
|
89
|
+
(): BaseQueryFn<
|
|
90
|
+
{
|
|
91
|
+
url: string;
|
|
92
|
+
method?: AxiosRequestConfig['method'];
|
|
93
|
+
data?: AxiosRequestConfig['data'];
|
|
94
|
+
params?: AxiosRequestConfig['params'];
|
|
95
|
+
headers?: AxiosRequestConfig['headers'];
|
|
96
|
+
showSuccessNotification?: boolean;
|
|
97
|
+
showFailureNotification?: boolean;
|
|
98
|
+
responseType?: ResponseType;
|
|
99
|
+
},
|
|
100
|
+
unknown,
|
|
101
|
+
unknown
|
|
102
|
+
> =>
|
|
103
|
+
async ({ url, method, data, params, headers, responseType }) => {
|
|
104
|
+
try {
|
|
105
|
+
const requestConfig: AxiosRequestConfig =
|
|
106
|
+
method === GET
|
|
107
|
+
? { url, method, params: params ?? data, headers }
|
|
108
|
+
: { url, method, data, params, headers };
|
|
109
|
+
|
|
110
|
+
if (responseType) requestConfig.responseType = responseType;
|
|
111
|
+
|
|
112
|
+
const result: AxiosResponse = await axiosInstance(requestConfig);
|
|
113
|
+
return { data: result.data };
|
|
114
|
+
} catch (axiosError) {
|
|
115
|
+
const error = axiosError as ApiErrorShape;
|
|
116
|
+
return {
|
|
117
|
+
error: {
|
|
118
|
+
status: error.response?.status,
|
|
119
|
+
data: error.response?.data?.errors,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default axiosBaseQuery;
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Notes:
|
|
129
|
+
|
|
130
|
+
- Endpoints pass `data` for both GET and non-GET — `axiosBaseQuery` routes it correctly (`params` for GET, `data` for others).
|
|
131
|
+
- `showSuccessNotification` / `showFailureNotification` are flags that get inspected here. Wiring goes to the Notification slice once that's set up.
|
|
132
|
+
- Returns the RTK Query `{ data }` / `{ error }` shape. Error.data is the raw `errors` payload — features format it for display.
|
|
133
|
+
|
|
134
|
+
## Order of imports matters
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
axiosInstance.ts ─── imports → interceptor.ts (side-effect)
|
|
138
|
+
│
|
|
139
|
+
▼ (default export)
|
|
140
|
+
baseQuery.ts ─── imports → axiosInstance (same singleton)
|
|
141
|
+
│
|
|
142
|
+
▼ (default export)
|
|
143
|
+
features/<X>/api.ts ─── createApi({ baseQuery: axiosBaseQuery() })
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Anyone calling `axiosInstance` (directly or via baseQuery) gets a fully-interceptor-wired instance because of the side-effect import chain.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Notification wiring
|
|
2
|
+
|
|
3
|
+
`axiosBaseQuery` and `interceptor.ts` both have commented-out hooks for dispatching toast notifications. This is how you wire them once you have a Notification slice.
|
|
4
|
+
|
|
5
|
+
## 1. Create the slice
|
|
6
|
+
|
|
7
|
+
`src/shared/Notification/slice.ts`:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
|
11
|
+
|
|
12
|
+
export type NotificationType = 'success' | 'error' | 'info' | 'warning';
|
|
13
|
+
|
|
14
|
+
interface NotificationState {
|
|
15
|
+
type: NotificationType | null;
|
|
16
|
+
message: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const initialState: NotificationState = { type: null, message: '' };
|
|
20
|
+
|
|
21
|
+
const notificationSlice = createSlice({
|
|
22
|
+
name: 'notification',
|
|
23
|
+
initialState,
|
|
24
|
+
reducers: {
|
|
25
|
+
setNotification: (
|
|
26
|
+
state,
|
|
27
|
+
action: PayloadAction<{ type: NotificationType; message: string }>,
|
|
28
|
+
) => {
|
|
29
|
+
state.type = action.payload.type;
|
|
30
|
+
state.message = action.payload.message;
|
|
31
|
+
},
|
|
32
|
+
clearNotification: (state) => {
|
|
33
|
+
state.type = null;
|
|
34
|
+
state.message = '';
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const { setNotification, clearNotification } = notificationSlice.actions;
|
|
40
|
+
export const notificationReducer = notificationSlice.reducer;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Register it in `src/redux/rootReducer.ts`:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { notificationReducer } from '@/shared/Notification/slice';
|
|
47
|
+
|
|
48
|
+
const rootReducer = combineReducers({
|
|
49
|
+
notification: notificationReducer,
|
|
50
|
+
// ...
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 2. Wire into the response interceptor
|
|
55
|
+
|
|
56
|
+
Uncomment in `src/axiosconfig/interceptor.ts`:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import store from '../redux/store.js';
|
|
60
|
+
import { setNotification } from '../shared/Notification/slice.js';
|
|
61
|
+
|
|
62
|
+
// inside the response error handler:
|
|
63
|
+
store.dispatch(
|
|
64
|
+
setNotification({
|
|
65
|
+
type: 'error',
|
|
66
|
+
message: extractMessage(err),
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A typical `extractMessage`:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
function extractMessage(err: ApiErrorShape): string {
|
|
75
|
+
const errors = err.response?.data?.errors;
|
|
76
|
+
if (Array.isArray(errors) && errors[0]) {
|
|
77
|
+
return errors[0].detail ?? errors[0].details ?? 'Request failed';
|
|
78
|
+
}
|
|
79
|
+
if (errors && typeof errors === 'object') {
|
|
80
|
+
const firstKey = Object.keys(errors)[0];
|
|
81
|
+
const firstVal = firstKey ? errors[firstKey]?.[0] : undefined;
|
|
82
|
+
return firstVal ?? 'Validation failed';
|
|
83
|
+
}
|
|
84
|
+
return err.response?.data?.message ?? 'Request failed';
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 3. Wire into `axiosBaseQuery`
|
|
89
|
+
|
|
90
|
+
Each endpoint sets `showSuccessNotification` / `showFailureNotification`. Adapt `baseQuery.ts`:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import store from '../redux/store.js';
|
|
94
|
+
import { setNotification } from '../shared/Notification/slice.js';
|
|
95
|
+
|
|
96
|
+
// success branch:
|
|
97
|
+
if (showSuccessNotification) {
|
|
98
|
+
store.dispatch(setNotification({ type: 'success', message: result.data?.message }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// failure branch:
|
|
102
|
+
if (showFailureNotification) {
|
|
103
|
+
store.dispatch(setNotification({ type: 'error', message: extractMessage(error) }));
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 4. Render the notifications
|
|
108
|
+
|
|
109
|
+
A small `<Notification />` component listens to the slice and renders toast UI (shadcn `<Toast>`, Sonner, etc.):
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import { useEffect } from 'react';
|
|
113
|
+
import { useAppSelector, useAppDispatch } from '@/redux/reduxHooks';
|
|
114
|
+
import { clearNotification } from './slice';
|
|
115
|
+
import { toast } from 'sonner';
|
|
116
|
+
|
|
117
|
+
export function Notification() {
|
|
118
|
+
const { type, message } = useAppSelector((s) => s.notification);
|
|
119
|
+
const dispatch = useAppDispatch();
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!type) return;
|
|
123
|
+
toast[type](message);
|
|
124
|
+
const t = setTimeout(() => dispatch(clearNotification()), 3000);
|
|
125
|
+
return () => clearTimeout(t);
|
|
126
|
+
}, [type, message, dispatch]);
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Mount it once in `App.tsx`:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
<Provider store={store}>
|
|
136
|
+
<Notification />
|
|
137
|
+
<BrowserRouter>
|
|
138
|
+
<AppRoutes />
|
|
139
|
+
</BrowserRouter>
|
|
140
|
+
</Provider>
|
|
141
|
+
```
|
|
@@ -0,0 +1,112 @@
|
|
|
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 constants, route paths, RTK Query tag types, validation regex patterns, and app-wide constants. Use when adding a new endpoint URL, route path, tag type, validation regex, or any other shared constant.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Constants Organisation
|
|
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
|
+
├── apiConstants.ts HTTP method names, API version, base URL helpers
|
|
15
|
+
├── appConstants.ts App-wide enums (ERROR / SUCCESS, storage keys, etc.)
|
|
16
|
+
├── urlConstants.ts Every API endpoint URL string
|
|
17
|
+
├── routeConstants.ts Every router path
|
|
18
|
+
├── tagTypes.ts RTK Query cache tag type names
|
|
19
|
+
└── regexConstants.ts Reusable validation regex
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## When to use which
|
|
23
|
+
|
|
24
|
+
| Adding … | Goes in | Naming convention |
|
|
25
|
+
| ---------------------------- | ------------------- | ---------------------------------------------- |
|
|
26
|
+
| A backend endpoint URL | `urlConstants.ts` | `<FEATURE>_URLS.<ACTION>` or `<FEATURE>_URL` |
|
|
27
|
+
| A frontend route path | `routeConstants.ts` | `ROUTES.<feature>.<view>` or `<FEATURE>_ROUTE` |
|
|
28
|
+
| An HTTP method (GET/POST/…) | `apiConstants.ts` | `GET`, `POST` constants |
|
|
29
|
+
| An RTK Query tag type | `tagTypes.ts` | `<Feature>Tag` literal in array |
|
|
30
|
+
| A validation regex | `regexConstants.ts` | `<FIELD>_REGEX` |
|
|
31
|
+
| A storage key | `appConstants.ts` | `<PURPOSE>_KEY` |
|
|
32
|
+
| An enum value used > 1 place | `appConstants.ts` | `<NAME>` (UPPER_SNAKE) |
|
|
33
|
+
|
|
34
|
+
## Workflow — adding a new endpoint
|
|
35
|
+
|
|
36
|
+
1. Open `src/utils/constants/urlConstants.ts`.
|
|
37
|
+
2. Add to the appropriate feature block (or create one):
|
|
38
|
+
```ts
|
|
39
|
+
export const KYC_URLS = {
|
|
40
|
+
LIST: '/kyc',
|
|
41
|
+
DETAIL: (id: string) => `/kyc/${id}`,
|
|
42
|
+
SUBMIT: '/kyc/submit',
|
|
43
|
+
} as const;
|
|
44
|
+
```
|
|
45
|
+
3. Reference from the feature's `api.ts`:
|
|
46
|
+
```ts
|
|
47
|
+
import { KYC_URLS } from '@/utils/constants/urlConstants';
|
|
48
|
+
```
|
|
49
|
+
4. If this endpoint introduces a new tag type, add it to `tagTypes.ts` and to the feature API's `tagTypes` array.
|
|
50
|
+
|
|
51
|
+
See [`references/example-files.md`](references/example-files.md) for full file templates.
|
|
52
|
+
|
|
53
|
+
## Workflow — adding a new route
|
|
54
|
+
|
|
55
|
+
1. Open `src/utils/constants/routeConstants.ts`.
|
|
56
|
+
2. Add the path:
|
|
57
|
+
```ts
|
|
58
|
+
export const ROUTES = {
|
|
59
|
+
kyc: {
|
|
60
|
+
list: '/kyc',
|
|
61
|
+
detail: '/kyc/:id',
|
|
62
|
+
submit: '/kyc/submit',
|
|
63
|
+
},
|
|
64
|
+
} as const;
|
|
65
|
+
```
|
|
66
|
+
3. Wire it in `src/routes/index.tsx`:
|
|
67
|
+
```tsx
|
|
68
|
+
<Route
|
|
69
|
+
path={ROUTES.kyc.list}
|
|
70
|
+
element={
|
|
71
|
+
<ProtectedRoute permission="kyc.view">
|
|
72
|
+
<KycList />
|
|
73
|
+
</ProtectedRoute>
|
|
74
|
+
}
|
|
75
|
+
/>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Workflow — adding a validation regex
|
|
79
|
+
|
|
80
|
+
Always centralise — never inline a regex in a Zod schema.
|
|
81
|
+
|
|
82
|
+
1. Open `src/utils/constants/regexConstants.ts`.
|
|
83
|
+
2. Add with documentation:
|
|
84
|
+
```ts
|
|
85
|
+
// PAN: 5 letters + 4 digits + 1 letter
|
|
86
|
+
export const PAN_REGEX = /^[A-Z]{5}[0-9]{4}[A-Z]$/;
|
|
87
|
+
```
|
|
88
|
+
3. Use in Zod:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { PAN_REGEX } from '@/utils/constants/regexConstants';
|
|
92
|
+
|
|
93
|
+
const schema = z.object({
|
|
94
|
+
pan: z.string().regex(PAN_REGEX, { message: 'Invalid PAN' }),
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The starter's `@<scope>/core/pii` already exports `PII_PATTERNS` with common BFSI regexes (PAN, Aadhaar, IFSC, mobile, email, etc.). Re-export those from `regexConstants.ts` rather than redefining.
|
|
99
|
+
|
|
100
|
+
## Conventions enforced
|
|
101
|
+
|
|
102
|
+
- ❌ NEVER hardcode a URL string in `api.ts` — always reference `urlConstants.ts`.
|
|
103
|
+
- ❌ NEVER inline a regex in component code or schema — always via `regexConstants.ts`.
|
|
104
|
+
- ❌ NEVER use a magic string for tag types — always via `tagTypes.ts`.
|
|
105
|
+
- ✅ Group by feature within each constants file (e.g. `KYC_URLS`, `LOAN_URLS`).
|
|
106
|
+
- ✅ Use `as const` on the exported objects so TypeScript infers literal types.
|
|
107
|
+
- ✅ Functions for dynamic paths (`DETAIL: (id) => ...`), strings for static ones.
|
|
108
|
+
|
|
109
|
+
## References
|
|
110
|
+
|
|
111
|
+
- [`references/example-files.md`](references/example-files.md) — full templates for each constants file
|
|
112
|
+
- [`references/tag-types-catalog.md`](references/tag-types-catalog.md) — RTK Query tag type naming + when each is invalidated
|