create-react-starter-kit 1.0.5 → 1.0.7
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/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +2 -1
- package/dist/cli-args.js.map +1 -1
- package/dist/generator/dependencies.d.ts.map +1 -1
- package/dist/generator/dependencies.js +5 -0
- package/dist/generator/dependencies.js.map +1 -1
- package/dist/generator/index.d.ts.map +1 -1
- package/dist/generator/index.js +5 -0
- package/dist/generator/index.js.map +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +8 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/templates/README.md.ejs +1 -1
- package/dist/templates/__when_apiLayer_axios__/src/services/api.ts.ejs +23 -0
- package/dist/templates/__when_apiLayer_axios__/src/services/http/client.ts.ejs +174 -0
- package/dist/templates/__when_apiLayer_axios__/src/services/http/errors.ts.ejs +81 -0
- package/dist/templates/__when_apiLayer_axios__/src/services/http/index.ts.ejs +5 -0
- package/dist/templates/__when_apiLayer_fetch__/src/services/api.ts.ejs +22 -0
- package/dist/templates/__when_apiLayer_fetch__/src/services/http/client.ts.ejs +195 -0
- package/dist/templates/__when_apiLayer_fetch__/src/services/http/errors.ts.ejs +81 -0
- package/dist/templates/__when_apiLayer_fetch__/src/services/http/index.ts.ejs +5 -0
- package/dist/templates/__when_buildTool_vite__/vite.config.ts.ejs +9 -1
- package/dist/templates/__when_buildTool_webpack__/webpack.config.js.ejs +1 -1
- package/dist/templates/__when_e2eTesting_cypress__/cypress.config.ts.ejs +2 -1
- package/dist/templates/__when_e2eTesting_playwright__/playwright.config.ts.ejs +4 -3
- package/dist/templates/__when_serverState_rtk-query__/src/store/postsApi.ts.ejs +28 -5
- package/dist/templates/__when_unitTesting_jest__/jest.config.json.ejs +8 -0
- package/dist/templates/__when_unitTesting_vitest__/tests/feedback.test.tsx.ejs +16 -0
- package/dist/templates/__when_unitTesting_vitest__/tests/posts.test.tsx.ejs +1 -3
- package/dist/templates/__when_unitTesting_vitest__/vitest.config.ts.ejs +31 -0
- package/dist/templates/docs/architecture.md.ejs +44 -25
- package/dist/templates/docs/best-practices.md.ejs +39 -3
- package/dist/templates/docs/examples/README.md.ejs +10 -10
- package/dist/templates/docs/examples/api.md.ejs +165 -8
- package/dist/templates/docs/examples/data-fetching.md.ejs +107 -6
- package/dist/templates/docs/examples/forms.md.ejs +66 -6
- package/dist/templates/docs/examples/state.md.ejs +50 -6
- package/dist/templates/docs/examples/styling.md.ejs +56 -8
- package/dist/templates/docs/examples/testing.md.ejs +63 -9
- package/dist/templates/docs/getting-started.md.ejs +63 -15
- package/dist/templates/docs/selected-stack.md.ejs +110 -8
- package/dist/templates/src/features/feedback/FeedbackForm.tsx.ejs +10 -0
- package/dist/templates/src/features/posts/PostsPanel.tsx.ejs +1 -1
- package/dist/templates/src/routes/PostsPage.tsx.ejs +7 -2
- package/dist/templates/src/types/index.ts.ejs +1 -1
- package/dist/templates/src/types/models.ts.ejs +18 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/templates/README.md.ejs +1 -1
- package/templates/__when_apiLayer_axios__/src/services/api.ts.ejs +23 -0
- package/templates/__when_apiLayer_axios__/src/services/http/client.ts.ejs +174 -0
- package/templates/__when_apiLayer_axios__/src/services/http/errors.ts.ejs +81 -0
- package/templates/__when_apiLayer_axios__/src/services/http/index.ts.ejs +5 -0
- package/templates/__when_apiLayer_fetch__/src/services/api.ts.ejs +22 -0
- package/templates/__when_apiLayer_fetch__/src/services/http/client.ts.ejs +195 -0
- package/templates/__when_apiLayer_fetch__/src/services/http/errors.ts.ejs +81 -0
- package/templates/__when_apiLayer_fetch__/src/services/http/index.ts.ejs +5 -0
- package/templates/__when_buildTool_vite__/vite.config.ts.ejs +9 -1
- package/templates/__when_buildTool_webpack__/webpack.config.js.ejs +1 -1
- package/templates/__when_e2eTesting_cypress__/cypress.config.ts.ejs +2 -1
- package/templates/__when_e2eTesting_playwright__/playwright.config.ts.ejs +4 -3
- package/templates/__when_serverState_rtk-query__/src/store/postsApi.ts.ejs +28 -5
- package/templates/__when_unitTesting_jest__/jest.config.json.ejs +8 -0
- package/templates/__when_unitTesting_vitest__/tests/feedback.test.tsx.ejs +16 -0
- package/templates/__when_unitTesting_vitest__/tests/posts.test.tsx.ejs +1 -3
- package/templates/__when_unitTesting_vitest__/vitest.config.ts.ejs +31 -0
- package/templates/docs/architecture.md.ejs +44 -25
- package/templates/docs/best-practices.md.ejs +39 -3
- package/templates/docs/examples/README.md.ejs +10 -10
- package/templates/docs/examples/api.md.ejs +165 -8
- package/templates/docs/examples/data-fetching.md.ejs +107 -6
- package/templates/docs/examples/forms.md.ejs +66 -6
- package/templates/docs/examples/state.md.ejs +50 -6
- package/templates/docs/examples/styling.md.ejs +56 -8
- package/templates/docs/examples/testing.md.ejs +63 -9
- package/templates/docs/getting-started.md.ejs +63 -15
- package/templates/docs/selected-stack.md.ejs +110 -8
- package/templates/src/features/feedback/FeedbackForm.tsx.ejs +10 -0
- package/templates/src/features/posts/PostsPanel.tsx.ejs +1 -1
- package/templates/src/routes/PostsPage.tsx.ejs +7 -2
- package/templates/src/types/index.ts.ejs +1 -1
- package/templates/src/types/models.ts.ejs +18 -0
- package/dist/templates/src/services/api.ts.ejs +0 -84
- package/templates/src/services/api.ts.ejs +0 -84
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
<% } -%>
|
|
5
5
|
"testEnvironment": "jsdom",
|
|
6
6
|
"setupFilesAfterEnv": ["<rootDir>/tests/setup.<%= ext %>"],
|
|
7
|
+
"testMatch": ["**/tests/**/*.test.[jt]s?(x)"],
|
|
8
|
+
"testPathIgnorePatterns": [
|
|
9
|
+
"/node_modules/",
|
|
10
|
+
"/dist/",
|
|
11
|
+
"/build/",
|
|
12
|
+
"/tests/e2e/",
|
|
13
|
+
"/e2e/"
|
|
14
|
+
],
|
|
7
15
|
"moduleNameMapper": {
|
|
8
16
|
"^@/(.*)$": "<rootDir>/src/$1",
|
|
9
17
|
"\\\\.(css|scss)$": "<rootDir>/tests/styleMock.<%= ext %>"
|
|
@@ -4,6 +4,19 @@ import userEvent from '@testing-library/user-event';
|
|
|
4
4
|
import { FeedbackForm } from '@/features/feedback/FeedbackForm';
|
|
5
5
|
import { renderWithProviders } from './test-utils';
|
|
6
6
|
|
|
7
|
+
<% if (serverState === 'rtk-query') { -%>
|
|
8
|
+
const createFeedback = vi.fn(() => ({
|
|
9
|
+
unwrap: vi.fn(async () => ({ id: 101 })),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock('@/store/postsApi', async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal<% if (isTypeScript) { %><typeof import('@/store/postsApi')><% } %>();
|
|
14
|
+
return {
|
|
15
|
+
...actual,
|
|
16
|
+
useCreateFeedbackMutation: () => [createFeedback, { isLoading: false }],
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
<% } else { -%>
|
|
7
20
|
vi.mock('@/services/api', async (importOriginal) => {
|
|
8
21
|
const actual = await importOriginal<% if (isTypeScript) { %><typeof import('@/services/api')><% } %>();
|
|
9
22
|
return {
|
|
@@ -11,6 +24,7 @@ vi.mock('@/services/api', async (importOriginal) => {
|
|
|
11
24
|
createFeedback: vi.fn(async () => ({ id: 101 })),
|
|
12
25
|
};
|
|
13
26
|
});
|
|
27
|
+
<% } -%>
|
|
14
28
|
|
|
15
29
|
describe('FeedbackForm', () => {
|
|
16
30
|
beforeEach(() => {
|
|
@@ -30,7 +44,9 @@ describe('FeedbackForm', () => {
|
|
|
30
44
|
|
|
31
45
|
it('submits valid feedback and shows success status', async () => {
|
|
32
46
|
const user = userEvent.setup();
|
|
47
|
+
<% if (serverState !== 'rtk-query') { -%>
|
|
33
48
|
const { createFeedback } = await import('@/services/api');
|
|
49
|
+
<% } -%>
|
|
34
50
|
renderWithProviders(<FeedbackForm />);
|
|
35
51
|
|
|
36
52
|
await user.type(screen.getByPlaceholderText('Ada Lovelace'), 'Ada Lovelace');
|
|
@@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';
|
|
|
4
4
|
import { PostsPanel } from '@/features/posts/PostsPanel';
|
|
5
5
|
import { renderWithProviders } from './test-utils';
|
|
6
6
|
|
|
7
|
+
<% if (serverState !== 'rtk-query') { -%>
|
|
7
8
|
vi.mock('@/services/api', async (importOriginal) => {
|
|
8
9
|
const actual = await importOriginal<% if (isTypeScript) { %><typeof import('@/services/api')><% } %>();
|
|
9
10
|
return {
|
|
@@ -14,9 +15,6 @@ vi.mock('@/services/api', async (importOriginal) => {
|
|
|
14
15
|
]),
|
|
15
16
|
};
|
|
16
17
|
});
|
|
17
|
-
|
|
18
|
-
<% if (serverState === 'rtk-query') { -%>
|
|
19
|
-
// RTK Query uses its own endpoint; mock still covers the transport used by other modes.
|
|
20
18
|
<% } -%>
|
|
21
19
|
|
|
22
20
|
describe('PostsPanel', () => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
import { defineConfig } from 'vitest/config';
|
|
5
|
+
|
|
6
|
+
const rootDir = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
// Standalone Vitest config for non-Vite build tools (Webpack, Parcel, etc.).
|
|
9
|
+
// Vite projects configure Vitest inside vite.config instead.
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
plugins: [react()],
|
|
12
|
+
resolve: {
|
|
13
|
+
alias: {
|
|
14
|
+
'@': path.resolve(rootDir, 'src'),
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
test: {
|
|
18
|
+
globals: true,
|
|
19
|
+
environment: 'jsdom',
|
|
20
|
+
setupFiles: './tests/setup.<%= ext %>',
|
|
21
|
+
include: ['tests/**/*.{test,spec}.{<%= isTypeScript ? 'ts,tsx' : 'js,jsx' %>}'],
|
|
22
|
+
exclude: [
|
|
23
|
+
'**/node_modules/**',
|
|
24
|
+
'**/dist/**',
|
|
25
|
+
'**/build/**',
|
|
26
|
+
'**/tests/e2e/**',
|
|
27
|
+
'**/e2e/**',
|
|
28
|
+
'**/*.e2e.*',
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -21,52 +21,71 @@ You selected a coherent default-friendly stack during scaffolding. The generator
|
|
|
21
21
|
|
|
22
22
|
```text
|
|
23
23
|
src/
|
|
24
|
-
main.<%= jsxExt %>
|
|
25
|
-
providers.<%= jsxExt %>
|
|
26
|
-
App.<%= jsxExt %>
|
|
24
|
+
main.<%= jsxExt %> # bootstrap + render
|
|
25
|
+
providers.<%= jsxExt %> # global providers (store, query, theme)
|
|
26
|
+
App.<%= jsxExt %> # route tree
|
|
27
27
|
assets/
|
|
28
|
-
components/
|
|
29
|
-
config/
|
|
30
|
-
features/
|
|
28
|
+
components/ # shared UI (layout/, ui/)
|
|
29
|
+
config/ # env + app config
|
|
30
|
+
features/ # domain modules (counter, posts, feedback)
|
|
31
31
|
hooks/
|
|
32
|
-
lib/
|
|
33
|
-
routes/
|
|
34
|
-
services/
|
|
35
|
-
store/
|
|
32
|
+
lib/ # validation, monitoring helpers
|
|
33
|
+
routes/ # page-level route components
|
|
34
|
+
services/ # HTTP client + domain API (when selected)
|
|
35
|
+
store/ # client state + RTK Query APIs (when selected)
|
|
36
36
|
styles/
|
|
37
|
-
types/
|
|
37
|
+
types/ # shared domain models
|
|
38
38
|
utils/
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
**Rules of thumb**
|
|
42
42
|
|
|
43
43
|
- Put new product work in `src/features/<domain>/`.
|
|
44
|
-
- Keep `components/`
|
|
44
|
+
- Keep `components/` presentational and reusable.
|
|
45
45
|
- Import with the `@/` alias.
|
|
46
46
|
- Prefer a feature's public surface over deep cross-feature imports.
|
|
47
|
+
- Keep providers thin — wiring belongs in `providers.<%= jsxExt %>`, business logic does not.
|
|
47
48
|
|
|
48
49
|
## Routes
|
|
49
50
|
|
|
50
|
-
| Path | Purpose |
|
|
51
|
-
|
|
52
|
-
| `/` | Home overview |
|
|
53
|
-
| `/counter` | Client state
|
|
54
|
-
| `/posts` | Server state
|
|
55
|
-
| `/feedback` | Forms + validation |
|
|
56
|
-
| `/about` | Orientation |
|
|
51
|
+
| Path | Purpose | Primary demo |
|
|
52
|
+
|------|---------|--------------|
|
|
53
|
+
| `/` | Home overview | Stack orientation |
|
|
54
|
+
| `/counter` | Client state | <%= stackLabels.stateManagement %> |
|
|
55
|
+
| `/posts` | Server state | <%= stackLabels.serverState %> |
|
|
56
|
+
| `/feedback` | Forms + validation | <%= stackLabels.forms %> |
|
|
57
|
+
| `/about` | Orientation | Architecture notes |
|
|
57
58
|
|
|
58
59
|
## Path aliases
|
|
59
60
|
|
|
60
|
-
`@/*` maps to `src/*` in TypeScript/JS config and in your bundler.
|
|
61
|
+
`@/*` maps to `src/*` in TypeScript/JS config and in your bundler. Prefer `@/features/...` over long relative paths.
|
|
61
62
|
|
|
62
63
|
## Runtime composition
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
```text
|
|
66
|
+
main → AppProviders → App (router) → layout → page → feature panels
|
|
67
|
+
```
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
Provider order matters: client store → server cache → UI theme.
|
|
67
70
|
|
|
68
71
|
## Boundaries
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
- **
|
|
72
|
-
-
|
|
73
|
+
<% if (serverState === 'rtk-query') { -%>
|
|
74
|
+
- **HTTP** for remote resources lives in RTK Query APIs (`src/store/*Api.<%= ext %>`), not in components.
|
|
75
|
+
- Use `fetchBaseQuery` + `prepareHeaders` for auth, base URL, and shared headers.
|
|
76
|
+
- Do **not** add a parallel Axios/Fetch client unless you have a clear non-RTK integration (uploads to a different host, third-party SDK, etc.).
|
|
77
|
+
<% } else { -%>
|
|
78
|
+
- **UI** should call domain helpers in `src/services/api.<%= ext %>`.
|
|
79
|
+
- **Transport** details (interceptors, tokens, timeouts) stay in `src/services/http/`.
|
|
80
|
+
- Components should not call `axios` / `fetch` directly.
|
|
81
|
+
<% } -%>
|
|
82
|
+
- **Schemas** live in `lib/validation` and are reused by forms and API payloads.
|
|
83
|
+
- **Env access** stays in `config/`, services/store, and `lib/monitoring`.
|
|
84
|
+
- **Client state** holds UI/ephemeral state; **server state** caches remote entities.
|
|
85
|
+
|
|
86
|
+
## Scaling tips
|
|
87
|
+
|
|
88
|
+
1. Split large features into `components/`, `hooks/`, and `api/` subfolders inside the feature.
|
|
89
|
+
2. Promote shared types into `src/types/` once two features need them.
|
|
90
|
+
3. Keep route files thin — they compose features, they should not own data logic.
|
|
91
|
+
4. Prefer explicit query keys / endpoint names over anonymous fetch calls.
|
|
@@ -2,9 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Code organization
|
|
4
4
|
|
|
5
|
-
- Colocate
|
|
5
|
+
- Colocate feature UI, hooks, and tests under `src/features/<domain>/`.
|
|
6
6
|
- Prefer small modules with clear exports over “utils dump” files.
|
|
7
7
|
- Name features after domains (`billing`, `auth`), not technical junk drawers.
|
|
8
|
+
- Keep route files as composers — they should not own fetching or form schemas.
|
|
9
|
+
|
|
10
|
+
## HTTP & data
|
|
11
|
+
|
|
12
|
+
<% if (serverState === 'rtk-query') { -%>
|
|
13
|
+
- Define one API slice per domain (or a carefully shared base API).
|
|
14
|
+
- Use `tagTypes` for cache invalidation instead of manual refetches when possible.
|
|
15
|
+
- Put auth in `prepareHeaders`.
|
|
16
|
+
- Prefer generated hooks in components; avoid dispatching raw endpoint actions unless needed.
|
|
17
|
+
<% } else { -%>
|
|
18
|
+
- Never call `axios` / `fetch` from React components — use `src/services/api.<%= ext %>`.
|
|
19
|
+
- Keep interceptors and error normalization in `src/services/http/`.
|
|
20
|
+
- Pass `AbortSignal` from query libraries into HTTP helpers.
|
|
21
|
+
<% } -%>
|
|
22
|
+
- Treat server cache and client UI state as separate concerns.
|
|
8
23
|
|
|
9
24
|
## Linting & formatting
|
|
10
25
|
|
|
@@ -13,16 +28,27 @@
|
|
|
13
28
|
- Husky + lint-staged formats staged files so diffs stay clean.
|
|
14
29
|
- commitlint enforces Conventional Commits on `git commit`.
|
|
15
30
|
|
|
31
|
+
**Commit style examples**
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
feat(posts): add pagination to posts query
|
|
35
|
+
fix(feedback): surface API validation errors
|
|
36
|
+
chore(deps): bump testing-library
|
|
37
|
+
```
|
|
38
|
+
|
|
16
39
|
## Environment variables
|
|
17
40
|
|
|
18
41
|
- Never commit `.env`.
|
|
19
|
-
- Prefix client-visible vars for your bundler.
|
|
42
|
+
- Prefix client-visible vars for your bundler (`VITE_*`).
|
|
20
43
|
- Validate required env at boot for production deploys.
|
|
44
|
+
- Document every variable in `.env.example` with a one-line purpose.
|
|
21
45
|
|
|
22
46
|
## Testing strategy
|
|
23
47
|
|
|
24
48
|
<% if (unitTesting === 'vitest' || unitTesting === 'jest') { -%>
|
|
25
49
|
- Unit/integration with **<%= stackLabels.unitTesting %>** + Testing Library for components.
|
|
50
|
+
- Prefer `getByRole` / label text over test IDs.
|
|
51
|
+
- Mock network at the domain boundary (`services/api` or RTK endpoints), not inside random UI helpers.
|
|
26
52
|
<% } else if (unitTesting === 'rtl') { -%>
|
|
27
53
|
- React Testing Library is installed — pair it with a runner of your choice.
|
|
28
54
|
<% } else { -%>
|
|
@@ -30,19 +56,27 @@
|
|
|
30
56
|
<% } -%>
|
|
31
57
|
<% if (e2eTesting !== 'none') { -%>
|
|
32
58
|
- E2E with **<%= stackLabels.e2eTesting %>** for critical user journeys (load home, submit form).
|
|
59
|
+
- Keep e2e thin: happy paths + one failure path per critical flow.
|
|
33
60
|
<% } -%>
|
|
34
61
|
<% if (visualTesting === 'storybook') { -%>
|
|
35
62
|
- Storybook for isolated UI development and visual review.
|
|
63
|
+
- Write stories for states (loading, empty, error, success), not only the happy path.
|
|
36
64
|
<% } else if (visualTesting === 'chromatic') { -%>
|
|
37
65
|
- Chromatic for visual regression against baselines.
|
|
38
66
|
<% } -%>
|
|
39
67
|
|
|
40
68
|
Aim for: fast unit tests on logic, fewer integration tests on features, thin e2e on happy paths.
|
|
41
69
|
|
|
70
|
+
## Accessibility
|
|
71
|
+
|
|
72
|
+
- Prefer semantic HTML (`button`, `label`, `nav`) before ARIA.
|
|
73
|
+
- Ensure keyboard focus order matches visual order.
|
|
74
|
+
- Pair every input with a visible label; surface errors with `aria-live` / `role="status"` where appropriate.
|
|
75
|
+
|
|
42
76
|
## Deployment notes
|
|
43
77
|
|
|
44
78
|
1. `<%= packageManager === 'npm' ? 'npm run' : packageManager %> build`
|
|
45
|
-
2. Serve `dist/`
|
|
79
|
+
2. Serve the bundler output (`dist/` / `build/`) behind HTTPS.
|
|
46
80
|
3. Configure SPA fallback to `index.html` for client routes.
|
|
47
81
|
4. Inject production env vars in the host (Vercel, Netlify, Cloudflare, nginx, etc.).
|
|
48
82
|
5. Enable error tracking DSN in production only.
|
|
@@ -54,3 +88,5 @@ Aim for: fast unit tests on logic, fewer integration tests on features, thin e2e
|
|
|
54
88
|
- [ ] Bundle analyzed for accidental secrets
|
|
55
89
|
- [ ] Monitoring verified with a test event
|
|
56
90
|
- [ ] Accessibility pass on primary flows
|
|
91
|
+
- [ ] Error states designed for `/posts` and `/feedback`
|
|
92
|
+
- [ ] Auth expiry path handled (401 → re-login / token refresh)
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# Examples index
|
|
2
2
|
|
|
3
|
-
Each
|
|
3
|
+
Each guide below explains **how to use** the wired library, **community best practices**, and **copy-paste examples** that match this project's folder layout.
|
|
4
4
|
|
|
5
|
-
| File | Topic |
|
|
6
|
-
|
|
7
|
-
| `state.md` | Client state patterns |
|
|
8
|
-
| `data-fetching.md` | Server state / caching |
|
|
9
|
-
| `forms.md` | Forms + validation |
|
|
10
|
-
| `styling.md` | Visual system |
|
|
11
|
-
| `api.md` | HTTP layer |
|
|
12
|
-
| `testing.md` | Unit / E2E / visual |
|
|
5
|
+
| File | Topic | Live route |
|
|
6
|
+
|------|-------|------------|
|
|
7
|
+
| `state.md` | Client state patterns | `/counter` |
|
|
8
|
+
| `data-fetching.md` | Server state / caching | `/posts` |
|
|
9
|
+
| `forms.md` | Forms + validation | `/feedback` |
|
|
10
|
+
| `styling.md` | Visual system | all pages |
|
|
11
|
+
| `api.md` | HTTP layer | used by posts/feedback |
|
|
12
|
+
| `testing.md` | Unit / E2E / visual | `tests/` |
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Start with the live route, then read the matching doc while you extend the feature.
|
|
@@ -2,16 +2,173 @@
|
|
|
2
2
|
|
|
3
3
|
**Choice:** <%= stackLabels.apiLayer %>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<% if (apiLayer === 'none') { -%>
|
|
6
|
+
## Why there is no Axios/Fetch client
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
You selected **RTK Query**, which already provides:
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
- a shared base query (`fetchBaseQuery`)
|
|
11
|
+
- caching, deduplication, and generated hooks
|
|
12
|
+
- a single place for headers via `prepareHeaders`
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
- Surface user-friendly errors in UI; log technical details to monitoring.
|
|
13
|
-
- Do not scatter `fetch`/`axios` calls across components.
|
|
14
|
+
Adding Axios/Fetch beside RTK Query usually creates **two HTTP stacks**, duplicated auth logic, and inconsistent error handling.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
**Where HTTP lives:** `src/store/postsApi.<%= ext %>`
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
### Extending RTK Query
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// add to endpoints
|
|
22
|
+
getPostById: builder.query({
|
|
23
|
+
query: (id) => `/posts/${id}`,
|
|
24
|
+
providesTags: (_result, _error, id) => [{ type: 'Posts', id }],
|
|
25
|
+
}),
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Auth header pattern
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
prepareHeaders: (headers) => {
|
|
32
|
+
const token = localStorage.getItem('access_token');
|
|
33
|
+
if (token) headers.set('Authorization', `Bearer ${token}`);
|
|
34
|
+
return headers;
|
|
35
|
+
},
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### When you might still add Axios/Fetch later
|
|
39
|
+
|
|
40
|
+
- Uploading to a third-party host that RTK should not cache
|
|
41
|
+
- Streaming / non-JSON APIs
|
|
42
|
+
- A legacy SDK that must use its own client
|
|
43
|
+
|
|
44
|
+
If that happens, isolate it under `src/services/` and keep RTK as the default for app REST resources.
|
|
45
|
+
|
|
46
|
+
See `docs/examples/data-fetching.md` for UI usage.
|
|
47
|
+
<% } else if (apiLayer === 'axios') { -%>
|
|
48
|
+
## Layout
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
src/services/
|
|
52
|
+
http/
|
|
53
|
+
client.<%= ext %> # axios instance + interceptors
|
|
54
|
+
errors.<%= ext %> # ApiError normalization
|
|
55
|
+
index.<%= ext %> # public exports
|
|
56
|
+
api.<%= ext %> # domain methods (fetchPosts, createFeedback)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## How to use
|
|
60
|
+
|
|
61
|
+
### From a query library
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { fetchPosts, createFeedback } from '@/services/api';
|
|
65
|
+
|
|
66
|
+
// TanStack Query
|
|
67
|
+
queryFn: ({ signal }) => fetchPosts(signal)
|
|
68
|
+
|
|
69
|
+
// mutation
|
|
70
|
+
await createFeedback({ name, email, message })
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Direct low-level calls (rare)
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { apiGet, apiPost, ApiError } from '@/services/http';
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const post = await apiGet(`/posts/${id}`);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error instanceof ApiError && error.isUnauthorized) {
|
|
82
|
+
// redirect to login / refresh token
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## What the Axios setup includes
|
|
88
|
+
|
|
89
|
+
| Concern | Implementation |
|
|
90
|
+
|---------|----------------|
|
|
91
|
+
| Base URL | `env.apiBaseUrl` |
|
|
92
|
+
| Timeout | 15s |
|
|
93
|
+
| Auth | `Authorization: Bearer <token>` from `localStorage.access_token` |
|
|
94
|
+
| Request tracing | `X-Request-Id` on every request |
|
|
95
|
+
| Errors | Normalized `ApiError` with `status`, `code`, `requestId` |
|
|
96
|
+
| 401 handling | Dispatches `auth:unauthorized` browser event |
|
|
97
|
+
| Verbs | `apiGet` / `apiPost` / `apiPut` / `apiPatch` / `apiDelete` |
|
|
98
|
+
| Cancellation | Pass `{ signal }` from AbortController / query libs |
|
|
99
|
+
|
|
100
|
+
## Best practices
|
|
101
|
+
|
|
102
|
+
- **Components never import axios.** Use domain helpers or at least `@/services/http`.
|
|
103
|
+
- Keep token reads inside the HTTP layer (`getAccessToken`), not in every feature.
|
|
104
|
+
- Listen once for `auth:unauthorized` near the app shell to centralize logout/redirect.
|
|
105
|
+
- Type responses at the boundary (`apiGet<Post[]>`).
|
|
106
|
+
- Log technical details; show user-friendly messages in UI.
|
|
107
|
+
- Prefer adding methods to `api.<%= ext %>` over spreading URLs across features.
|
|
108
|
+
|
|
109
|
+
## Extending with a refresh-token flow
|
|
110
|
+
|
|
111
|
+
1. Detect `ApiError.isUnauthorized` in the response interceptor.
|
|
112
|
+
2. Queue in-flight requests while refreshing.
|
|
113
|
+
3. Retry the original request with the new access token.
|
|
114
|
+
4. Fail the queue and redirect if refresh fails.
|
|
115
|
+
|
|
116
|
+
Keep that logic in `http/client.<%= ext %>` so features stay unaware.
|
|
117
|
+
|
|
118
|
+
## Example: add a new domain method
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// src/services/api.ts
|
|
122
|
+
export async function fetchPost(id: number, signal?: AbortSignal) {
|
|
123
|
+
return apiGet<Post>(`/posts/${id}`, { signal });
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
<% } else { -%>
|
|
127
|
+
## Layout
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
src/services/
|
|
131
|
+
http/
|
|
132
|
+
client.<%= ext %> # fetch wrapper + timeout/auth/errors
|
|
133
|
+
errors.<%= ext %> # ApiError normalization
|
|
134
|
+
index.<%= ext %>
|
|
135
|
+
api.<%= ext %> # domain methods
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## How to use
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { fetchPosts, createFeedback } from '@/services/api';
|
|
142
|
+
import { apiGet, ApiError } from '@/services/http';
|
|
143
|
+
|
|
144
|
+
await fetchPosts(signal);
|
|
145
|
+
await createFeedback(payload);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
await apiGet('/posts/1');
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error instanceof ApiError && error.isRetryable) {
|
|
151
|
+
// show retry UI
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## What the Fetch setup includes
|
|
157
|
+
|
|
158
|
+
| Concern | Implementation |
|
|
159
|
+
|---------|----------------|
|
|
160
|
+
| Base URL | `env.apiBaseUrl` |
|
|
161
|
+
| Timeout | AbortController (15s default) |
|
|
162
|
+
| Auth | Bearer token from `localStorage.access_token` |
|
|
163
|
+
| Request tracing | `X-Request-Id` |
|
|
164
|
+
| Errors | Normalized `ApiError` |
|
|
165
|
+
| 401 handling | `auth:unauthorized` event |
|
|
166
|
+
| Verbs | get/post/put/patch/delete helpers |
|
|
167
|
+
|
|
168
|
+
## Best practices
|
|
169
|
+
|
|
170
|
+
- Prefer domain helpers over raw `request()` in components.
|
|
171
|
+
- Pass `signal` from TanStack Query / user cancellation.
|
|
172
|
+
- Keep JSON as the default; use `rawBody: true` for `FormData` uploads.
|
|
173
|
+
- Centralize auth and error mapping — do not re-implement per feature.
|
|
174
|
+
<% } -%>
|
|
@@ -1,18 +1,119 @@
|
|
|
1
1
|
# Data fetching example
|
|
2
2
|
|
|
3
3
|
**Library:** <%= stackLabels.serverState %>
|
|
4
|
+
<% if (apiLayer !== 'none') { -%>
|
|
4
5
|
**Transport:** <%= stackLabels.apiLayer %>
|
|
6
|
+
<% } else { -%>
|
|
7
|
+
**Transport:** RTK Query `fetchBaseQuery` (no separate Axios/Fetch client)
|
|
8
|
+
<% } -%>
|
|
5
9
|
|
|
6
|
-
**Live demo:**
|
|
10
|
+
**Live demo:** `/posts` → `src/features/posts/PostsPanel.<%= jsxExt %>`
|
|
7
11
|
|
|
8
12
|
## When to use
|
|
9
13
|
|
|
10
|
-
Any remote read/write that benefits from caching, deduplication, retries, or background refresh
|
|
14
|
+
Any remote read/write that benefits from caching, deduplication, retries, or background refresh:
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
- lists and detail pages
|
|
17
|
+
- polling dashboards
|
|
18
|
+
- mutations that should refresh related queries
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
## How to use in this project
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
<% if (serverState === 'rtk-query') { -%>
|
|
23
|
+
### 1. Define endpoints
|
|
17
24
|
|
|
18
|
-
See `src/
|
|
25
|
+
See `src/store/postsApi.<%= ext %>`:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
export const postsApi = createApi({
|
|
29
|
+
reducerPath: 'postsApi',
|
|
30
|
+
baseQuery: fetchBaseQuery({
|
|
31
|
+
baseUrl: env.apiBaseUrl,
|
|
32
|
+
prepareHeaders: (headers) => {
|
|
33
|
+
const token = localStorage.getItem('access_token');
|
|
34
|
+
if (token) headers.set('Authorization', `Bearer ${token}`);
|
|
35
|
+
return headers;
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
tagTypes: ['Posts'],
|
|
39
|
+
endpoints: (builder) => ({
|
|
40
|
+
getPosts: builder.query({
|
|
41
|
+
query: () => '/posts?_limit=5',
|
|
42
|
+
providesTags: ['Posts'],
|
|
43
|
+
}),
|
|
44
|
+
createFeedback: builder.mutation({
|
|
45
|
+
query: (body) => ({ url: '/posts', method: 'POST', body }),
|
|
46
|
+
invalidatesTags: ['Posts'],
|
|
47
|
+
}),
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 2. Consume hooks in UI
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
const { data, isLoading, error, refetch, isFetching } = useGetPostsQuery();
|
|
56
|
+
const [createFeedback] = useCreateFeedbackMutation();
|
|
57
|
+
await createFeedback(values).unwrap();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Best practices (RTK Query)
|
|
61
|
+
|
|
62
|
+
- Prefer **tags** over manual `refetch()` for coherence after writes.
|
|
63
|
+
- Keep `prepareHeaders` as the single place for auth.
|
|
64
|
+
- Split large APIs with `injectEndpoints` as the app grows.
|
|
65
|
+
- Use `transformResponse` for shaping DTO → UI models at the boundary.
|
|
66
|
+
- Handle errors with `error` from the hook; surface user-friendly messages in UI.
|
|
67
|
+
<% } else if (serverState === 'tanstack-query') { -%>
|
|
68
|
+
### Query example
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
const { data, isLoading, error, refetch } = useQuery({
|
|
72
|
+
queryKey: ['posts'],
|
|
73
|
+
queryFn: ({ signal }) => fetchPosts(signal),
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`fetchPosts` lives in `src/services/api.<%= ext %>` and already goes through the shared HTTP client.
|
|
78
|
+
|
|
79
|
+
### Mutation example
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
const mutation = useMutation({
|
|
83
|
+
mutationFn: createFeedback,
|
|
84
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }),
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Best practices (TanStack Query)
|
|
89
|
+
|
|
90
|
+
- Hierarchical keys: `['posts']`, `['posts', postId]`, `['users', userId, 'posts']`
|
|
91
|
+
- Always accept/pass `AbortSignal` for cancellable requests
|
|
92
|
+
- Set sensible `staleTime` (already defaulted in `providers.<%= jsxExt %>`)
|
|
93
|
+
- Keep `queryFn` pure: no React state writes inside it
|
|
94
|
+
<% } else if (serverState === 'swr') { -%>
|
|
95
|
+
### Example
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
const { data, error, isLoading, mutate } = useSWR('/posts?_limit=5');
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The shared fetcher in `providers.<%= jsxExt %>` calls `apiGet`.
|
|
102
|
+
|
|
103
|
+
### Best practices (SWR)
|
|
104
|
+
|
|
105
|
+
- Use stable keys (URL or serialized params)
|
|
106
|
+
- Call `mutate` after writes to revalidate
|
|
107
|
+
- Prefer the global fetcher; override per-hook only when necessary
|
|
108
|
+
<% } else { -%>
|
|
109
|
+
Manual fetching is used in the posts panel. As soon as you add caching needs, introduce TanStack Query, SWR, or RTK Query.
|
|
110
|
+
<% } -%>
|
|
111
|
+
|
|
112
|
+
## Best practice (all libraries)
|
|
113
|
+
|
|
114
|
+
Separate **server cache** from **client UI state**. Key queries by resource identity, not by component instance.
|
|
115
|
+
|
|
116
|
+
## Related docs
|
|
117
|
+
|
|
118
|
+
- HTTP transport details: `docs/examples/api.md`
|
|
119
|
+
- Architecture boundaries: `docs/architecture.md`
|