Skip to content

Commit af24592

Browse files
committed
feat: loading/error-boundary DX, bundle analyzer, golden-paths doc and CLAUDE.md update
1 parent 47d1bd2 commit af24592

9 files changed

Lines changed: 8383 additions & 2815 deletions

File tree

CLAUDE.md

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,19 @@ Shared enums in `src/common/enums`: `Role`, `SortOrder`, `CustomTaskStatus`, `Ta
9494
- Query: `extends PaginationDto` + `@Filterable` decorators (`@FilterSearch`, `@FilterExact`, `@FilterBoolean`, `@FilterDateFrom`, `@FilterDateTo`, `@FilterIn`, `@FilterBetween`)
9595
- Response: `@ApiProperty` on exposed fields
9696

97-
### Entity Registration (CRITICAL)
97+
### Entity Registration
9898

99-
New entities MUST be added to BOTH arrays in `apps/backend/src/database/database.module.ts` — failure causes 500 errors.
99+
`autoLoadEntities: true` is enabled, so a new entity is picked up from any
100+
`TypeOrmModule.forFeature([...])` — register it in its **feature module** only.
101+
Never add it to a `forRoot` entities array (there isn't one anymore).
102+
103+
### Error Contract & Logging
104+
105+
- All errors share ONE envelope: `{ success: false, error: { statusCode, message,
106+
errors?, correlationId, timestamp, path } }` — emitted identically by
107+
`ResponseService.error()` and `HttpExceptionFilter`.
108+
- Every response carries an `x-correlation-id` header (pino logs the same id) —
109+
this is the trace key for debugging a reported error.
100110

101111
### i18n
102112

@@ -118,8 +128,11 @@ NEVER use `synchronize: true` in production.
118128

119129
### Types & Enums
120130

121-
- **ALWAYS** import from `@myorg/api-client` — NEVER create custom interfaces or hardcode enum strings
122-
- Regenerate after backend changes: `pnpm generate:api`
131+
- The API client is **orval-generated** (types + Zod + React Query hooks) from the
132+
backend OpenAPI spec. **ALWAYS** import types/enums from `@myorg/api-client`
133+
NEVER create custom interfaces or hardcode enum strings.
134+
- Regenerate after backend changes: `pnpm generate:api` (offline — runs the
135+
DB-independent generator into the committed `openapi.json`, then orval).
123136

124137
### Forms
125138

@@ -143,8 +156,24 @@ All private pages: `SectionContainer` + `PageHeader` + `<div className="p-4 lg:p
143156

144157
### Server Actions
145158

146-
- Place in `_actions/` folders (excluded from routing)
147-
- Use `revalidatePath()` after mutations
159+
- Call orval-generated functions (e.g. `usersQueryControllerFindAll(params, cfg)`)
160+
and hooks (`useUsersCommandControllerCreate`) — NEVER hand-write fetch/axios.
161+
- In Server Components / Server Actions, pass `await getServerRequestConfig()`
162+
(from `@myorg/api-client/server`) as the last arg so cookies + locale are sent.
163+
- Wrap every Server Action body in `safeAction(...)` — it returns the typed
164+
`{ success, data, message, error }` envelope and never throws on network errors.
165+
Errors carry `error.correlationId` (matches the backend `x-correlation-id` header).
166+
- Place in `_actions/` folders (excluded from routing); `revalidatePath()` after a
167+
successful mutation.
168+
169+
### Testing
170+
171+
- **Vitest** for unit/component tests (`*.spec.ts` backend, `*.test.tsx` web),
172+
**Playwright** for E2E (`apps/web/e2e/`). `pnpm test` runs them via turbo.
173+
- Backend: test Actions in isolation with a mocked repository — see
174+
`create-user.action.spec.ts` (the golden blueprint). No DB needed.
175+
- Web: drive components like a user (type + submit) and assert behaviour — see
176+
`universal-form.test.tsx`.
148177

149178
### Routing
150179

@@ -173,7 +202,7 @@ app/[locale]/(main)/
173202
11. `@I18n()` for messages
174203
12. Soft deletes, UUID keys, indexes on queried columns
175204
13. `QueryHelper.paginate()` for pagination
176-
14. Register entities in `database.module.ts` (both arrays)
205+
14. Register a new entity in its module's `TypeOrmModule.forFeature([...])``autoLoadEntities` handles the connection (no more dual-array registration)
177206
15. Import types/enums from `@myorg/api-client` on frontend
178207
16. `UniversalForm` + Zod for forms
179208
17. `date-fns`, `ts-pattern`, `SectionContainer` + `PageHeader`
@@ -187,7 +216,7 @@ app/[locale]/(main)/
187216
4. `synchronize: true` in production
188217
5. Return raw entities (use ResponseService/DTOs)
189218
6. N+1 queries, magic numbers/strings
190-
7. Forget entity registration in `database.module.ts`
219+
7. Manually list entities in the `forRoot` array (use `forFeature` + `autoLoadEntities`)
191220
8. Inline enum arrays in Swagger (creates duplicates)
192221
9. Custom interfaces/types on frontend for API data
193222
10. Hardcode enum string values on frontend

