@malamute/ai-rules 1.0.0 → 1.2.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 (133) hide show
  1. package/README.md +270 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
  4. package/configs/_shared/.claude/rules/conventions/git.md +265 -0
  5. package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
  6. package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
  7. package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
  8. package/configs/_shared/.claude/rules/devops/docker.md +275 -0
  9. package/configs/_shared/.claude/rules/devops/nx.md +194 -0
  10. package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
  11. package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
  12. package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
  13. package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
  14. package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
  15. package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
  16. package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
  17. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
  18. package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
  19. package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
  20. package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
  21. package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
  22. package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
  23. package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
  24. package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
  25. package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
  26. package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
  27. package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
  28. package/configs/_shared/.claude/rules/quality/logging.md +45 -0
  29. package/configs/_shared/.claude/rules/quality/observability.md +240 -0
  30. package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
  31. package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
  32. package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
  33. package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
  34. package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
  35. package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  36. package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  37. package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  38. package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
  39. package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
  40. package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
  41. package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
  42. package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
  43. package/configs/_shared/CLAUDE.md +52 -149
  44. package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
  45. package/configs/angular/.claude/rules/core/resource.md +285 -0
  46. package/configs/angular/.claude/rules/core/signals.md +323 -0
  47. package/configs/angular/.claude/rules/http.md +338 -0
  48. package/configs/angular/.claude/rules/routing.md +291 -0
  49. package/configs/angular/.claude/rules/ssr.md +312 -0
  50. package/configs/angular/.claude/rules/state/signal-store.md +408 -0
  51. package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
  52. package/configs/angular/.claude/rules/testing.md +7 -7
  53. package/configs/angular/.claude/rules/ui/aria.md +422 -0
  54. package/configs/angular/.claude/rules/ui/forms.md +424 -0
  55. package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
  56. package/configs/angular/.claude/settings.json +1 -0
  57. package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
  58. package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
  59. package/configs/angular/CLAUDE.md +24 -216
  60. package/configs/dotnet/.claude/rules/background-services.md +552 -0
  61. package/configs/dotnet/.claude/rules/configuration.md +426 -0
  62. package/configs/dotnet/.claude/rules/ddd.md +447 -0
  63. package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/.claude/rules/mediatr.md +320 -0
  65. package/configs/dotnet/.claude/rules/middleware.md +489 -0
  66. package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/.claude/rules/validation.md +388 -0
  68. package/configs/dotnet/.claude/settings.json +21 -3
  69. package/configs/dotnet/CLAUDE.md +53 -286
  70. package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/.claude/rules/dependencies.md +170 -0
  72. package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
  73. package/configs/fastapi/.claude/rules/lifespan.md +274 -0
  74. package/configs/fastapi/.claude/rules/middleware.md +229 -0
  75. package/configs/fastapi/.claude/rules/pydantic.md +433 -0
  76. package/configs/fastapi/.claude/rules/responses.md +251 -0
  77. package/configs/fastapi/.claude/rules/routers.md +202 -0
  78. package/configs/fastapi/.claude/rules/security.md +222 -0
  79. package/configs/fastapi/.claude/rules/testing.md +251 -0
  80. package/configs/fastapi/.claude/rules/websockets.md +298 -0
  81. package/configs/fastapi/.claude/settings.json +33 -0
  82. package/configs/fastapi/CLAUDE.md +144 -0
  83. package/configs/flask/.claude/rules/blueprints.md +208 -0
  84. package/configs/flask/.claude/rules/cli.md +285 -0
  85. package/configs/flask/.claude/rules/configuration.md +281 -0
  86. package/configs/flask/.claude/rules/context.md +238 -0
  87. package/configs/flask/.claude/rules/error-handlers.md +278 -0
  88. package/configs/flask/.claude/rules/extensions.md +278 -0
  89. package/configs/flask/.claude/rules/flask.md +171 -0
  90. package/configs/flask/.claude/rules/marshmallow.md +206 -0
  91. package/configs/flask/.claude/rules/security.md +267 -0
  92. package/configs/flask/.claude/rules/testing.md +284 -0
  93. package/configs/flask/.claude/settings.json +33 -0
  94. package/configs/flask/CLAUDE.md +166 -0
  95. package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/.claude/rules/filters.md +376 -0
  97. package/configs/nestjs/.claude/rules/interceptors.md +317 -0
  98. package/configs/nestjs/.claude/rules/middleware.md +321 -0
  99. package/configs/nestjs/.claude/rules/modules.md +26 -0
  100. package/configs/nestjs/.claude/rules/pipes.md +351 -0
  101. package/configs/nestjs/.claude/rules/websockets.md +451 -0
  102. package/configs/nestjs/.claude/settings.json +16 -2
  103. package/configs/nestjs/CLAUDE.md +57 -215
  104. package/configs/nextjs/.claude/rules/api-routes.md +358 -0
  105. package/configs/nextjs/.claude/rules/authentication.md +355 -0
  106. package/configs/nextjs/.claude/rules/components.md +52 -0
  107. package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/.claude/rules/database.md +400 -0
  109. package/configs/nextjs/.claude/rules/middleware.md +303 -0
  110. package/configs/nextjs/.claude/rules/routing.md +324 -0
  111. package/configs/nextjs/.claude/rules/seo.md +350 -0
  112. package/configs/nextjs/.claude/rules/server-actions.md +353 -0
  113. package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
  114. package/configs/nextjs/.claude/settings.json +5 -0
  115. package/configs/nextjs/CLAUDE.md +69 -331
  116. package/package.json +23 -9
  117. package/src/cli.js +220 -0
  118. package/src/config.js +29 -0
  119. package/src/index.js +13 -0
  120. package/src/installer.js +361 -0
  121. package/src/merge.js +116 -0
  122. package/src/tech-config.json +29 -0
  123. package/src/utils.js +96 -0
  124. package/configs/python/.claude/rules/flask.md +0 -332
  125. package/configs/python/.claude/settings.json +0 -18
  126. package/configs/python/CLAUDE.md +0 -273
  127. package/src/install.js +0 -315
  128. /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
  129. /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
  130. /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
  131. /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
  132. /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
  133. /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
