@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,323 @@
1
+ ---
2
+ paths:
3
+ - "**/*.component.ts"
4
+ - "**/*.component.html"
5
+ - "**/*.store.ts"
6
+ - "**/+state/**/*.ts"
7
+ - "**/data-access/**/*.ts"
8
+ ---
9
+
10
+ # Angular Signals & Reactivity
11
+
12
+ ## Signals - State Management
13
+
14
+ ```typescript
15
+ // Writable signal - use protected for template-bound, private for internal
16
+ protected readonly count = signal(0);
17
+ protected readonly items = signal<Item[]>([]);
18
+
19
+ // Read value
20
+ const current = this.count();
21
+
22
+ // Update value
23
+ this.count.set(10);
24
+ this.count.update(v => v + 1);
25
+ this.items.update(items => [...items, newItem]);
26
+ ```
27
+
28
+ ## Computed - Derived State
29
+
30
+ ```typescript
31
+ // Automatically updates when dependencies change
32
+ protected readonly doubleCount = computed(() => this.count() * 2);
33
+ protected readonly isEmpty = computed(() => this.items().length === 0);
34
+ protected readonly filteredItems = computed(() =>
35
+ this.items().filter(item => item.active)
36
+ );
37
+
38
+ // Multiple dependencies
39
+ protected readonly summary = computed(() => ({
40
+ total: this.items().length,
41
+ active: this.items().filter(i => i.active).length,
42
+ selected: this.selectedId() !== null,
43
+ }));
44
+ ```
45
+
46
+ ## Effect - Side Effects
47
+
48
+ ```typescript
49
+ // Runs when any signal inside changes
50
+ effect(() => {
51
+ console.log('Count changed:', this.count());
52
+ });
53
+
54
+ // With cleanup
55
+ effect((onCleanup) => {
56
+ const subscription = someObservable$.subscribe();
57
+ onCleanup(() => subscription.unsubscribe());
58
+ });
59
+
60
+ // Conditional tracking
61
+ effect(() => {
62
+ if (this.isEnabled()) {
63
+ // Only tracks isEnabled and data when isEnabled is true
64
+ this.saveData(this.data());
65
+ }
66
+ });
67
+ ```
68
+
69
+ ## Signal Inputs/Outputs
70
+
71
+ ```typescript
72
+ // Inputs - use input() function, NOT @Input() decorator
73
+ public readonly name = input<string>(); // Optional, undefined if not provided
74
+ public readonly name = input('default'); // With default value
75
+ public readonly name = input.required<string>(); // Required, error if not provided
76
+
77
+ // Transform input
78
+ public readonly count = input(0, { transform: numberAttribute });
79
+
80
+ // Alias
81
+ public readonly userName = input<string>('', { alias: 'name' });
82
+ ```
83
+
84
+ ```typescript
85
+ // Outputs - use output() function, NOT @Output() decorator
86
+ public readonly clicked = output<void>();
87
+ public readonly selected = output<Item>();
88
+ public readonly valueChange = output<string>();
89
+
90
+ // Emit
91
+ this.clicked.emit();
92
+ this.selected.emit(item);
93
+ ```
94
+
95
+ ```typescript
96
+ // Two-way binding with model()
97
+ public readonly value = model<string>(''); // Creates input + output pair
98
+ public readonly value = model.required<string>(); // Required two-way binding
99
+
100
+ // Parent usage: [(value)]="parentValue"
101
+ // Internally creates: [value]="..." (valueChange)="..."
102
+
103
+ // Update in component
104
+ this.value.set('new value');
105
+ ```
106
+
107
+ ## Convert Observables to Signals
108
+
109
+ ```typescript
110
+ // toSignal - convert observable to signal
111
+ protected readonly data = toSignal(this.http.get<Data[]>('/api/data'), {
112
+ initialValue: []
113
+ });
114
+
115
+ // With error handling
116
+ protected readonly data = toSignal(this.http.get<Data[]>('/api/data'), {
117
+ initialValue: [],
118
+ rejectErrors: true,
119
+ });
120
+
121
+ // From store
122
+ protected readonly users = this.store.selectSignal(selectAllUsers);
123
+ protected readonly loading = this.store.selectSignal(selectLoading);
124
+ ```
125
+
126
+ ## linkedSignal - Resettable Derived State
127
+
128
+ `linkedSignal` creates a writable signal that resets when its source changes:
129
+
130
+ ```typescript
131
+ // Pagination that resets when filter changes
132
+ protected readonly searchQuery = signal('');
133
+ protected readonly currentPage = linkedSignal(() => {
134
+ this.searchQuery(); // Track dependency
135
+ return 1; // Reset to page 1 when query changes
136
+ });
137
+
138
+ // User can still manually change page
139
+ this.currentPage.set(5);
140
+
141
+ // But when searchQuery changes, currentPage resets to 1
142
+ ```
143
+
144
+ ```typescript
145
+ // Form field that resets to server value but can be edited
146
+ protected readonly selectedUser = input.required<User>();
147
+ protected readonly editedEmail = linkedSignal(() => this.selectedUser().email);
148
+
149
+ // User edits the email
150
+ this.editedEmail.set('new@email.com');
151
+
152
+ // When selectedUser changes, editedEmail resets to new user's email
153
+ ```
154
+
155
+ ```typescript
156
+ // Settings with server defaults that can be overridden
157
+ protected readonly serverSettings = toSignal(this.settingsService.get());
158
+ protected readonly localTheme = linkedSignal(() => this.serverSettings()?.theme ?? 'light');
159
+
160
+ // User can override locally
161
+ this.localTheme.set('dark');
162
+
163
+ // When server settings reload, resets to server value
164
+ ```
165
+
166
+ ## View Queries as Signals
167
+
168
+ ```typescript
169
+ // viewChild - single element/component reference
170
+ protected readonly inputRef = viewChild<ElementRef<HTMLInputElement>>('nameInput');
171
+ protected readonly inputRef = viewChild.required<ElementRef>('nameInput'); // Required
172
+
173
+ // viewChildren - multiple references
174
+ protected readonly items = viewChildren(ItemComponent);
175
+ protected readonly listItems = viewChildren<ElementRef>('listItem');
176
+
177
+ // contentChild - projected content
178
+ protected readonly header = contentChild<TemplateRef<unknown>>('header');
179
+ protected readonly header = contentChild.required(HeaderComponent);
180
+
181
+ // contentChildren - multiple projected content
182
+ protected readonly tabs = contentChildren(TabComponent);
183
+ ```
184
+
185
+ ```typescript
186
+ // Usage example
187
+ @Component({
188
+ selector: 'app-search',
189
+ template: `
190
+ <input #searchInput type="text" />
191
+ <button (click)="focusInput()">Focus</button>
192
+ `,
193
+ })
194
+ export class SearchComponent {
195
+ private readonly searchInput = viewChild.required<ElementRef<HTMLInputElement>>('searchInput');
196
+
197
+ public focusInput(): void {
198
+ this.searchInput().nativeElement.focus();
199
+ }
200
+ }
201
+ ```
202
+
203
+ ```typescript
204
+ // Content projection example
205
+ @Component({
206
+ selector: 'app-card',
207
+ template: `
208
+ <div class="card">
209
+ <header>
210
+ <ng-content select="[card-header]" />
211
+ </header>
212
+ <div class="body">
213
+ <ng-content />
214
+ </div>
215
+ </div>
216
+ `,
217
+ })
218
+ export class CardComponent {
219
+ private readonly headerContent = contentChild<ElementRef>('card-header');
220
+
221
+ protected readonly hasHeader = computed(() => !!this.headerContent());
222
+ }
223
+ ```
224
+
225
+ ## RxJS Interop
226
+
227
+ ```typescript
228
+ // toObservable - convert signal to observable
229
+ private readonly count$ = toObservable(this.count);
230
+
231
+ // Use when you need RxJS operators
232
+ this.searchQuery$.pipe(
233
+ debounceTime(300),
234
+ distinctUntilChanged(),
235
+ switchMap(query => this.searchService.search(query))
236
+ ).subscribe();
237
+
238
+ // Cleanup with takeUntilDestroyed
239
+ private readonly destroyRef = inject(DestroyRef);
240
+
241
+ this.source$.pipe(
242
+ takeUntilDestroyed(this.destroyRef)
243
+ ).subscribe(data => this.handleData(data));
244
+ ```
245
+
246
+ ## Template Control Flow
247
+
248
+ ```html
249
+ <!-- Conditionals -->
250
+ @if (loading()) {
251
+ <app-spinner />
252
+ } @else if (error()) {
253
+ <app-error [message]="error()" />
254
+ } @else {
255
+ <app-content [data]="data()" />
256
+ }
257
+
258
+ <!-- Loops - track is REQUIRED -->
259
+ @for (item of items(); track item.id) {
260
+ <app-item [item]="item" (click)="select(item)" />
261
+ } @empty {
262
+ <p>No items found</p>
263
+ }
264
+
265
+ <!-- Switch -->
266
+ @switch (status()) {
267
+ @case ('loading') { <app-spinner /> }
268
+ @case ('error') { <app-error /> }
269
+ @default { <app-content /> }
270
+ }
271
+
272
+ <!-- Defer (lazy loading) -->
273
+ @defer (on viewport) {
274
+ <app-heavy-component />
275
+ } @placeholder {
276
+ <div>Loading...</div>
277
+ }
278
+ ```
279
+
280
+ ## Anti-patterns to Avoid
281
+
282
+ ```typescript
283
+ // BAD: Using decorators
284
+ @Input() name: string; // Use input() instead
285
+ @Output() click = new EventEmitter(); // Use output() instead
286
+ @ViewChild('ref') ref: ElementRef; // Use viewChild() instead
287
+
288
+ // BAD: Constructor injection
289
+ constructor(private store: Store) {} // Use inject() instead
290
+
291
+ // BAD: Subscribing in component
292
+ ngOnInit() {
293
+ this.data$.subscribe(d => this.data = d); // Use toSignal() instead
294
+ }
295
+
296
+ // BAD: Missing track in @for
297
+ @for (item of items()) { // WRONG - missing track
298
+ <div>{{ item.name }}</div>
299
+ }
300
+
301
+ // GOOD: Always use track
302
+ @for (item of items(); track item.id) {
303
+ <div>{{ item.name }}</div>
304
+ }
305
+
306
+ // BAD: Using computed() for resettable state
307
+ protected readonly currentPage = computed(() => 1); // Read-only, can't be changed!
308
+
309
+ // GOOD: Use linkedSignal() for resettable writable state
310
+ protected readonly currentPage = linkedSignal(() => {
311
+ this.filter(); // Reset when filter changes
312
+ return 1;
313
+ });
314
+
315
+ // BAD: Setting signals in computed
316
+ protected readonly total = computed(() => {
317
+ this.loading.set(true); // WRONG - side effect in computed!
318
+ return this.items().length;
319
+ });
320
+
321
+ // GOOD: computed should be pure
322
+ protected readonly total = computed(() => this.items().length);
323
+ ```
@@ -0,0 +1,338 @@
1
+ ---
2
+ paths:
3
+ - "**/interceptors/**"
4
+ - "**/services/**/*.service.ts"
5
+ - "**/*.interceptor.ts"
6
+ ---
7
+
8
+ # Angular HTTP & Interceptors
9
+
10
+ ## HTTP Client Setup
11
+
12
+ ```typescript
13
+ // app.config.ts
14
+ import { provideHttpClient, withInterceptors } from '@angular/common/http';
15
+ import { authInterceptor } from './core/interceptors/auth.interceptor';
16
+ import { errorInterceptor } from './core/interceptors/error.interceptor';
17
+ import { loggingInterceptor } from './core/interceptors/logging.interceptor';
18
+
19
+ export const appConfig: ApplicationConfig = {
20
+ providers: [
21
+ provideHttpClient(
22
+ withInterceptors([
23
+ loggingInterceptor,
24
+ authInterceptor,
25
+ errorInterceptor,
26
+ ])
27
+ ),
28
+ ],
29
+ };
30
+ ```
31
+
32
+ ## Interceptors (Functional)
33
+
34
+ ### Auth Interceptor
35
+
36
+ ```typescript
37
+ // interceptors/auth.interceptor.ts
38
+ import { HttpInterceptorFn } from '@angular/common/http';
39
+ import { inject } from '@angular/core';
40
+ import { AuthService } from '../services/auth.service';
41
+
42
+ export const authInterceptor: HttpInterceptorFn = (req, next) => {
43
+ const authService = inject(AuthService);
44
+ const token = authService.accessToken();
45
+
46
+ if (token && !req.url.includes('/auth/')) {
47
+ const authReq = req.clone({
48
+ setHeaders: {
49
+ Authorization: `Bearer ${token}`,
50
+ },
51
+ });
52
+ return next(authReq);
53
+ }
54
+
55
+ return next(req);
56
+ };
57
+ ```
58
+
59
+ ### Error Interceptor
60
+
61
+ ```typescript
62
+ // interceptors/error.interceptor.ts
63
+ import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
64
+ import { inject } from '@angular/core';
65
+ import { Router } from '@angular/router';
66
+ import { catchError, throwError } from 'rxjs';
67
+ import { NotificationService } from '../services/notification.service';
68
+
69
+ export const errorInterceptor: HttpInterceptorFn = (req, next) => {
70
+ const router = inject(Router);
71
+ const notification = inject(NotificationService);
72
+
73
+ return next(req).pipe(
74
+ catchError((error: HttpErrorResponse) => {
75
+ switch (error.status) {
76
+ case 401:
77
+ router.navigate(['/login']);
78
+ break;
79
+ case 403:
80
+ notification.error('You do not have permission for this action');
81
+ break;
82
+ case 404:
83
+ notification.error('Resource not found');
84
+ break;
85
+ case 422:
86
+ // Validation errors - let component handle
87
+ break;
88
+ case 500:
89
+ notification.error('Server error. Please try again later.');
90
+ break;
91
+ default:
92
+ notification.error('An unexpected error occurred');
93
+ }
94
+
95
+ return throwError(() => error);
96
+ })
97
+ );
98
+ };
99
+ ```
100
+
101
+ ### Retry Interceptor
102
+
103
+ ```typescript
104
+ // interceptors/retry.interceptor.ts
105
+ import { HttpInterceptorFn } from '@angular/common/http';
106
+ import { retry, timer } from 'rxjs';
107
+
108
+ export const retryInterceptor: HttpInterceptorFn = (req, next) => {
109
+ // Only retry GET requests
110
+ if (req.method !== 'GET') {
111
+ return next(req);
112
+ }
113
+
114
+ return next(req).pipe(
115
+ retry({
116
+ count: 3,
117
+ delay: (error, retryCount) => {
118
+ // Exponential backoff: 1s, 2s, 4s
119
+ const delayMs = Math.pow(2, retryCount - 1) * 1000;
120
+ console.log(`Retry ${retryCount} after ${delayMs}ms`);
121
+ return timer(delayMs);
122
+ },
123
+ resetOnSuccess: true,
124
+ })
125
+ );
126
+ };
127
+ ```
128
+
129
+ ### Caching Interceptor
130
+
131
+ ```typescript
132
+ // interceptors/cache.interceptor.ts
133
+ import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
134
+ import { of, tap } from 'rxjs';
135
+
136
+ const cache = new Map<string, HttpResponse<unknown>>();
137
+
138
+ export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
139
+ // Only cache GET requests
140
+ if (req.method !== 'GET') {
141
+ return next(req);
142
+ }
143
+
144
+ // Check for no-cache header
145
+ if (req.headers.has('x-no-cache')) {
146
+ cache.delete(req.urlWithParams);
147
+ return next(req);
148
+ }
149
+
150
+ const cached = cache.get(req.urlWithParams);
151
+ if (cached) {
152
+ return of(cached.clone());
153
+ }
154
+
155
+ return next(req).pipe(
156
+ tap((event) => {
157
+ if (event instanceof HttpResponse) {
158
+ cache.set(req.urlWithParams, event.clone());
159
+ }
160
+ })
161
+ );
162
+ };
163
+ ```
164
+
165
+ ### Loading Interceptor
166
+
167
+ ```typescript
168
+ // interceptors/loading.interceptor.ts
169
+ import { HttpInterceptorFn } from '@angular/common/http';
170
+ import { inject } from '@angular/core';
171
+ import { finalize } from 'rxjs';
172
+ import { LoadingService } from '../services/loading.service';
173
+
174
+ export const loadingInterceptor: HttpInterceptorFn = (req, next) => {
175
+ const loadingService = inject(LoadingService);
176
+
177
+ // Skip for specific requests
178
+ if (req.headers.has('x-skip-loading')) {
179
+ return next(req);
180
+ }
181
+
182
+ loadingService.show();
183
+
184
+ return next(req).pipe(
185
+ finalize(() => loadingService.hide())
186
+ );
187
+ };
188
+ ```
189
+
190
+ ### Logging Interceptor
191
+
192
+ ```typescript
193
+ // interceptors/logging.interceptor.ts
194
+ import { HttpInterceptorFn } from '@angular/common/http';
195
+ import { tap } from 'rxjs';
196
+
197
+ export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
198
+ const startTime = Date.now();
199
+
200
+ return next(req).pipe(
201
+ tap({
202
+ next: (event) => {
203
+ if (event.type !== 0) { // Skip progress events
204
+ const duration = Date.now() - startTime;
205
+ console.log(`${req.method} ${req.url} - ${duration}ms`);
206
+ }
207
+ },
208
+ error: (error) => {
209
+ const duration = Date.now() - startTime;
210
+ console.error(`${req.method} ${req.url} - FAILED - ${duration}ms`, error);
211
+ },
212
+ })
213
+ );
214
+ };
215
+ ```
216
+
217
+ ## API Service Pattern
218
+
219
+ ```typescript
220
+ // services/api.service.ts
221
+ @Injectable({ providedIn: 'root' })
222
+ export class ApiService {
223
+ private readonly http = inject(HttpClient);
224
+ private readonly baseUrl = inject(API_BASE_URL);
225
+
226
+ public get<T>(path: string, params?: HttpParams): Observable<T> {
227
+ return this.http.get<T>(`${this.baseUrl}${path}`, { params });
228
+ }
229
+
230
+ public post<T>(path: string, body: unknown): Observable<T> {
231
+ return this.http.post<T>(`${this.baseUrl}${path}`, body);
232
+ }
233
+
234
+ public put<T>(path: string, body: unknown): Observable<T> {
235
+ return this.http.put<T>(`${this.baseUrl}${path}`, body);
236
+ }
237
+
238
+ public patch<T>(path: string, body: unknown): Observable<T> {
239
+ return this.http.patch<T>(`${this.baseUrl}${path}`, body);
240
+ }
241
+
242
+ public delete<T>(path: string): Observable<T> {
243
+ return this.http.delete<T>(`${this.baseUrl}${path}`);
244
+ }
245
+ }
246
+
247
+ // services/user.service.ts
248
+ @Injectable({ providedIn: 'root' })
249
+ export class UserService {
250
+ private readonly api = inject(ApiService);
251
+
252
+ public getAll(): Observable<User[]> {
253
+ return this.api.get<User[]>('/users');
254
+ }
255
+
256
+ public getById(id: string): Observable<User> {
257
+ return this.api.get<User>(`/users/${id}`);
258
+ }
259
+
260
+ public create(data: CreateUserDto): Observable<User> {
261
+ return this.api.post<User>('/users', data);
262
+ }
263
+
264
+ public update(id: string, data: UpdateUserDto): Observable<User> {
265
+ return this.api.patch<User>(`/users/${id}`, data);
266
+ }
267
+
268
+ public delete(id: string): Observable<void> {
269
+ return this.api.delete<void>(`/users/${id}`);
270
+ }
271
+ }
272
+ ```
273
+
274
+ ## Error Handling in Components
275
+
276
+ ```typescript
277
+ @Component({ ... })
278
+ export class UserFormComponent {
279
+ private readonly userService = inject(UserService);
280
+
281
+ protected readonly error = signal<string | null>(null);
282
+ protected readonly fieldErrors = signal<Record<string, string>>({});
283
+ protected readonly isSubmitting = signal(false);
284
+
285
+ public async onSubmit(data: CreateUserDto): Promise<void> {
286
+ this.isSubmitting.set(true);
287
+ this.error.set(null);
288
+ this.fieldErrors.set({});
289
+
290
+ this.userService.create(data).subscribe({
291
+ next: (user) => {
292
+ this.router.navigate(['/users', user.id]);
293
+ },
294
+ error: (err: HttpErrorResponse) => {
295
+ if (err.status === 422 && err.error?.errors) {
296
+ // Handle validation errors
297
+ const errors: Record<string, string> = {};
298
+ for (const e of err.error.errors) {
299
+ errors[e.field] = e.message;
300
+ }
301
+ this.fieldErrors.set(errors);
302
+ } else {
303
+ this.error.set('Failed to create user');
304
+ }
305
+ this.isSubmitting.set(false);
306
+ },
307
+ });
308
+ }
309
+ }
310
+ ```
311
+
312
+ ## Anti-patterns
313
+
314
+ ```typescript
315
+ // BAD: Class-based interceptors (deprecated)
316
+ @Injectable()
317
+ export class AuthInterceptor implements HttpInterceptor { ... }
318
+
319
+ // GOOD: Functional interceptors
320
+ export const authInterceptor: HttpInterceptorFn = (req, next) => { ... };
321
+
322
+ // BAD: Hardcoded URLs
323
+ this.http.get('https://api.example.com/users');
324
+
325
+ // GOOD: Use injection token
326
+ this.http.get(`${this.baseUrl}/users`);
327
+
328
+ // BAD: Not handling errors
329
+ this.http.get('/users').subscribe(users => this.users = users);
330
+
331
+ // GOOD: Handle errors
332
+ this.http.get('/users').pipe(
333
+ catchError(err => {
334
+ this.error.set('Failed to load users');
335
+ return of([]);
336
+ })
337
+ ).subscribe(users => this.users.set(users));
338
+ ```