apps/web/next.config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os from 'node:os';
22
import type { NextConfig } from 'next';
33
import createNextIntlPlugin from 'next-intl/plugin';
4+
import bundleAnalyzer from '@next/bundle-analyzer';
45

56
function lanIps(): string[] {
67
return Object.values(os.networkInterfaces())
@@ -35,4 +36,7 @@ const nextConfig: NextConfig = {
3536
};
3637

3738
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
38-
export default withNextIntl(nextConfig);
39+
const withBundleAnalyzer = bundleAnalyzer({
40+
enabled: process.env.ANALYZE === 'true',
41+
});
42+
export default withBundleAnalyzer(withNextIntl(nextConfig));

apps/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"scripts": {
66
"dev": "next dev --turbopack -H 0.0.0.0",
77
"build": "next build",
8+
"analyze": "ANALYZE=true next build",
89
"start": "next start",
910
"lint": "eslint . --max-warnings=0",
1011
"typecheck": "tsc --noEmit",
@@ -37,6 +38,7 @@
3738
"zod": "catalog:"
3839
},
3940
"devDependencies": {
41+
"@next/bundle-analyzer": "catalog:next",
4042
"@playwright/test": "^1.60.0",
4143
"@tailwindcss/postcss": "catalog:",
4244
"@testing-library/jest-dom": "^6.9.1",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Route-level loading UI (golden path). Next streams this instantly while a
3+
* Server Component awaits data, so navigation never blocks on the network.
4+
* Clone this file into any route segment that fetches.
5+
*/
6+
export default function Loading() {
7+
return (
8+
<div className="space-y-4 p-4 lg:p-12">
9+
<div className="h-8 w-48 animate-pulse rounded-md bg-muted" />
10+
<div className="space-y-2">
11+
{Array.from({ length: 6 }).map((_, i) => (
12+
<div
13+
key={i}
14+
className="h-12 w-full animate-pulse rounded-md bg-muted"
15+
/>
16+
))}
17+
</div>
18+
</div>
19+
);
20+
}

apps/web/src/app/error.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ export default function Error({ error, reset }: ErrorProps) {
8484
.
8585
</p>
8686
</div>
87+
88+
{/* Always-visible reference so users can quote it to support and it
89+
can be traced to the server logs (Next error digest). */}
90+
{error.digest && (
91+
<p className="text-center text-xs text-gray-400">
92+
Reference: {error.digest}
93+
</p>
94+
)}
8795
</CardContent>
8896
</Card>
8997
</div>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use client';
2+
3+
import { Component, type ReactNode } from 'react';
4+
5+
interface ErrorBoundaryProps {
6+
children: ReactNode;
7+
/** Rendered instead of the crashed subtree. */
8+
fallback?: ReactNode;
9+
}
10+
11+
interface ErrorBoundaryState {
12+
hasError: boolean;
13+
}
14+
15+
/**
16+
* Localized error boundary for widget-level isolation: wrap a panel so one
17+
* crashing component degrades to a fallback instead of taking down the page.
18+
* (Route-level errors are handled by `error.tsx`; this is for sub-trees.)
19+
*
20+
* @example
21+
* <ErrorBoundary fallback={<p>Couldn't load stats.</p>}>
22+
* <StatsWidget />
23+
* </ErrorBoundary>
24+
*/
25+
export class ErrorBoundary extends Component<
26+
ErrorBoundaryProps,
27+
ErrorBoundaryState
28+
> {
29+
override state: ErrorBoundaryState = { hasError: false };
30+
31+
static getDerivedStateFromError(): ErrorBoundaryState {
32+
return { hasError: true };
33+
}
34+
35+
override componentDidCatch(error: unknown): void {
36+
console.error('ErrorBoundary caught:', error);
37+
}
38+
39+
override render(): ReactNode {
40+
if (this.state.hasError) {
41+
return (
42+
this.props.fallback ?? (
43+
<div className="rounded-md border border-destructive/40 bg-destructive/5 p-4 text-sm text-destructive">
44+
Something went wrong in this section.
45+
</div>
46+
)
47+
);
48+
}
49+
return this.props.children;
50+
}
51+
}

docs/HARDENING_CHECKLIST.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ Legend: `[ ]` todo · `[~]` in progress · `[x]` done · `[!]` blocked (needs de
5757
- [x] 5.1 `safeAction` wrapper in api-client (no-throw, returns typed envelope)
5858
- [x] 5.2 Rewrote all server actions + pages + auth/actions.ts → orval functions + `safeAction` + `getServerRequestConfig` (~14 files). Found/fixed orval generating POST as a query (removed bad `query` override); fixed a bearer-auth spec bug; smart envelope transformer (skip already-enveloped DTOs); `ApiErrorDto.message` → plain `string`.
5959
- [x] 5.3 Replaced hand-written `UpdateProfileData`/`ChangePasswordData` with aliases to generated DTOs
60-
- [ ] 5.4 Query-key factory + standardized mutation invalidation
61-
- [ ] 5.5 `loading.tsx` golden path + reusable `<ErrorBoundary>` + correlation ID in `error.tsx`
60+
- [x] 5.4 Query keys — orval generates `getXxxQueryKey()` + manages hook keys; invalidate via `queryClient.invalidateQueries({ queryKey: getXxxQueryKey() })`. (No separate factory needed — provided by generation.)
61+
- [x] 5.5 `loading.tsx` golden path + reusable `<ErrorBoundary>` (`components/error-boundary.tsx`) + always-visible error `digest`/correlation reference in `error.tsx`
6262
- [x] 5.6 Fixed all frontend `any` (query-client, universal-form generic-forwardRef, table-filters, locale cast, health route, sidebar). **web typecheck 0, lint 0, `next build`**
6363

6464
## EPIC 6 — Testing (review #4)
@@ -70,10 +70,10 @@ Legend: `[ ]` todo · `[~]` in progress · `[x]` done · `[!]` blocked (needs de
7070

7171
## EPIC 7 — AI-native docs & polish (review #1.1, #1.3, #3.3, #5.3)
7272

73-
- [ ] 7.1 `docs/golden-paths/` index (one perfect example per artifact) linked from CLAUDE.md
74-
- [ ] 7.2 Update CLAUDE.md to match new patterns (safeAction, autoload, orval/zod, testing)
73+
- [x] 7.1 `docs/golden-paths.md`one exemplar per artifact (Action, controller, server action, form, tests, E2E)
74+
- [x] 7.2 Updated CLAUDE.md: orval client + `safeAction` + `getServerRequestConfig`, `autoLoadEntities` (footgun removed), unified error contract + correlation IDs, Testing section
7575
- [x] 7.3 Replace `console.log` banner in `main.ts` with pino logger (done early — backend pass)
76-
- [ ] 7.4 `pnpm analyze` wiring for `@next/bundle-analyzer`
76+
- [x] 7.4 `pnpm --filter web analyze` wired via `@next/bundle-analyzer` (gated on `ANALYZE=true`)
7777

7878
## EPIC 8 — Final verification
7979

docs/golden-paths.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Golden Paths
2+
3+
One flawless, verified example per artifact. When adding a feature, **clone the
4+
nearest example** below rather than inventing a pattern — every file here passes
5+
the full typecheck + lint + test gate.
6+
7+
## Backend (NestJS)
8+
9+
| Artifact | Golden file |
10+
| -------------------- | ------------------------------------------------------------------------ |
11+
| Action (logic) | `apps/backend/src/modules/users/actions/create-user.action.ts` |
12+
| Command controller | `apps/backend/src/modules/users/controllers/users-command.controller.ts` |
13+
| Query controller | `apps/backend/src/modules/users/controllers/users-query.controller.ts` |
14+
| Service (DB) | `apps/backend/src/modules/users/users.service.ts` |
15+
| Entity | `apps/backend/src/modules/users/entities/user.entity.ts` |
16+
| Create / Query DTO | `apps/backend/src/modules/users/dto/` |
17+
| Error envelope | `apps/backend/src/common/filters/http-exception.filter.ts` |
18+
| **Action unit test** | `apps/backend/src/modules/users/actions/create-user.action.spec.ts` |
19+
20+
Reference module: `apps/backend/src/modules/users/`.
21+
22+
## Frontend (Next.js)
23+
24+
| Artifact | Golden file |
25+
| ---------------------- | ------------------------------------------------------------------------------------------------ |
26+
| Server Action | `apps/web/src/app/[locale]/(main)/(private)/profile/change-password/_actions/change-password.ts` |
27+
| Server Component fetch | `apps/web/src/app/[locale]/(main)/(private)/admin/users/(list)/page.tsx` |
28+
| Client form (mutation) | `apps/web/src/app/[locale]/(main)/(private)/admin/add-user-client/_components/add-user-form.tsx` |
29+
| Form primitive | `apps/web/src/components/forms/universal-form.tsx` |
30+
| Route loading skeleton | `apps/web/src/app/[locale]/(main)/(private)/loading.tsx` |
31+
| Route error boundary | `apps/web/src/app/[locale]/(main)/(private)/error.tsx` |
32+
| Widget error boundary | `apps/web/src/components/error-boundary.tsx` |
33+
| **Component test** | `apps/web/src/components/forms/universal-form.test.tsx` |
34+
| **E2E test** | `apps/web/e2e/auth.spec.ts` |
35+
36+
## API client (`@myorg/api-client`)
37+
38+
- **Generated** by orval from `packages/api-client/openapi.json` — never edit
39+
`src/generated/**` by hand. Regenerate with `pnpm generate:api`.
40+
- Call generated functions + hooks; wrap Server Actions in `safeAction`; pass
41+
`getServerRequestConfig()` from `@myorg/api-client/server` on the server.
42+
43+
## The gate
44+
45+
`pnpm typecheck && pnpm lint && pnpm test && pnpm build` — all green. Enforced
46+
locally by husky (lint-staged + pre-push typecheck) and in CI
47+
(`.github/workflows/ci.yml`).

0 commit comments

Comments
 (0)