@@ -0,0 +1,291 @@
1
+ ---
2
+ paths:
3
+ - "**/*.routes.ts"
4
+ - "**/app.routes.ts"
5
+ - "**/app.config.ts"
6
+ ---
7
+
8
+ # Angular Routing (Angular 21)
9
+
10
+ ## Route Configuration
11
+
12
+ ### Basic Routes
13
+
14
+ ```typescript
15
+ // app.routes.ts
16
+ import { Routes } from '@angular/router';
17
+
18
+ export const routes: Routes = [
19
+ { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
20
+ { path: 'dashboard', loadComponent: () => import('./dashboard/dashboard.component') },
21
+ { path: 'users', loadChildren: () => import('./users/users.routes') },
22
+ { path: '**', loadComponent: () => import('./not-found/not-found.component') },
23
+ ];
24
+ ```
25
+
26
+ ### Feature Routes with Lazy Loading
27
+
28
+ ```typescript
29
+ // users/users.routes.ts
30
+ import { Routes } from '@angular/router';
31
+ import { authGuard } from '@app/core/guards/auth.guard';
32
+ import { userResolver } from './resolvers/user.resolver';
33
+
34
+ export default [
35
+ {
36
+ path: '',
37
+ loadComponent: () => import('./user-list/user-list.component'),
38
+ canActivate: [authGuard],
39
+ },
40
+ {
41
+ path: ':id',
42
+ loadComponent: () => import('./user-detail/user-detail.component'),
43
+ resolve: { user: userResolver },
44
+ },
45
+ {
46
+ path: ':id/edit',
47
+ loadComponent: () => import('./user-edit/user-edit.component'),
48
+ canActivate: [authGuard],
49
+ canDeactivate: [unsavedChangesGuard],
50
+ },
51
+ ] satisfies Routes;
52
+ ```
53
+
54
+ ## Guards (Functional)
55
+
56
+ ### Auth Guard
57
+
58
+ ```typescript
59
+ // guards/auth.guard.ts
60
+ import { inject } from '@angular/core';
61
+ import { CanActivateFn, Router } from '@angular/router';
62
+ import { AuthService } from '@app/core/services/auth.service';
63
+
64
+ export const authGuard: CanActivateFn = () => {
65
+ const authService = inject(AuthService);
66
+ const router = inject(Router);
67
+
68
+ if (authService.isAuthenticated()) {
69
+ return true;
70
+ }
71
+
72
+ return router.createUrlTree(['/login'], {
73
+ queryParams: { returnUrl: router.url },
74
+ });
75
+ };
76
+ ```
77
+
78
+ ### Role Guard
79
+
80
+ ```typescript
81
+ // guards/role.guard.ts
82
+ import { inject } from '@angular/core';
83
+ import { CanActivateFn, Router } from '@angular/router';
84
+ import { AuthService } from '@app/core/services/auth.service';
85
+
86
+ export const roleGuard = (allowedRoles: string[]): CanActivateFn => {
87
+ return () => {
88
+ const authService = inject(AuthService);
89
+ const router = inject(Router);
90
+ const userRole = authService.currentUser()?.role;
91
+
92
+ if (userRole && allowedRoles.includes(userRole)) {
93
+ return true;
94
+ }
95
+
96
+ return router.createUrlTree(['/forbidden']);
97
+ };
98
+ };
99
+
100
+ // Usage in routes
101
+ {
102
+ path: 'admin',
103
+ loadComponent: () => import('./admin/admin.component'),
104
+ canActivate: [authGuard, roleGuard(['admin', 'superadmin'])],
105
+ }
106
+ ```
107
+
108
+ ### Unsaved Changes Guard
109
+
110
+ ```typescript
111
+ // guards/unsaved-changes.guard.ts
112
+ import { CanDeactivateFn } from '@angular/router';
113
+
114
+ export interface HasUnsavedChanges {
115
+ hasUnsavedChanges(): boolean;
116
+ }
117
+
118
+ export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
119
+ if (component.hasUnsavedChanges()) {
120
+ return confirm('You have unsaved changes. Do you really want to leave?');
121
+ }
122
+ return true;
123
+ };
124
+ ```
125
+
126
+ ## Resolvers
127
+
128
+ ```typescript
129
+ // resolvers/user.resolver.ts
130
+ import { inject } from '@angular/core';
131
+ import { ResolveFn, Router } from '@angular/router';
132
+ import { catchError, EMPTY } from 'rxjs';
133
+ import { UserService } from '../services/user.service';
134
+ import { User } from '../models/user.model';
135
+
136
+ export const userResolver: ResolveFn<User> = (route) => {
137
+ const userService = inject(UserService);
138
+ const router = inject(Router);
139
+ const userId = route.paramMap.get('id')!;
140
+
141
+ return userService.getById(userId).pipe(
142
+ catchError(() => {
143
+ router.navigate(['/users']);
144
+ return EMPTY;
145
+ })
146
+ );
147
+ };
148
+
149
+ // Usage in component
150
+ @Component({ ... })
151
+ export class UserDetailComponent {
152
+ private readonly route = inject(ActivatedRoute);
153
+ protected readonly user = toSignal(
154
+ this.route.data.pipe(map(data => data['user'] as User))
155
+ );
156
+ }
157
+ ```
158
+
159
+ ## Route Parameters
160
+
161
+ ```typescript
162
+ @Component({ ... })
163
+ export class UserDetailComponent {
164
+ private readonly route = inject(ActivatedRoute);
165
+
166
+ // Signal-based params
167
+ protected readonly userId = toSignal(
168
+ this.route.paramMap.pipe(map(params => params.get('id')))
169
+ );
170
+
171
+ // Query params
172
+ protected readonly tab = toSignal(
173
+ this.route.queryParamMap.pipe(map(params => params.get('tab') ?? 'profile'))
174
+ );
175
+ }
176
+ ```
177
+
178
+ ## Programmatic Navigation
179
+
180
+ ```typescript
181
+ @Component({ ... })
182
+ export class UserListComponent {
183
+ private readonly router = inject(Router);
184
+
185
+ public navigateToUser(userId: string): void {
186
+ this.router.navigate(['/users', userId]);
187
+ }
188
+
189
+ public navigateWithQuery(): void {
190
+ this.router.navigate(['/users'], {
191
+ queryParams: { status: 'active', page: 1 },
192
+ queryParamsHandling: 'merge', // or 'preserve'
193
+ });
194
+ }
195
+
196
+ public navigateRelative(): void {
197
+ // From /users to /users/123
198
+ this.router.navigate(['123'], { relativeTo: this.route });
199
+ }
200
+ }
201
+ ```
202
+
203
+ ## Route Layout with Named Outlets
204
+
205
+ ```typescript
206
+ // routes
207
+ {
208
+ path: 'dashboard',
209
+ component: DashboardLayoutComponent,
210
+ children: [
211
+ { path: '', loadComponent: () => import('./main/main.component') },
212
+ { path: '', outlet: 'sidebar', loadComponent: () => import('./sidebar/sidebar.component') },
213
+ ],
214
+ }
215
+
216
+ // template
217
+ <router-outlet />
218
+ <router-outlet name="sidebar" />
219
+ ```
220
+
221
+ ## Title & Meta
222
+
223
+ ```typescript
224
+ // routes
225
+ {
226
+ path: 'users',
227
+ loadComponent: () => import('./users/users.component'),
228
+ title: 'User Management',
229
+ data: {
230
+ meta: {
231
+ description: 'Manage user accounts',
232
+ },
233
+ },
234
+ }
235
+
236
+ // Title strategy (app.config.ts)
237
+ export const appConfig: ApplicationConfig = {
238
+ providers: [
239
+ provideRouter(routes, withComponentInputBinding()),
240
+ {
241
+ provide: TitleStrategy,
242
+ useClass: CustomTitleStrategy,
243
+ },
244
+ ],
245
+ };
246
+ ```
247
+
248
+ ## Preloading Strategies
249
+
250
+ ```typescript
251
+ // app.config.ts
252
+ import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
253
+
254
+ export const appConfig: ApplicationConfig = {
255
+ providers: [
256
+ provideRouter(
257
+ routes,
258
+ withPreloading(PreloadAllModules), // or custom strategy
259
+ withComponentInputBinding(),
260
+ withRouterConfig({ onSameUrlNavigation: 'reload' }),
261
+ ),
262
+ ],
263
+ };
264
+ ```
265
+
266
+ ## Anti-patterns
267
+
268
+ ```typescript
269
+ // BAD: Class-based guards (deprecated)
270
+ @Injectable()
271
+ export class AuthGuard implements CanActivate { ... }
272
+
273
+ // GOOD: Functional guards
274
+ export const authGuard: CanActivateFn = () => { ... };
275
+
276
+ // BAD: Subscribing in component for route params
277
+ ngOnInit() {
278
+ this.route.params.subscribe(params => this.id = params['id']);
279
+ }
280
+
281
+ // GOOD: Signal-based
282
+ protected readonly id = toSignal(
283
+ this.route.paramMap.pipe(map(p => p.get('id')))
284
+ );
285
+
286
+ // BAD: Hardcoded paths
287
+ this.router.navigateByUrl('/users/123/edit');
288
+
289
+ // GOOD: Relative navigation or route constants
290
+ this.router.navigate(['edit'], { relativeTo: this.route });
291
+ ```
@@ -0,0 +1,312 @@
1
+ ---
2
+ paths:
3
+ - "**/app.config.ts"
4
+ - "**/app.config.server.ts"
5
+ - "**/app.routes.server.ts"
6
+ - "**/server.ts"
7
+ - "**/main.server.ts"
8
+ ---
9
+
10
+ # Angular SSR & Hydration
11
+
12
+ ## Server Configuration
13
+
14
+ ```typescript
15
+ // app.config.ts
16
+ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
17
+ import { provideRouter } from '@angular/router';
18
+ import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
19
+
20
+ import { routes } from './app.routes';
21
+
22
+ export const appConfig: ApplicationConfig = {
23
+ providers: [
24
+ provideZoneChangeDetection({ eventCoalescing: true }),
25
+ provideRouter(routes),
26
+ provideClientHydration(withEventReplay()),
27
+ ],
28
+ };
29
+ ```
30
+
31
+ ```typescript
32
+ // app.config.server.ts
33
+ import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
34
+ import { provideServerRendering } from '@angular/platform-server';
35
+ import { provideServerRoutesConfig } from '@angular/ssr';
36
+
37
+ import { appConfig } from './app.config';
38
+ import { serverRoutes } from './app.routes.server';
39
+
40
+ const serverConfig: ApplicationConfig = {
41
+ providers: [
42
+ provideServerRendering(),
43
+ provideServerRoutesConfig(serverRoutes),
44
+ ],
45
+ };
46
+
47
+ export const config = mergeApplicationConfig(appConfig, serverConfig);
48
+ ```
49
+
50
+ ## Server Routes Configuration
51
+
52
+ ```typescript
53
+ // app.routes.server.ts
54
+ import { RenderMode, ServerRoute } from '@angular/ssr';
55
+
56
+ export const serverRoutes: ServerRoute[] = [
57
+ {
58
+ path: '',
59
+ renderMode: RenderMode.Prerender, // Static at build time
60
+ },
61
+ {
62
+ path: 'products',
63
+ renderMode: RenderMode.Prerender,
64
+ async getPrerenderParams() {
65
+ // Return params for prerendering
66
+ return [{ id: '1' }, { id: '2' }, { id: '3' }];
67
+ },
68
+ },
69
+ {
70
+ path: 'dashboard/**',
71
+ renderMode: RenderMode.Client, // Client-only, no SSR
72
+ },
73
+ {
74
+ path: '**',
75
+ renderMode: RenderMode.Server, // SSR at request time
76
+ },
77
+ ];
78
+ ```
79
+
80
+ ## Render Modes
81
+
82
+ | Mode | When to Use | SEO | Performance |
83
+ |------|-------------|-----|-------------|
84
+ | `Prerender` | Static content, marketing pages | Best | Best (cached) |
85
+ | `Server` | Dynamic content, user-specific | Good | Good |
86
+ | `Client` | Dashboards, authenticated areas | None | Fastest initial |
87
+
88
+ ## Platform Detection
89
+
90
+ ```typescript
91
+ import { isPlatformBrowser, isPlatformServer } from '@angular/common';
92
+ import { PLATFORM_ID, inject } from '@angular/core';
93
+
94
+ @Component({ ... })
95
+ export class MyComponent {
96
+ private readonly platformId = inject(PLATFORM_ID);
97
+
98
+ protected readonly isBrowser = isPlatformBrowser(this.platformId);
99
+ protected readonly isServer = isPlatformServer(this.platformId);
100
+
101
+ constructor() {
102
+ if (this.isBrowser) {
103
+ // Browser-only code (localStorage, window, etc.)
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ ## afterNextRender / afterRender
110
+
111
+ Use these for DOM manipulation that should only happen in browser:
112
+
113
+ ```typescript
114
+ import { afterNextRender, afterRender, Component, ElementRef, viewChild } from '@angular/core';
115
+
116
+ @Component({ ... })
117
+ export class ChartComponent {
118
+ private readonly canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('chart');
119
+
120
+ constructor() {
121
+ // Runs once after first render (browser only)
122
+ afterNextRender(() => {
123
+ this.initChart(this.canvas().nativeElement);
124
+ });
125
+
126
+ // Runs after every render (browser only)
127
+ afterRender(() => {
128
+ this.updateChart();
129
+ });
130
+ }
131
+
132
+ private initChart(canvas: HTMLCanvasElement): void {
133
+ // Safe to use browser APIs here
134
+ const ctx = canvas.getContext('2d');
135
+ // Initialize chart library...
136
+ }
137
+ }
138
+ ```
139
+
140
+ ## Transfer State
141
+
142
+ Share data between server and client to avoid duplicate requests:
143
+
144
+ ```typescript
145
+ import { makeStateKey, TransferState } from '@angular/core';
146
+
147
+ const USERS_KEY = makeStateKey<User[]>('users');
148
+
149
+ @Component({ ... })
150
+ export class UserListComponent {
151
+ private readonly transferState = inject(TransferState);
152
+ private readonly userService = inject(UserService);
153
+ private readonly platformId = inject(PLATFORM_ID);
154
+
155
+ protected readonly users = signal<User[]>([]);
156
+
157
+ constructor() {
158
+ afterNextRender(() => {
159
+ this.loadUsers();
160
+ });
161
+
162
+ // On server, load and store
163
+ if (isPlatformServer(this.platformId)) {
164
+ this.loadUsersServer();
165
+ }
166
+
167
+ // On client, retrieve from transfer state first
168
+ if (isPlatformBrowser(this.platformId)) {
169
+ const cached = this.transferState.get(USERS_KEY, null);
170
+ if (cached) {
171
+ this.users.set(cached);
172
+ this.transferState.remove(USERS_KEY);
173
+ }
174
+ }
175
+ }
176
+
177
+ private async loadUsersServer(): Promise<void> {
178
+ const users = await firstValueFrom(this.userService.getAll());
179
+ this.users.set(users);
180
+ this.transferState.set(USERS_KEY, users);
181
+ }
182
+ }
183
+ ```
184
+
185
+ ## Hydration Best Practices
186
+
187
+ ### Avoid Hydration Mismatch
188
+
189
+ ```typescript
190
+ // BAD: Different content on server vs client
191
+ @Component({
192
+ template: `<p>Current time: {{ now }}</p>`,
193
+ })
194
+ export class TimeComponent {
195
+ now = new Date().toISOString(); // Different on server vs client!
196
+ }
197
+
198
+ // GOOD: Use consistent initial state
199
+ @Component({
200
+ template: `<p>Current time: {{ now() }}</p>`,
201
+ })
202
+ export class TimeComponent {
203
+ protected readonly now = signal<string>('');
204
+
205
+ constructor() {
206
+ afterNextRender(() => {
207
+ this.now.set(new Date().toISOString());
208
+ });
209
+ }
210
+ }
211
+ ```
212
+
213
+ ### Skip Hydration for Dynamic Content
214
+
215
+ ```html
216
+ <!-- Skip hydration for parts that will differ -->
217
+ <div ngSkipHydration>
218
+ <app-live-clock />
219
+ <app-user-presence />
220
+ </div>
221
+ ```
222
+
223
+ ```typescript
224
+ // Or in component
225
+ @Component({
226
+ host: { ngSkipHydration: 'true' },
227
+ })
228
+ export class LiveClockComponent { }
229
+ ```
230
+
231
+ ## HTTP Caching with Transfer State
232
+
233
+ ```typescript
234
+ // interceptors/transfer-state.interceptor.ts
235
+ import { HttpInterceptorFn } from '@angular/common/http';
236
+ import { inject } from '@angular/core';
237
+ import { makeStateKey, TransferState } from '@angular/core';
238
+ import { isPlatformServer } from '@angular/common';
239
+ import { PLATFORM_ID } from '@angular/core';
240
+ import { of, tap } from 'rxjs';
241
+
242
+ export const transferStateInterceptor: HttpInterceptorFn = (req, next) => {
243
+ if (req.method !== 'GET') {
244
+ return next(req);
245
+ }
246
+
247
+ const transferState = inject(TransferState);
248
+ const platformId = inject(PLATFORM_ID);
249
+ const key = makeStateKey<unknown>(req.urlWithParams);
250
+
251
+ if (isPlatformServer(platformId)) {
252
+ return next(req).pipe(
253
+ tap((response) => {
254
+ transferState.set(key, response);
255
+ })
256
+ );
257
+ }
258
+
259
+ const cached = transferState.get(key, null);
260
+ if (cached) {
261
+ transferState.remove(key);
262
+ return of(cached);
263
+ }
264
+
265
+ return next(req);
266
+ };
267
+ ```
268
+
269
+ ## Anti-patterns
270
+
271
+ ```typescript
272
+ // BAD: Direct DOM access without platform check
273
+ @Component({ ... })
274
+ export class MyComponent {
275
+ constructor() {
276
+ window.addEventListener('scroll', this.onScroll); // Crashes on server!
277
+ }
278
+ }
279
+
280
+ // GOOD: Use afterNextRender
281
+ @Component({ ... })
282
+ export class MyComponent {
283
+ constructor() {
284
+ afterNextRender(() => {
285
+ window.addEventListener('scroll', this.onScroll);
286
+ });
287
+ }
288
+ }
289
+
290
+
291
+ // BAD: localStorage without platform check
292
+ const token = localStorage.getItem('token'); // Crashes on server!
293
+
294
+ // GOOD: Check platform or use afterNextRender
295
+ constructor() {
296
+ afterNextRender(() => {
297
+ const token = localStorage.getItem('token');
298
+ this.token.set(token);
299
+ });
300
+ }
301
+
302
+
303
+ // BAD: Using setTimeout for "after render"
304
+ setTimeout(() => {
305
+ this.initChart();
306
+ }, 0);
307
+
308
+ // GOOD: Use afterNextRender
309
+ afterNextRender(() => {
310
+ this.initChart();
311
+ });
312
+ ```