Browse Source

Merge commit 'f14f98f766d70dd6c9b5e2c13cf0d67ab927a33a' into feature/public-access-filter

pull/5848/head
Germán Martín 4 weeks ago
parent
commit
cff5b08652
  1. 130
      .agents/skills/angular-developer/SKILL.md
  2. 160
      .agents/skills/angular-developer/references/angular-animations.md
  3. 410
      .agents/skills/angular-developer/references/angular-aria.md
  4. 86
      .agents/skills/angular-developer/references/cli.md
  5. 59
      .agents/skills/angular-developer/references/component-harnesses.md
  6. 91
      .agents/skills/angular-developer/references/component-styling.md
  7. 117
      .agents/skills/angular-developer/references/components.md
  8. 97
      .agents/skills/angular-developer/references/creating-services.md
  9. 69
      .agents/skills/angular-developer/references/data-resolvers.md
  10. 67
      .agents/skills/angular-developer/references/define-routes.md
  11. 72
      .agents/skills/angular-developer/references/defining-providers.md
  12. 120
      .agents/skills/angular-developer/references/di-fundamentals.md
  13. 66
      .agents/skills/angular-developer/references/e2e-testing.md
  14. 83
      .agents/skills/angular-developer/references/effects.md
  15. 43
      .agents/skills/angular-developer/references/hierarchical-injectors.md
  16. 80
      .agents/skills/angular-developer/references/host-elements.md
  17. 63
      .agents/skills/angular-developer/references/injection-context.md
  18. 101
      .agents/skills/angular-developer/references/inputs.md
  19. 59
      .agents/skills/angular-developer/references/linked-signal.md
  20. 61
      .agents/skills/angular-developer/references/loading-strategies.md
  21. 106
      .agents/skills/angular-developer/references/mcp.md
  22. 30
      .agents/skills/angular-developer/references/migrations.md
  23. 69
      .agents/skills/angular-developer/references/navigate-to-routes.md
  24. 86
      .agents/skills/angular-developer/references/outputs.md
  25. 122
      .agents/skills/angular-developer/references/reactive-forms.md
  26. 44
      .agents/skills/angular-developer/references/rendering-strategies.md
  27. 77
      .agents/skills/angular-developer/references/resource.md
  28. 56
      .agents/skills/angular-developer/references/route-animations.md
  29. 52
      .agents/skills/angular-developer/references/route-guards.md
  30. 45
      .agents/skills/angular-developer/references/router-lifecycle.md
  31. 87
      .agents/skills/angular-developer/references/router-testing.md
  32. 68
      .agents/skills/angular-developer/references/show-routes-with-outlets.md
  33. 897
      .agents/skills/angular-developer/references/signal-forms.md
  34. 94
      .agents/skills/angular-developer/references/signals-overview.md
  35. 69
      .agents/skills/angular-developer/references/tailwind-css.md
  36. 114
      .agents/skills/angular-developer/references/template-driven-forms.md
  37. 66
      .agents/skills/angular-developer/references/testing-fundamentals.md
  38. 5958
      .agents/skills/nestjs-best-practices/AGENTS.md
  39. 130
      .agents/skills/nestjs-best-practices/SKILL.md
  40. 56
      .agents/skills/nestjs-best-practices/rules/_sections.md
  41. 28
      .agents/skills/nestjs-best-practices/rules/_template.md
  42. 182
      .agents/skills/nestjs-best-practices/rules/api-use-dto-serialization.md
  43. 202
      .agents/skills/nestjs-best-practices/rules/api-use-interceptors.md
  44. 205
      .agents/skills/nestjs-best-practices/rules/api-use-pipes.md
  45. 191
      .agents/skills/nestjs-best-practices/rules/api-versioning.md
  46. 80
      .agents/skills/nestjs-best-practices/rules/arch-avoid-circular-deps.md
  47. 82
      .agents/skills/nestjs-best-practices/rules/arch-feature-modules.md
  48. 141
      .agents/skills/nestjs-best-practices/rules/arch-module-sharing.md
  49. 106
      .agents/skills/nestjs-best-practices/rules/arch-single-responsibility.md
  50. 108
      .agents/skills/nestjs-best-practices/rules/arch-use-events.md
  51. 97
      .agents/skills/nestjs-best-practices/rules/arch-use-repository-pattern.md
  52. 139
      .agents/skills/nestjs-best-practices/rules/db-avoid-n-plus-one.md
  53. 129
      .agents/skills/nestjs-best-practices/rules/db-use-migrations.md
  54. 140
      .agents/skills/nestjs-best-practices/rules/db-use-transactions.md
  55. 222
      .agents/skills/nestjs-best-practices/rules/devops-graceful-shutdown.md
  56. 167
      .agents/skills/nestjs-best-practices/rules/devops-use-config-module.md
  57. 232
      .agents/skills/nestjs-best-practices/rules/devops-use-logging.md
  58. 104
      .agents/skills/nestjs-best-practices/rules/di-avoid-service-locator.md
  59. 165
      .agents/skills/nestjs-best-practices/rules/di-interface-segregation.md
  60. 221
      .agents/skills/nestjs-best-practices/rules/di-liskov-substitution.md
  61. 86
      .agents/skills/nestjs-best-practices/rules/di-prefer-constructor-injection.md
  62. 94
      .agents/skills/nestjs-best-practices/rules/di-scope-awareness.md
  63. 101
      .agents/skills/nestjs-best-practices/rules/di-use-interfaces-tokens.md
  64. 125
      .agents/skills/nestjs-best-practices/rules/error-handle-async-errors.md
  65. 114
      .agents/skills/nestjs-best-practices/rules/error-throw-http-exceptions.md
  66. 140
      .agents/skills/nestjs-best-practices/rules/error-use-exception-filters.md
  67. 226
      .agents/skills/nestjs-best-practices/rules/micro-use-health-checks.md
  68. 167
      .agents/skills/nestjs-best-practices/rules/micro-use-patterns.md
  69. 252
      .agents/skills/nestjs-best-practices/rules/micro-use-queues.md
  70. 109
      .agents/skills/nestjs-best-practices/rules/perf-async-hooks.md
  71. 121
      .agents/skills/nestjs-best-practices/rules/perf-lazy-loading.md
  72. 131
      .agents/skills/nestjs-best-practices/rules/perf-optimize-database.md
  73. 128
      .agents/skills/nestjs-best-practices/rules/perf-use-caching.md
  74. 146
      .agents/skills/nestjs-best-practices/rules/security-auth-jwt.md
  75. 125
      .agents/skills/nestjs-best-practices/rules/security-rate-limiting.md
  76. 139
      .agents/skills/nestjs-best-practices/rules/security-sanitize-output.md
  77. 135
      .agents/skills/nestjs-best-practices/rules/security-use-guards.md
  78. 150
      .agents/skills/nestjs-best-practices/rules/security-validate-all-input.md
  79. 178
      .agents/skills/nestjs-best-practices/rules/test-e2e-supertest.md
  80. 179
      .agents/skills/nestjs-best-practices/rules/test-mock-external-services.md
  81. 153
      .agents/skills/nestjs-best-practices/rules/test-use-testing-module.md
  82. 1
      .claude/skills/angular-developer
  83. 1
      .claude/skills/nestjs-best-practices
  84. 3
      .config/prisma.ts
  85. 2
      .env.dev
  86. 2
      .env.example
  87. 2
      .gitignore
  88. 1
      .npmrc
  89. 3
      .prettierignore
  90. 12
      .vscode/launch.json
  91. 182
      CHANGELOG.md
  92. 6
      DEVELOPMENT.md
  93. 41
      README.md
  94. 5
      apps/api/src/app/access/access.controller.ts
  95. 11
      apps/api/src/app/account/account.controller.ts
  96. 4
      apps/api/src/app/account/account.module.ts
  97. 6
      apps/api/src/app/activities/activities.controller.ts
  98. 11
      apps/api/src/app/activities/activities.service.ts
  99. 352
      apps/api/src/app/admin/admin.service.ts
  100. 42
      apps/api/src/app/app.module.ts

130
.agents/skills/angular-developer/SKILL.md

@ -0,0 +1,130 @@
---
name: angular-developer
description: Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.
license: MIT
metadata:
author: Copyright 2026 Google LLC
version: '1.0'
---
# Angular Developer Guidelines
1. Always analyze the project's Angular version before providing guidance, as best practices and available features can vary significantly between versions. If creating a new project with Angular CLI, do not specify a version unless prompted by the user.
2. When generating code, follow Angular's style guide and best practices for maintainability and performance. Use the Angular CLI for scaffolding components, services, directives, pipes, and routes to ensure consistency.
3. Once you finish generating code, run `ng build` to ensure there are no build errors. If there are errors, analyze the error messages and fix them before proceeding. Do not skip this step, as it is critical for ensuring the generated code is correct and functional.
## Creating New Projects
If no guidelines are provided by the user, here are same default rules to follow when creating a new Angular project:
1. Use the latest stable version of Angular unless the user specifies otherwise.
2. Use Signals Forms for form management in new projects (available in Angular v21 and newer) [Find out more](references/signal-forms.md).
**Execution Rules for `ng new`:**
When asked to create a new Angular project, you must determine the correct execution command by following these strict steps:
**Step 1: Check for an explicit user version.**
- **IF** the user requests a specific version (e.g., Angular 15), bypass local installations and strictly use `npx`.
- **Command:** `npx @angular/cli@<requested_version> new <project-name>`
**Step 2: Check for an existing Angular installation.**
- **IF** no specific version is requested, run `ng version` in the terminal to check if the Angular CLI is already installed on the system.
- **IF** the command succeeds and returns an installed version, use the local/global installation directly.
- **Command:** `ng new <project-name>`
**Step 3: Fallback to Latest.**
- **IF** no specific version is requested AND the `ng version` command fails (indicating no Angular installation exists), you must use `npx` to fetch the latest version.
- **Command:** `npx @angular/cli@latest new <project-name>`
## Components
When working with Angular components, consult the following references based on the task:
- **Fundamentals**: Anatomy, metadata, core concepts, and template control flow (@if, @for, @switch). Read [components.md](references/components.md)
- **Inputs**: Signal-based inputs, transforms, and model inputs. Read [inputs.md](references/inputs.md)
- **Outputs**: Signal-based outputs and custom event best practices. Read [outputs.md](references/outputs.md)
- **Host Elements**: Host bindings and attribute injection. Read [host-elements.md](references/host-elements.md)
If you require deeper documentation not found in the references above, read the documentation at `https://angular.dev/guide/components`.
## Reactivity and Data Management
When managing state and data reactivity, use Angular Signals and consult the following references:
- **Signals Overview**: Core signal concepts (`signal`, `computed`), reactive contexts, and `untracked`. Read [signals-overview.md](references/signals-overview.md)
- **Dependent State (`linkedSignal`)**: Creating writable state linked to source signals. Read [linked-signal.md](references/linked-signal.md)
- **Async Reactivity (`resource`)**: Fetching asynchronous data directly into signal state. Read [resource.md](references/resource.md)
- **Side Effects (`effect`)**: Logging, third-party DOM manipulation (`afterRenderEffect`), and when NOT to use effects. Read [effects.md](references/effects.md)
## Forms
In most cases for new apps, **prefer signal forms**. When making a forms decision, analyze the project and consider the following guidelines:
- if the application is using v21 or newer and this is a new form, **prefer signal forms**.
-For older applications or when working with existing forms, use the appropriate form type that matches the applications current form strategy.
- **Signal Forms**: Use signals for form state management. Read [signal-forms.md](references/signal-forms.md)
- **Template-driven forms**: Use for simple forms. Read [template-driven-forms.md](references/template-driven-forms.md)
- **Reactive forms**: Use for complex forms. Read [reactive-forms.md](references/reactive-forms.md)
## Dependency Injection
When implementing dependency injection in Angular, follow these guidelines:
- **Fundamentals**: Overview of Dependency Injection, services, and the `inject()` function. Read [di-fundamentals.md](references/di-fundamentals.md)
- **Creating and Using Services**: Creating services, the `providedIn: 'root'` option, and injecting into components or other services. Read [creating-services.md](references/creating-services.md)
- **Defining Dependency Providers**: Automatic vs manual provision, `InjectionToken`, `useClass`, `useValue`, `useFactory`, and scopes. Read [defining-providers.md](references/defining-providers.md)
- **Injection Context**: Where `inject()` is allowed, `runInInjectionContext`, and `assertInInjectionContext`. Read [injection-context.md](references/injection-context.md)
- **Hierarchical Injectors**: The `EnvironmentInjector` vs `ElementInjector`, resolution rules, modifiers (`optional`, `skipSelf`), and `providers` vs `viewProviders`. Read [hierarchical-injectors.md](references/hierarchical-injectors.md)
## Angular Aria
When building accessible custom components for any of the following patterns: Accordion, Listbox, Combobox, Menu, Tabs, Toolbar, Tree, Grid, consult the following reference:
- **Angular Aria Components**: Building headless, accessible components (Accordion, Listbox, Combobox, Menu, Tabs, Toolbar, Tree, Grid) and styling ARIA attributes. Read [angular-aria.md](references/angular-aria.md)
## Routing
When implementing navigation in Angular, consult the following references:
- **Define Routes**: URL paths, static vs dynamic segments, wildcards, and redirects. Read [define-routes.md](references/define-routes.md)
- **Route Loading Strategies**: Eager vs lazy loading, and context-aware loading. Read [loading-strategies.md](references/loading-strategies.md)
- **Show Routes with Outlets**: Using `<router-outlet>`, nested outlets, and named outlets. Read [show-routes-with-outlets.md](references/show-routes-with-outlets.md)
- **Navigate to Routes**: Declarative navigation with `RouterLink` and programmatic navigation with `Router`. Read [navigate-to-routes.md](references/navigate-to-routes.md)
- **Control Route Access with Guards**: Implementing `CanActivate`, `CanMatch`, and other guards for security. Read [route-guards.md](references/route-guards.md)
- **Data Resolvers**: Pre-fetching data before route activation with `ResolveFn`. Read [data-resolvers.md](references/data-resolvers.md)
- **Router Lifecycle and Events**: Chronological order of navigation events and debugging. Read [router-lifecycle.md](references/router-lifecycle.md)
- **Rendering Strategies**: CSR, SSG (Prerendering), and SSR with hydration. Read [rendering-strategies.md](references/rendering-strategies.md)
- **Route Transition Animations**: Enabling and customizing the View Transitions API. Read [route-animations.md](references/route-animations.md)
If you require deeper documentation or more context, visit the [official Angular Routing guide](https://angular.dev/guide/routing).
## Styling and Animations
When implementing styling and animations in Angular, consult the following references:
- **Using Tailwind CSS with Angular**: Integrating Tailwind CSS into Angular projects. Read [tailwind-css.md](references/tailwind-css.md)
- **Angular Animations**: Using native CSS (recommended) or the legacy DSL for dynamic effects. Read [angular-animations.md](references/angular-animations.md)
- **Styling components**: Best practices for component styles and encapsulation. Read [component-styling.md](references/component-styling.md)
## Testing
When writing or updating tests, consult the following references based on the task:
- **Fundamentals**: Best practices for unit testing (Vitest), async patterns, and `TestBed`. Read [testing-fundamentals.md](references/testing-fundamentals.md)
- **Component Harnesses**: Standard patterns for robust component interaction. Read [component-harnesses.md](references/component-harnesses.md)
- **Router Testing**: Using `RouterTestingHarness` for reliable navigation tests. Read [router-testing.md](references/router-testing.md)
- **End-to-End (E2E) Testing**: Best practices for E2E tests with Cypress. Read [e2e-testing.md](references/e2e-testing.md)
## Tooling
When working with Angular tooling, consult the following references:
- **Angular CLI**: Creating applications, generating code (components, routes, services), serving, and building. Read [cli.md](references/cli.md)
- **Code Modernization**: Automatically refactoring to modern standards using migrations. Read [migrations.md](references/migrations.md)
- **Angular MCP Server**: Available tools, configuration, and experimental features. Read [mcp.md](references/mcp.md)

160
.agents/skills/angular-developer/references/angular-animations.md

@ -0,0 +1,160 @@
# Angular Animations
When animating elements in Angular, **first analyze the project's Angular version** in `package.json`.
For modern applications (**Angular v20.2 and above**), prefer using native CSS with `animate.enter` and `animate.leave`. For older applications, you may need to use the deprecated `@angular/animations` package.
## 1. Native CSS Animations (v20.2+ Recommended)
Modern Angular provides `animate.enter` and `animate.leave` to animate elements as they enter or leave the DOM. They apply CSS classes at the appropriate times.
### `animate.enter` and `animate.leave`
Use these directly on elements to apply CSS classes during the enter or leave phase. Angular automatically removes the enter classes when the animation completes. For `animate.leave`, Angular waits for the animation to finish before removing the element from the DOM.
`animate.enter` example:
```html
@if (isShown()) {
<div class="enter-container" animate.enter="enter-animation">
<p>The box is entering.</p>
</div>
}
```
```css
/* Ensure you have a starting style if using transitions instead of keyframes */
.enter-container {
border: 1px solid #dddddd;
margin-top: 1em;
padding: 20px;
font-weight: bold;
font-size: 20px;
}
.enter-container p {
margin: 0;
}
.enter-animation {
animation: slide-fade 1s;
}
@keyframes slide-fade {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
```
_Note: `animate.leave` may be added to child elements being removed._
### Event Bindings and Third-party Libraries
You can bind to `(animate.enter)` and `(animate.leave)` to call functions or use JS libraries like GSAP.
```html
@if(show()) {
<div (animate.leave)="onLeave($event)">...</div>
}
```
```ts
import { AnimationCallbackEvent } from '@angular/core';
onLeave(event: AnimationCallbackEvent) {
// Custom animation logic here
// CRITICAL: You MUST call animationComplete() when done so Angular removes the element!
event.animationComplete();
}
```
## 2. Advanced CSS Animations
CSS offers robust tools for advanced animation sequences.
### Animating State and Styles
Toggle CSS classes on elements using property binding to trigger transitions.
```html
<div [class.open]="isOpen">...</div>
```
```css
div {
transition: height 0.3s ease-out;
height: 100px;
}
div.open {
height: 200px;
}
```
### Animating Auto Height
You can use `css-grid` to animate to auto height.
```css
.container {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s;
}
.container.open {
grid-template-rows: 1fr;
}
.container > div {
overflow: hidden;
}
```
### Staggering and Parallel Animations
- **Staggering**: Use `animation-delay` or `transition-delay` with different values for items in a list.
- **Parallel**: Apply multiple animations in the `animation` shorthand (e.g., `animation: rotate 3s, fade-in 2s;`).
### Programmatic Control
Retrieve animations directly using standard Web APIs:
```ts
const animations = element.getAnimations();
animations.forEach((anim) => anim.pause());
```
## 3. Legacy Animations DSL (Deprecated)
For older projects (pre v20.2 or where `@angular/animations` is already heavily used), you use the component metadata DSL.
**Important:** Do not mix legacy animations and `animate.enter`/`leave` in the same component.
### Setup
```ts
bootstrapApplication(App, {
providers: [provideAnimationsAsync()],
});
```
### Defining Transitions
```ts
import {signal} from '@angular/core';
import {trigger, state, style, animate, transition} from '@angular/animations';
@Component({
animations: [
trigger('openClose', [
state('open', style({opacity: 1})),
state('closed', style({opacity: 0})),
transition('open <=> closed', [animate('0.5s')]),
]),
],
template: `<div [@openClose]="isOpen() ? 'open' : 'closed'">...</div>`,
})
export class OpenClose {
isOpen = signal(true);
}
```

410
.agents/skills/angular-developer/references/angular-aria.md

@ -0,0 +1,410 @@
# Angular Aria
Angular Aria (`@angular/aria`) is a collection of headless, accessible directives that implement common WAI-ARIA patterns. These directives handle keyboard interactions, ARIA attributes, focus management, and screen reader support.
**As an AI Agent, your role is to provide the HTML structure and CSS styling**, while the directives handle the complex accessibility logic.
## Styling Headless Components
Because Angular Aria components are headless, they do not come with default styles. You **must** use CSS to style different states based on the ARIA attributes or structural classes the directives automatically apply.
Common ARIA attributes to target in CSS:
- `[aria-expanded="true"]` / `[aria-expanded="false"]`
- `[aria-selected="true"]`
- `[aria-disabled="true"]`
- `[aria-current="page"]` (for navigation)
---
**CRITICAL**: Before using this package, it must be installed via the package manager. Confirm that it has been installed in the project. Use `npm install @angular/aria` to install if necessary.
## 1. Accordion
Organizes related content into expandable/collapsible sections.
**Usage:** The Accordion is a layout component designed to organize content into logical groups that users can expand one at a time to reduce scrolling on content-heavy pages. Use it for FAQs, long forms, or progressive disclosure of information, but avoid it for primary navigation or scenarios where users must view multiple sections of content simultaneously.
**Imports:** `import { AccordionContent, AccordionGroup, AccordionPanel, AccordionTrigger } from '@angular/aria/accordion';`
**Directives:** `ngAccordionGroup`, `ngAccordionTrigger`, `ngAccordionPanel`, `ngAccordionContent` (for lazy loading).
```ts
@Component({
selector: 'app-cmp',
imports: [AccordionContent, AccordionGroup, AccordionPanel, AccordionTrigger],
template: `...`,
styles: [],
})
export class App {
protected readonly title = signal('angular-app');
}
```
```html
<div ngAccordionGroup [multiExpandable]="false">
<div class="accordion-item">
<button ngAccordionTrigger panelId="panel-1" class="accordion-header">
Section 1
<span class="icon"></span>
</button>
<div ngAccordionPanel panelId="panel-1" class="accordion-panel">
<ng-template ngAccordionContent>
<p>Lazy loaded content here.</p>
</ng-template>
</div>
</div>
</div>
```
**Styling Strategy:**
Target the `[aria-expanded]` attribute on the trigger to rotate icons, and style the panel visibility.
```css
.accordion-header[aria-expanded='true'] .icon {
transform: rotate(180deg);
}
/* The panel directive handles DOM removal, but you can style the transition */
.accordion-panel {
padding: 1rem;
border-top: 1px solid #ccc;
}
```
---
## 2. Listbox
A foundational directive for displaying a list of options. Used for visible selection lists (not dropdowns).
**Usage:** Visible selectable lists (single or multi-select).
**Imports:** `import {Listbox, Option} from '@angular/aria/listbox';`
**Directives:** `ngListbox`, `ngOption`.
```ts
@Component({
selector: 'app-cmp',
imports: [Listbox, Option],
template: `...`,
styles: [],
})
export class App {
protected readonly title = signal('angular-app');
}
```
```html
<!-- horizontal or vertical orientation -->
<ul ngListbox [(values)]="selectedItems" orientation="horizontal" [multi]="true">
<li ngOption value="apple" class="option">Apple</li>
<li ngOption value="banana" class="option">Banana</li>
</ul>
```
**Styling Strategy:**
Target `[aria-selected="true"]` for selected state and `:focus-visible` or `[data-active]` for the focused item (Angular Aria uses roving tabindex or activedescendant).
```css
.option {
padding: 8px;
cursor: pointer;
}
.option[aria-selected='true'] {
background: #e0f7fa;
font-weight: bold;
}
/* Focus state managed by aria */
.option:focus-visible {
outline: 2px solid blue;
}
```
---
## 3. Combobox, Select, and Multiselect
These patterns combine `ngCombobox` with a popup containing an `ngListbox`.
- **Combobox**: Text input + popup (used for Autocomplete).
- **Select**: Readonly Combobox + single-select Listbox.
- **Multiselect**: Readonly Combobox + multi-select Listbox.
**Usage:** The Combobox is a low-level primitive directive that synchronizes a text input with a popup, serving as the foundational logic for autocomplete, select, and multiselect patterns. Use it specifically for building custom filtering, unique selection requirements, or specialized input-to-popup coordination that deviates from standard, documented components.
**Imports:**
```
import {Combobox, ComboboxInput, ComboboxPopupContainer} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
```
**Directives:** `ngCombobox`, `ngComboboxInput`, `ngComboboxPopupContainer`, `ngListbox`, `ngOption`.
```html
<!-- Example: Standard Select -->
<div ngCombobox [readonly]="true">
<button ngComboboxInput class="select-trigger">
{{ selectedValue() || 'Choose an option' }}
</button>
<ng-template ngComboboxPopupContainer>
<ul ngListbox [(values)]="selectedValue" class="dropdown-menu">
<li ngOption value="option1">Option 1</li>
<li ngOption value="option2">Option 2</li>
</ul>
</ng-template>
</div>
```
**Styling Strategy:**
Style the popup container to look like a dropdown floating above content (often paired with CDK Overlay).
```css
.select-trigger {
width: 200px;
padding: 8px;
text-align: left;
}
.dropdown-menu {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #ccc;
background: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
```
---
## 4. Menu and Menubar
For actions, commands, and context menus (not for form selection).
**Usage:** The Menubar is a high-level navigation pattern designed for building desktop-style application command bars (e.g., File, Edit, View) that stay persistent across an interface. It is best utilized for organizing complex commands into logical top-level categories with full horizontal keyboard support, but it should be avoided for simple standalone action lists or mobile-first layouts where horizontal space is constrained.
**Imports:** `import {MenuBar, Menu, MenuContent, MenuItem} from '@angular/aria/menu';`
**Directives:** `ngMenuBar`, `ngMenu`, `ngMenuItem`, `ngMenuTrigger`.
```html
<!-- Menubar Example -->
<ul ngMenuBar class="menubar">
<li ngMenuItem value="file">
<button ngMenuTrigger [menu]="fileMenu">File</button>
</li>
</ul>
<ul ngMenu #fileMenu="ngMenu" class="menu">
<li ngMenuItem value="new">New</li>
<li ngMenuItem value="open">Open</li>
</ul>
```
**Styling Strategy:**
Use flexbox for the menubar. Hide/show submenus based on the trigger's state.
```css
.menubar {
display: flex;
gap: 10px;
list-style: none;
padding: 0;
}
.menu {
background: white;
border: 1px solid #ccc;
padding: 5px 0;
}
.menu li {
padding: 5px 15px;
cursor: pointer;
}
```
---
## 5. Tabs
Layered content sections where only one panel is visible.
**Usage:** The Tabs component is used to organize related content into distinct, navigable sections, allowing users to switch between categories or views without leaving the page. It is ideal for settings panels, multi-topic documentation, or dashboards, but should be avoided for sequential workflows (steppers) or when navigation involves more than 7–8 sections.
**Imports:** `import {Tab, Tabs, TabList, TabPanel, TabContent} from '@angular/aria/tabs';`
**Directives:** `ngTabs`, `ngTabList`, `ngTab`, `ngTabPanel`, `ngTabContent`.
```html
<div ngTabs>
<ul ngTabList class="tab-list">
<li ngTab value="profile" class="tab-btn">Profile</li>
<li ngTab value="security" class="tab-btn">Security</li>
</ul>
<div ngTabPanel value="profile" class="tab-panel">
<ng-template ngTabContent>Profile Settings</ng-template>
</div>
<div ngTabPanel value="security" class="tab-panel">
<ng-template ngTabContent>Security Settings</ng-template>
</div>
</div>
```
**Styling Strategy:**
Target `[aria-selected="true"]` on the tab buttons.
```css
.tab-list {
display: flex;
border-bottom: 2px solid #ccc;
list-style: none;
padding: 0;
}
.tab-btn {
padding: 10px 20px;
cursor: pointer;
border-bottom: 2px solid transparent;
}
.tab-btn[aria-selected='true'] {
border-bottom-color: blue;
font-weight: bold;
}
.tab-panel {
padding: 20px;
}
```
---
## 6. Toolbar
Groups related controls (like text formatting).
**Usage:** The Toolbar is an organizational component designed to group frequently accessed, related controls into a single logical container. It is best used to enhance keyboard efficiency (via arrow-key navigation) and visual structure for workflows requiring repeated actions, such as text formatting or media controls.
**Imports:** `import {Toolbar, ToolbarWidget, ToolbarWidgetGroup} from '@angular/aria/toolbar';`
**Directives:** `ngToolbar`, `ngToolbarWidget`, `ngToolbarWidgetGroup`.
```html
<div ngToolbar class="toolbar">
<div ngToolbarWidgetGroup [multi]="true" role="group" aria-label="Formatting">
<button ngToolbarWidget value="bold" class="tool-btn">B</button>
<button ngToolbarWidget value="italic" class="tool-btn">I</button>
</div>
</div>
```
**Styling Strategy:**
Target `[aria-pressed="true"]` (for toggle buttons) or `[aria-checked="true"]` (for radio groups) within the toolbar.
```css
.toolbar {
display: flex;
gap: 5px;
padding: 8px;
background: #f5f5f5;
}
.tool-btn {
padding: 5px 10px;
border: 1px solid #ccc;
}
.tool-btn[aria-pressed='true'],
.tool-btn[aria-checked='true'] {
background: #ddd;
}
```
---
## 7. Tree
Displays hierarchical data (file systems, nested nav).
**Usage:** The Tree component is designed for navigating and displaying deeply nested, hierarchical data structures like file systems, organization charts, or complex site architectures. It should be used specifically for multi-level relationships where users need to expand or collapse branches, but it should be avoided for flat lists, data tables, or simple selection menus.
**Imports:** `import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';`
**Directives:** `ngTree`, `ngTreeItem`, `ngTreeGroup`.
```html
<ul ngTree class="tree">
<li ngTreeItem value="documents">
<span class="tree-label">Documents</span>
<ul ngTreeGroup class="tree-group">
<li ngTreeItem value="resume">Resume.pdf</li>
</ul>
</li>
</ul>
```
**Styling Strategy:**
Target `[aria-expanded]` to show/hide children or rotate chevron icons. Use `padding-left` on nested groups to show hierarchy.
```css
.tree,
.tree-group {
list-style: none;
padding-left: 20px;
}
.tree-label::before {
content: '▶ ';
display: inline-block;
transition: transform 0.2s;
}
li[aria-expanded='true'] > .tree-label::before {
transform: rotate(90deg);
}
```
## 8. Grid
A two-dimensional interactive collection of cells enabling navigation via arrow keys.
**Usage:** Data tables, calendars, spreadsheets, and layout patterns for interactive elements.
**Directives:** `ngGrid`, `ngGridRow`, `ngGridCell`, `ngGridCellWidget`.
```html
<table ngGrid [multi]="true" [enableSelection]="true" class="grid-table">
<tr ngGridRow>
<th ngGridCell role="columnheader">Name</th>
<th ngGridCell role="columnheader">Status</th>
</tr>
<tr ngGridRow>
<td ngGridCell>Project A</td>
<td ngGridCell [(selected)]="isSelected">
<button ngGridCellWidget (activated)="onActivate()">Active</button>
</td>
</tr>
</table>
```
**Styling Strategy:**
Target `[aria-selected="true"]` for selected cells and `:focus-visible` for the active cell (roving tabindex) or `[aria-activedescendant]` on the container.
```css
.grid-table {
border-collapse: collapse;
}
[ngGridCell] {
padding: 8px;
border: 1px solid #ddd;
}
[ngGridCell][aria-selected='true'] {
background: #e3f2fd;
}
/* Focus state managed by roving tabindex */
[ngGridCell]:focus-visible {
outline: 2px solid #2196f3;
outline-offset: -2px;
}
```
## General Rules for Agents
1. **Never use native HTML elements like `<select>`** when asked to implement these specific Aria patterns. Use the `ng*` directives.
2. **Handle CSS manually**: Remember that `Angular Aria` does NOT provide styles. You must write the CSS, targeting the native ARIA attributes (`aria-expanded`, `aria-selected`, etc.) that the directives automatically toggle.
3. **Lazy Loading**: Always use the provided structural directives (`ngAccordionContent`, `ngTabContent`) inside `ng-template` for heavy content panels to ensure they are lazily rendered.

86
.agents/skills/angular-developer/references/cli.md

@ -0,0 +1,86 @@
# Angular CLI Guide for Agents
The Angular CLI (`ng`) is the primary tool for managing an Angular workspace. Always prefer CLI commands over manual file creation or generic `npm` commands when modifying project structure or adding Angular-specific dependencies.
## 1. Managing Dependencies
**ALWAYS use `ng add` for Angular libraries** instead of `npm install`. `ng add` installs the package AND runs initialization schematics (e.g., configuring `angular.json`, updating root providers).
```bash
ng add @angular/material
ng add tailwindcss
ng add @angular/fire
```
To update the application and its dependencies (which automatically runs code migrations):
```bash
ng update @angular/core@<latest or specific version> @angular/cli<latest or specific version>
```
## 2. Generating Code (`ng generate` or `ng g`)
Always use the CLI to generate code to ensure it adheres to Angular standards and updates necessary configuration files automatically.
| Target | Command | Notes |
| :----------- | :-------------------- | :--------------------------------------------------------------------------------------------- |
| Component | `ng g c path/to/name` | Generates a component. Use `--inline-style` (`-s`) or `--inline-template` (`-t`) if requested. |
| Service | `ng g s path/to/name` | Generates an `@Injectable({providedIn: 'root'})` service. |
| Directive | `ng g d path/to/name` | Generates a directive. |
| Pipe | `ng g p path/to/name` | Generates a pipe. |
| Guard | `ng g g path/to/name` | Generates a functional route guard. |
| Environments | `ng g environments` | Scaffolds `src/environments/` and updates `angular.json` with file replacements. |
_Note: There is no command to generate a single route definition. Generate a component, then manually add it to the `Routes` array in `app.routes.ts`._
## 3. Development Server & Proxying
Start the local development server with hot-module replacement (HMR):
```bash
ng serve
```
### Backend API Proxying
To proxy API requests during development (e.g., rerouting `/api` to a local Node server):
1. Create `src/proxy.conf.json`:
```json
{
"/api/**": {"target": "http://localhost:3000", "secure": false}
}
```
2. Update `angular.json` under the `serve` target:
```json
"serve": {
"builder": "@angular/build:dev-server",
"options": { "proxyConfig": "src/proxy.conf.json" }
}
```
## 4. Building the Application
Compile the application into an output directory (default: `dist/<project-name>/browser`). Modern Angular uses the `@angular/build:application` builder (esbuild-based).
```bash
ng build
```
- `ng build` defaults to the production configuration, which enables Ahead-of-Time (AOT) compilation, minification, and tree-shaking.
- Target specific configurations defined in `angular.json` using `--configuration`: `ng build --configuration=staging`.
## 5. Testing
- **Unit Tests**: Run `ng test` to execute unit tests via the configured test runner (e.g., Karma or Vitest).
- **End-to-End (E2E)**: Run `ng e2e`. If no E2E framework is configured, the CLI will prompt to install one (Cypress, Playwright, Puppeteer, etc.).
## 6. Deployment
To deploy an application, you must first add a deployment builder, then run the deploy command:
```bash
# Example for Firebase
ng add @angular/fire
ng deploy
```

59
.agents/skills/angular-developer/references/component-harnesses.md

@ -0,0 +1,59 @@
# Testing with Component Harnesses
Component harnesses are the standard, preferred way to interact with components in tests. They provide a robust, user-centric API that makes tests less brittle and easier to read by insulating them from changes to a component's internal DOM structure.
## Why Use Harnesses?
- **Robustness:** Tests don't break when you refactor a component's internal HTML or CSS classes.
- **Readability:** Tests describe interactions from a user's perspective (e.g., `button.click()`, `slider.getValue()`) instead of through DOM queries (`fixture.nativeElement.querySelector(...)`).
- **Reusability:** The same harness can be used in both unit tests and E2E tests.
Angular Material provides a test harness for every component in its library.
## Using a Harness in a Unit Test
The `TestbedHarnessEnvironment` is the entry point for using harnesses in unit tests.
### Example: Testing with a `MatButtonHarness`
```ts
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {MatButtonHarness} from '@angular/material/button/testing';
import {MyButtonContainerComponent} from './my-button-container.component';
describe('MyButtonContainerComponent', () => {
let fixture: ComponentFixture<MyButtonContainerComponent>;
let loader: HarnessLoader;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyButtonContainerComponent, MatButtonModule],
}).compileComponents();
fixture = TestBed.createComponent(MyButtonContainerComponent);
// Create a harness loader for the component's fixture
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should find a button with specific text', async () => {
// Load the harness for a MatButton with the text "Submit"
const submitButton = await loader.getHarness(MatButtonHarness.with({text: 'Submit'}));
// Use the harness API to interact with the component
expect(await submitButton.isDisabled()).toBe(false);
await submitButton.click();
// ... assertions
});
});
```
### Key Concepts
1. **`HarnessLoader`**: An object used to find and create harness instances. Get a loader for your component's fixture using `TestbedHarnessEnvironment.loader(fixture)`.
2. **`loader.getHarness(HarnessClass)`**: Asynchronously finds and returns a harness instance for the first matching component.
3. **`HarnessClass.with({ ... })`**: Many harnesses provide a static `with` method that returns a `HarnessPredicate`. This allows you to filter and find components based on their properties, like text, selector, or disabled state. Always use this to precisely target the component you want to test.
4. **Harness API:** Once you have a harness instance, use its methods (e.g., `.click()`, `.getText()`, `.getValue()`) to interact with the component. These methods automatically handle waiting for async operations and change detection.

91
.agents/skills/angular-developer/references/component-styling.md

@ -0,0 +1,91 @@
# Component Styling
Angular components can define styles that apply specifically to their template, enabling encapsulation and modularity.
## Defining Styles
Styles can be defined inline or in separate files.
```ts
@Component({
selector: 'app-photo',
// Inline styles
styles: `
img {
border-radius: 50%;
}
`,
// OR external file
styleUrl: 'photo.component.css',
})
export class Photo {}
```
## View Encapsulation
Every component has a view encapsulation setting that determines how styles are scoped.
| Mode | Behavior |
| :------------------------------ | :-------------------------------------------------------------------------------------------- |
| `Emulated` (Default) | Scopes styles to the component using unique HTML attributes. Global styles can still leak in. |
| `ShadowDom` | Uses the browser's native Shadow DOM API to isolate styles completely. |
| `None` | Disables encapsulation. Component styles become global. |
| `ExperimentalIsolatedShadowDom` | Strictly guarantees that only the component's styles apply. |
### Usage
```ts
import { ViewEncapsulation } from '@angular/core';
@Component({
...,
encapsulation: ViewEncapsulation.None,
})
export class GlobalStyled {}
```
## Special Selectors
### `:host`
Targets the component's host element (the element matching the component's selector).
```css
:host {
display: block;
border: 1px solid black;
}
```
### `:host-context()`
Targets the host element based on some condition in its ancestry.
```css
/* Apply styles if any ancestor has the 'theme-dark' class */
:host-context(.theme-dark) {
background-color: #333;
}
```
### `::ng-deep`
Disables view encapsulation for a specific rule, allowing it to "leak" into child components.
**Note: The Angular team strongly discourages the use of `::ng-deep`.** It is supported only for backwards compatibility.
## Styles in Templates
You can use `<style>` elements directly in a component's template. View encapsulation rules still apply.
```html
<style>
.dynamic-class {
color: red;
}
</style>
<div class="dynamic-class">Hello</div>
```
## External Styles
Using `<link>` or `@import` in CSS is treated as external styles. **External styles are not affected by emulated view encapsulation.**

117
.agents/skills/angular-developer/references/components.md

@ -0,0 +1,117 @@
# Components
Angular components are the fundamental building blocks of an application. Each component consists of a TypeScript class with behaviors, an HTML template, and a CSS selector.
## Component Definition
Use the `@Component` decorator to define a component's metadata.
```ts
@Component({
selector: 'app-profile',
template: `
<img src="profile.jpg" alt="Profile photo" />
<button (click)="save()">Save</button>
`,
styles: `
img {
border-radius: 50%;
}
`,
})
export class Profile {
save() {
/* ... */
}
}
```
## Metadata Options
- `selector`: The CSS selector that identifies this component in templates.
- `template`: Inline HTML template (preferred for small templates).
- `templateUrl`: Path to an external HTML file.
- `styles`: Inline CSS styles.
- `styleUrl` / `styleUrls`: Path(s) to external CSS file(s).
- `imports`: Lists the components, directives, or pipes used in this component's template.
## Using Components
To use a component, add it to the `imports` array of the consuming component and use its selector in the template.
```ts
@Component({
selector: 'app-root',
imports: [Profile],
template: `<app-profile />`,
})
export class App {}
```
## Template Control Flow
Angular uses built-in blocks for conditional rendering and loops.
### Conditional Rendering (`@if`)
Use `@if` to conditionally show content. You can include `@else if` and `@else` blocks.
```html
@if (user.isAdmin) {
<admin-dashboard />
} @else if (user.isModerator) {
<mod-dashboard />
} @else {
<standard-dashboard />
}
```
**Result aliasing**: Save the result of the expression for reuse.
```html
@if (user.settings(); as settings) {
<p>Theme: {{ settings.theme }}</p>
}
```
### Loops (`@for`)
The `@for` block iterates over collections. The `track` expression is **required** for performance and DOM reuse.
```html
<ul>
@for (item of items(); track item.id; let i = $index, total = $count) {
<li>{{ i + 1 }}/{{ total }}: {{ item.name }}</li>
} @empty {
<li>No items to display.</li>
}
</ul>
```
**Implicit Variables**: `$index`, `$count`, `$first`, `$last`, `$even`, `$odd`.
### Switching Content (`@switch`)
The `@switch` block renders content based on a value. It uses strict equality (`===`) and has **no fallthrough**.
```html
@switch (status()) { @case ('loading') { <app-spinner /> } @case ('error') { <app-error-msg /> }
@case ('success') { <app-data-grid /> } @default {
<p>Unknown status</p>
} }
```
**Exhaustive Type Checking**: Use `@default never;` to ensure all cases of a union type are handled.
```html
@switch (state) { @case ('on') { ... } @case ('off') { ... } @default never; // Errors if a new
state like 'standby' is added }
```
## Core Concepts
- **Host Element**: The DOM element that matches the component's selector.
- **View**: The DOM rendered by the component's template inside the host element.
- **Standalone**: By default, components are standalone (since Angular 19, `standalone: true` is default). For older versions, `standalone: true` must be explicit or the component must be part of an `NgModule`.
- **Component Tree**: Angular applications are structured as a tree of components, where each component can host child components.
- **Component Naming**: Do not add suffixes the `Component` suffix for Component classes (e.g., AppComponent) unless the project has been configured to use that naming configuration.

97
.agents/skills/angular-developer/references/creating-services.md

@ -0,0 +1,97 @@
# Creating and Using Services
Services in Angular are reusable pieces of code that handle data fetching, business logic, or state management that multiple components or other services need to access.
## Creating a Service
You can generate a service using the Angular CLI:
```bash
ng generate service my-data
```
Or you can manually create a TypeScript class and decorate it with `@Injectable()`.
```ts
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class BasicDataStore {
private data: string[] = [];
addData(item: string): void {
this.data.push(item);
}
getData(): string[] {
return [...this.data];
}
}
```
### The `providedIn: 'root'` Option
Using `providedIn: 'root'` is the recommended approach for most services. It tells Angular to:
- **Create a single instance (singleton)** for the entire application.
- **Make it available everywhere** automatically, without needing to list it in any `providers` array.
- **Enable tree-shaking**, meaning the service is only included in the final JavaScript bundle if it is actually injected somewhere.
## Injecting a Service
Once a service is created, you can inject it into components, directives, or other services using the `inject()` function.
### Injecting into a Component
```ts
import {Component, inject} from '@angular/core';
import {BasicDataStore} from './basic-data-store.service';
@Component({
selector: 'app-example',
template: `
<div>
<p>Data items: {{ dataStore.getData().length }}</p>
<button (click)="dataStore.addData('New Item')">Add Item</button>
</div>
`,
})
export class Example {
// Inject the service as a class field
dataStore = inject(BasicDataStore);
}
```
### Injecting into Another Service
Services can inject other services in the exact same way.
```ts
import {Injectable, inject} from '@angular/core';
import {AdvancedDataStore} from './advanced-data-store.service';
@Injectable({
providedIn: 'root',
})
export class BasicDataStore {
// Injecting another service
private advancedDataStore = inject(AdvancedDataStore);
private data: string[] = [];
getData(): string[] {
// Combine data from this service and the injected service
return [...this.data, ...this.advancedDataStore.getData()];
}
}
```
## Advanced Service Patterns
While `providedIn: 'root'` covers most scenarios, you may sometimes need:
- **Component-specific instances**: If a component needs its own isolated instance of a service, provide it directly in the component's `@Component({ providers: [MyService] })` array.
- **Factory providers**: For dynamic creation.
- **Value providers**: For injecting configuration objects.

69
.agents/skills/angular-developer/references/data-resolvers.md

@ -0,0 +1,69 @@
# Data Resolvers
Data resolvers fetch data before a route activates, ensuring components have the necessary data upon rendering.
## Creating a Resolver
Implement the `ResolveFn` type.
```ts
export const userResolver: ResolveFn<User> = (route, state) => {
const userService = inject(UserService);
const id = route.paramMap.get('id')!;
return userService.getUser(id);
};
```
## Configuring the Route
Add the resolver under the `resolve` key.
```ts
{
path: 'user/:id',
component: UserProfile,
resolve: {
user: userResolver
}
}
```
## Accessing Resolved Data
### 1. Via `ActivatedRoute` (Traditional)
```ts
private route = inject(ActivatedRoute);
data = toSignal(this.route.data);
user = computed(() => this.data().user);
```
### 2. Via Component Inputs (Modern)
Enable `withComponentInputBinding()` in `provideRouter` to pass resolved data directly to `@Input` or `input()`.
```ts
// app.config.ts
provideRouter(routes, withComponentInputBinding());
// component.ts
user = input.required<User>();
```
## Error Handling
Navigation is blocked if a resolver fails.
- Use `withNavigationErrorHandler` for global handling.
- Use `catchError` within the resolver to return a `RedirectCommand` or fallback data.
```ts
return userService
.get(id)
.pipe(catchError(() => of(new RedirectCommand(router.parseUrl('/error')))));
```
## Best Practices
- **Keep it lightweight**: Fetch only critical data.
- **Provide feedback**: Listen to router events to show a global loading bar during navigation, as the UI stays on the old page until the resolver finishes.

67
.agents/skills/angular-developer/references/define-routes.md

@ -0,0 +1,67 @@
# Define Routes
Routes are objects that define which component should render for a specific URL path.
## Basic Configuration
Define routes in a `Routes` array and provide them using `provideRouter` in your `appConfig`.
```ts
// app.routes.ts
export const routes: Routes = [
{path: '', component: HomePage},
{path: 'admin', component: AdminPage},
];
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)],
};
```
## URL Paths
- **Static**: Matches an exact string (e.g., `'admin'`).
- **Route Parameters**: Dynamic segments prefixed with a colon (e.g., `'user/:id'`).
- **Wildcard**: Matches any URL using `**`. Useful for "Not Found" pages. **Always place at the end of the array.**
## Matching Strategy
Angular uses a **first-match wins** strategy. Specific routes must come before less specific ones.
## Redirects
Use `redirectTo` to point one path to another.
```ts
{ path: 'articles', redirectTo: '/blog' },
{ path: 'blog', component: Blog },
```
## Page Titles
Associate titles with routes for accessibility. Titles can be static or dynamic (via `ResolveFn` or a custom `TitleStrategy`).
```ts
{ path: 'home', component: Home, title: 'Home Page' }
```
## Route Data and Providers
- **Static Data**: Attach metadata using the `data` property.
- **Route Providers**: Scope dependencies to a specific route and its children using the `providers` array.
## Nested (Child) Routes
Define sub-views using the `children` property. Parent components must include a `<router-outlet />`.
```ts
{
path: 'product/:id',
component: Product,
children: [
{ path: 'info', component: ProductInfo },
{ path: 'reviews', component: ProductReviews },
],
}
```

72
.agents/skills/angular-developer/references/defining-providers.md

@ -0,0 +1,72 @@
# Defining Dependency Providers
Angular offers automatic and manual ways to provide dependencies to its Dependency Injection (DI) system.
## Automatic Provision
The most common way to provide a service is using `providedIn: 'root'` on an `@Injectable()`.
### InjectionToken
Use `InjectionToken` for non-class dependencies (configuration objects, functions, primitives). An `InjectionToken` can also be automatically provided.
```ts
import {InjectionToken} from '@angular/core';
export interface AppConfig {
apiUrl: string;
}
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config', {
providedIn: 'root',
factory: () => ({apiUrl: 'https://api.example.com'}),
});
```
## Manual Provision
You use the `providers` array when a service lacks `providedIn`, when you want a new instance for a specific component, or when configuring runtime values.
```ts
@Component({
providers: [
// Shorthand for { provide: LocalService, useClass: LocalService }
LocalService,
// useClass: Swap implementations
{provide: Logger, useClass: BetterLogger},
// useValue: Provide static values
{provide: API_URL_TOKEN, useValue: 'https://api.example.com'},
// useFactory: Generate value dynamically
{
provide: ApiClient,
useFactory: (http = inject(HttpClient)) => new ApiClient(http),
},
// useExisting: Create an alias
{provide: OldLogger, useExisting: NewLogger},
// multi: Provide multiple values for the same token as an array
{provide: INTERCEPTOR_TOKEN, useClass: AuthInterceptor, multi: true},
],
})
export class Example {}
```
## Scopes of Providers
- **Application Bootstrap**: Global singletons. Use for HTTP clients, logging, or app-wide config.
- **Component/Directive**: Isolated instances. Use for component-specific state or forms. Services are destroyed when the component is destroyed.
- **Route**: Feature-specific services loaded only with specific routes.
## Library Pattern: `provide*` functions
Library authors should export functions that return provider arrays to encapsulate configuration:
```ts
export function provideAnalytics(config: AnalyticsConfig): Provider[] {
return [{provide: ANALYTICS_CONFIG, useValue: config}, AnalyticsService];
}
```

120
.agents/skills/angular-developer/references/di-fundamentals.md

@ -0,0 +1,120 @@
# Dependency Injection (DI) Fundamentals
Dependency Injection (DI) is a design pattern used to organize and share code across an application by allowing you to "inject" features into different parts. This improves code maintainability, scalability, and testability.
## How DI Works in Angular
There are two primary ways code interacts with Angular's DI system:
1. **Providing**: Making values (objects, functions, primitives) available to the DI system.
2. **Injecting**: Asking the DI system for those values.
Angular components, directives, and services automatically participate in DI.
## Services
A **service** is the most common way to share data and functionality across an application. It is a TypeScript class decorated with `@Injectable()`.
### Creating a Service
Use the `providedIn: 'root'` option in the `@Injectable` decorator to make the service a singleton available throughout the entire application. This is the recommended approach for most services.
```ts
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root', // Makes this a singleton available everywhere
})
export class AnalyticsLogger {
trackEvent(category: string, value: string) {
console.log('Analytics event logged:', {category, value});
}
}
```
Common uses for services include:
- Data clients (API calls)
- State management
- Authentication and authorization
- Logging and error handling
- Utility functions
## Injecting Dependencies
Use Angular's `inject()` function to request dependencies.
### The `inject()` Function
You can use the `inject()` function to get an instance of a service (or any other provided token).
```ts
import {Component, inject} from '@angular/core';
import {Router} from '@angular/router';
import {AnalyticsLogger} from './analytics-logger.service';
@Component({
selector: 'app-navbar',
template: `<a href="#" (click)="navigateToDetail($event)">Detail Page</a>`,
})
export class Navbar {
// Injecting dependencies using class field initializers
private router = inject(Router);
private analytics = inject(AnalyticsLogger);
navigateToDetail(event: Event) {
event.preventDefault();
this.analytics.trackEvent('navigation', '/details');
this.router.navigate(['/details']);
}
}
```
### Where can `inject()` be used? (Injection Context)
You can call `inject()` in an **injection context**. The most common injection contexts are during the construction of a component, directive, or service.
Valid places to call `inject()`:
1. **Class field initializers** (Recommended)
2. **Constructor body**
3. **Route guards and resolvers** (which are executed in an injection context)
4. **Factory functions** used in providers
```typescript
import {Component, Directive, Injectable, inject, ElementRef} from '@angular/core';
import {HttpClient} from '@angular/common/http';
// 1. In a Component (Field Initializer & Constructor)
@Component({
/*...*/
})
export class Example {
private service1 = inject(MyService); // ✅ Field initializer
private service2: MyService;
constructor() {
this.service2 = inject(MyService); // ✅ Constructor body
}
}
// 2. In a Directive
@Directive({
/*...*/
})
export class MyDirective {
private element = inject(ElementRef); // ✅ Field initializer
}
// 3. In a Service
@Injectable({providedIn: 'root'})
export class MyService {
private http = inject(HttpClient); // ✅ Field initializer
}
// 4. In a Route Guard (Functional)
export const authGuard = () => {
const auth = inject(AuthService); // ✅ Route Guard
return auth.isAuthenticated();
};
```

66
.agents/skills/angular-developer/references/e2e-testing.md

@ -0,0 +1,66 @@
# End-to-End (E2E) Testing
This project uses [Cypress](https://www.cypress.io/) for end-to-end (E2E) testing, which simulates real user interactions in a browser. The E2E tests are located primarily within the `devtools/` package.
## Running E2E Tests
The primary way to run E2E tests is through the `pnpm` script defined in the root `package.json`.
1. **Build DevTools:** The E2E tests run against a built version of the devtools extension. You must build it first:
```shell
pnpm -F ng-devtools-mcp build:dev
```
2. **Run Cypress:** Use the `cy:open` or `cy:run` script:
- To open the interactive Cypress Test Runner:
```shell
pnpm -F ng-devtools-mcp cy:open
```
- To run the tests headlessly in the terminal (ideal for CI):
```shell
pnpm -F ng-devtools-mcp cy:run
```
## Test Structure
- **Configuration:** The main Cypress configuration is located at `devtools/cypress.json`.
- **Specs:** Test files (specs) are located in `devtools/cypress/integration/`.
- **Custom Commands:** Reusable custom commands and actions are defined in `devtools/cypress/support/`.
### Example E2E Test Snippet
A typical test might look like this:
```typescript
// in devtools/cypress/integration/profiler.spec.ts
describe('Profiler', () => {
beforeEach(() => {
cy.visit('/?e2e-app');
cy.wait(1000);
cy.get('ng-devtools-tabs').find('a').contains('Profiler').click();
});
it('should record and display profiling data', () => {
// Find the record button and click it
cy.get('button[aria-label="start-recording-button"]').click();
// Interact with the test application to generate profiling data
cy.get('body').find('#cards button').first().click();
cy.wait(500);
// Stop recording
cy.get('button[aria-label="stop-recording-button"]').click();
// Assert that the flame graph is now visible
cy.get('ng-devtools-recording-timeline').find('canvas').should('be.visible');
});
});
```
### Best Practices
- **Use `data-` attributes:** Whenever possible, use `data-cy` or similar attributes for selecting elements to make tests more resilient to CSS or structural changes.
- **Custom Commands:** Encapsulate common sequences of actions into custom commands in the `support` directory to keep tests clean and readable.
- **Wait for Application State:** Use `cy.wait()` for arbitrary waits sparingly. Prefer to wait for specific UI elements to appear or for network requests to complete to avoid flaky tests.

83
.agents/skills/angular-developer/references/effects.md

@ -0,0 +1,83 @@
# Side Effects with `effect` and `afterRenderEffect`
In Angular, an **effect** is an operation that runs whenever one or more signal values it tracks change.
## When to use `effect`
Effects are intended for syncing signal state to imperative, non-signal APIs.
**Valid Use Cases:**
- Logging analytics.
- Syncing state to `localStorage` or `sessionStorage`.
- Performing custom rendering to a `<canvas>` or 3rd-party charting library.
**CRITICAL RULE: DO NOT use effects to propagate state.**
If you find yourself using `.set()` or `.update()` on a signal _inside_ an effect to keep two signals in sync, you are making a mistake. This causes `ExpressionChangedAfterItHasBeenChecked` errors and infinite loops. **Always use `computed()` or `linkedSignal()` for state derivation.**
## Basic Usage
Effects execute asynchronously during the change detection process. They always run at least once.
```ts
import { Component, signal, effect } from '@angular/core';
@Component({...})
export class Example {
count = signal(0);
constructor() {
// Effect must be created in an injection context (e.g., a constructor)
effect((onCleanup) => {
console.log(`Count changed to ${this.count()}`);
const timer = setTimeout(() => console.log('Timer finished'), 1000);
// Cleanup function runs before the next execution, or when destroyed
onCleanup(() => clearTimeout(timer));
});
}
}
```
## DOM Manipulation with `afterRenderEffect`
Standard `effect` runs _before_ Angular updates the DOM. If you need to manually inspect or modify the DOM based on a signal change (e.g., integrating a 3rd party UI library), use `afterRenderEffect`.
`afterRenderEffect` runs after Angular has finished rendering the DOM.
### Render Phases
To prevent reflows (forced layout thrashing), `afterRenderEffect` forces you to divide your DOM reads and writes into specific phases.
```ts
import { Component, afterRenderEffect, viewChild, ElementRef } from '@angular/core';
@Component({...})
export class Chart {
canvas = viewChild.required<ElementRef>('canvas');
constructor() {
afterRenderEffect({
// 1. Read from the DOM
earlyRead: () => {
return this.canvas().nativeElement.getBoundingClientRect().width;
},
// 2. Write to the DOM (receives the result of the previous phase)
write: (width) => {
// NEVER read from the DOM in the write phase.
setupChart(this.canvas().nativeElement, width);
}
});
}
}
```
**Available Phases (executed in this order):**
1. `earlyRead`
2. `write` (Never read here)
3. `mixedReadWrite` (Avoid if possible)
4. `read` (Never write here)
_Note: `afterRenderEffect` only runs on the client, never during Server-Side Rendering (SSR)._

43
.agents/skills/angular-developer/references/hierarchical-injectors.md

@ -0,0 +1,43 @@
# Hierarchical Injectors
Angular's dependency injection system is hierarchical, meaning services can be scoped to different levels of the application.
## Types of Injector Hierarchies
1. **`EnvironmentInjector` Hierarchy**: Configured via `@Injectable({ providedIn: 'root' })` or `ApplicationConfig.providers` during bootstrap. These are global singletons.
2. **`ElementInjector` Hierarchy**: Created implicitly at each DOM element. Configured via the `providers` or `viewProviders` array in `@Component()` or `@Directive()`.
## Resolution Rules
When a dependency is requested, Angular resolves it in two phases:
1. It searches up the **`ElementInjector`** tree, starting from the requesting component/directive up to the root element.
2. If not found, it searches the **`EnvironmentInjector`** tree, starting from the closest environment injector up to the root.
3. If still not found, it throws an error (unless marked optional).
## Resolution Modifiers
You can alter how Angular searches for a dependency using the options object in `inject()`:
- **`optional`**: If the dependency isn't found, return `null` instead of throwing an error.
- **`self`**: Only check the current `ElementInjector`. Do not look up the parent tree.
- **`skipSelf`**: Start searching in the parent `ElementInjector`, skipping the current element.
- **`host`**: Stop searching when reaching the host component's view boundary.
```ts
@Component({...})
export class Example {
// Returns null if not found instead of crashing
optionalService = inject(MyService, { optional: true });
// Skips this component's providers, looks at parent
parentService = inject(ParentService, { skipSelf: true });
}
```
## `providers` vs `viewProviders`
When providing a service at the component level:
- **`providers`**: The service is available to the component, its view (template), and any **projected content** (`<ng-content>`).
- **`viewProviders`**: The service is available to the component and its view, but **NOT** to projected content. Use this to isolate services from content passed in by consumers.

80
.agents/skills/angular-developer/references/host-elements.md

@ -0,0 +1,80 @@
# Component Host Elements
The **host element** is the DOM element that matches a component's selector. The component's template renders inside this element.
## Binding to the Host Element
Use the `host` property in the `@Component` decorator to bind properties, attributes, styles, and events to the host element. This is the **preferred approach** over legacy decorators.
```ts
@Component({
selector: 'custom-slider',
host: {
'role': 'slider', // Static attribute
'[attr.aria-valuenow]': 'value', // Attribute binding
'[class.active]': 'isActive()', // Class binding
'[style.color]': 'color()', // Style binding
'[tabIndex]': 'disabled ? -1 : 0', // Property binding
'(keydown)': 'onKeyDown($event)', // Event binding
},
})
export class CustomSlider {
value = 0;
disabled = false;
isActive = signal(false);
color = signal('blue');
onKeyDown(event: KeyboardEvent) {
/* ... */
}
}
```
## Legacy Decorators
`@HostBinding` and `@HostListener` are supported for backwards compatibility but should be avoided in new code.
```ts
export class CustomSlider {
@HostBinding('tabIndex')
get tabIndex() {
return this.disabled ? -1 : 0;
}
@HostListener('keydown', ['$event'])
onKeyDown(event: KeyboardEvent) {
/* ... */
}
}
```
## Binding Collisions
If both the component (host binding) and the consumer (template binding) bind to the same property:
1. **Static vs Static**: The instance (consumer) binding wins.
2. **Static vs Dynamic**: The dynamic binding wins.
3. **Dynamic vs Dynamic**: The component's host binding wins.
## Injecting Host Attributes
Use `HostAttributeToken` with the `inject` function to read static attributes from the host element at construction time.
```ts
import {Component, HostAttributeToken, inject} from '@angular/core';
@Component({
selector: 'app-btn',
template: `<ng-content />`,
})
export class AppButton {
// Throws error if 'type' is missing unless injected with { optional: true }
type = inject(new HostAttributeToken('type'));
}
```
Usage:
```html
<app-btn type="primary">Click Me</app-btn>
```

63
.agents/skills/angular-developer/references/injection-context.md

@ -0,0 +1,63 @@
# Injection Context
The `inject()` function can only be used when code is executing within an **injection context**.
## Where is an Injection Context Available?
An injection context is automatically available in:
1. **Field initializers** of classes instantiated by DI (`@Injectable`, `@Component`, `@Directive`, `@Pipe`).
2. **Constructors** of classes instantiated by DI.
3. **Factory functions** specified in `useFactory` or `InjectionToken` configurations.
4. **Functional APIs** executed by Angular (e.g., functional route guards, resolvers, interceptors).
```ts
@Component({...})
export class Example {
// ✅ Valid: Field initializer
private router = inject(Router);
constructor() {
// ✅ Valid: Constructor
const http = inject(HttpClient);
}
onClick() {
// ❌ Invalid: Not an injection context
// const auth = inject(AuthService);
}
}
```
## `runInInjectionContext`
If you need to run a function within an injection context (often needed for dynamic component creation or testing), use `runInInjectionContext`. This requires access to an existing injector (like `EnvironmentInjector` or `Injector`).
```ts
import {Injectable, inject, EnvironmentInjector, runInInjectionContext} from '@angular/core';
@Injectable({providedIn: 'root'})
export class MyService {
private injector = inject(EnvironmentInjector);
doSomethingDynamic() {
runInInjectionContext(this.injector, () => {
// ✅ Now valid to use inject() here
const router = inject(Router);
});
}
}
```
## `assertInInjectionContext`
Use `assertInInjectionContext` in utility functions to guarantee they are called from a valid context. It throws a clear error if not.
```ts
import {assertInInjectionContext, inject, ElementRef} from '@angular/core';
export function injectNativeElement<T extends Element>(): T {
assertInInjectionContext(injectNativeElement);
return inject(ElementRef).nativeElement;
}
```

101
.agents/skills/angular-developer/references/inputs.md

@ -0,0 +1,101 @@
# Inputs
Inputs allow data to flow from a parent component to a child component. Angular recommends using the signal-based `input` API for modern applications.
## Signal-based Inputs
Declare inputs using the `input()` function. This returns an `InputSignal`.
```ts
import {Component, input, computed} from '@angular/core';
@Component({
selector: 'app-user',
template: `<p>User: {{ name() }} ({{ age() }})</p>`,
})
export class User {
// Optional input with default value
name = input('Guest');
// Required input
age = input.required<number>();
// Inputs are reactive signals
label = computed(() => `Name: ${this.name()}`);
}
```
### Usage in Template
```html
<app-user [name]="userName" [age]="25" />
```
## Configuration Options
The `input` function accepts a config object:
- **Alias**: Change the property name used in templates.
- **Transform**: Modify the value before it reaches the component.
```ts
import { input, booleanAttribute } from '@angular/core';
@Component({...})
export class CustomButton {
// Alias example
label = input('', { alias: 'btnLabel' });
// Transform example using built-in helper
disabled = input(false, { transform: booleanAttribute });
}
```
## Model Inputs (Two-Way Binding)
Use `model()` to create an input that supports two-way data binding.
```ts
@Component({
selector: 'custom-counter',
template: `<button (click)="increment()">+</button>`,
})
export class CustomCounter {
value = model(0);
increment() {
this.value.update((v) => v + 1);
}
}
```
### Usage
```html
<!-- Two-way binding with a signal -->
<custom-counter [(value)]="mySignal" />
<!-- Two-way binding with a plain property -->
<custom-counter [(value)]="myProperty" />
```
## Decorator-based Inputs (@Input)
The legacy API remains supported but is not recommended for new code.
```ts
import { Component, Input } from '@angular/core';
@Component({...})
export class Legacy {
@Input({ required: true }) value = 0;
@Input({ transform: trimString }) label = '';
}
```
## Best Practices
- **Prefer Signals**: Use `input()` instead of `@Input()` for better reactivity and type safety.
- **Required Inputs**: Use `input.required()` for mandatory data to get build-time errors.
- **Pure Transforms**: Ensure input transform functions are pure and statically analyzable.
- **Avoid Collisions**: Do not use input names that collide with standard DOM properties (e.g., `id`, `title`).

59
.agents/skills/angular-developer/references/linked-signal.md

@ -0,0 +1,59 @@
# Dependent State with `linkedSignal`
The `linkedSignal` function lets you create writable state that is intrinsically linked to some other state. It is perfect for state that needs a default value derived from an input or another signal, but can still be independently modified by the user.
If the source state changes, the `linkedSignal` resets to a new computed value.
## Basic Usage
When you only need to recompute based on a source, pass a computation function. `linkedSignal` works like `computed`, but the resulting signal is writable (you can call `.set()` or `.update()` on it).
```ts
import { Component, signal, linkedSignal } from '@angular/core';
@Component({...})
export class ShippingMethodPicker {
shippingOptions = signal(['Ground', 'Air', 'Sea']);
// Defaults to the first option.
// If shippingOptions changes, selectedOption resets to the new first option.
selectedOption = linkedSignal(() => this.shippingOptions()[0]);
changeShipping(index: number) {
// We can still manually update this signal!
this.selectedOption.set(this.shippingOptions()[index]);
}
}
```
## Advanced Usage: Accounting for Previous State
Sometimes, when the source state changes, you want to preserve the user's manual selection if it is still valid. To do this, use the object syntax providing `source` and `computation`.
The `computation` function receives the new value of the source, and a `previous` object containing the previous source value and the previous `linkedSignal` value.
```ts
interface ShippingMethod { id: number; name: string; }
@Component({...})
export class ShippingMethodPicker {
shippingOptions = signal<ShippingMethod[]>([
{id: 0, name: 'Ground'}, {id: 1, name: 'Air'}, {id: 2, name: 'Sea'}
]);
selectedOption = linkedSignal<ShippingMethod[], ShippingMethod>({
source: this.shippingOptions,
computation: (newOptions, previous) => {
// If the newly loaded options still contain the user's previously
// selected option, keep it selected. Otherwise, reset to the first option.
return newOptions.find(opt => opt.id === previous?.value.id) ?? newOptions[0];
}
});
}
```
### When to use `linkedSignal` vs `computed` vs `effect`
- Use `computed`: When state is **strictly** derived from other state and should never be manually updated.
- Use `linkedSignal`: When state is derived from other state, but the user **must** be able to override or manually update it.
- **Never** use `effect` to sync one piece of state to another. That is an anti-pattern. Use `computed` or `linkedSignal` instead.

61
.agents/skills/angular-developer/references/loading-strategies.md

@ -0,0 +1,61 @@
# Route Loading Strategies
Angular supports two main strategies for loading routes and components to balance initial load time and navigation responsiveness.
## Eager Loading
Components are bundled into the initial JavaScript payload and are available immediately.
```ts
{ path: 'home', component: Home }
```
- **Pros**: Seamless transitions.
- **Cons**: Increases initial bundle size.
## Lazy Loading
Components or routes are loaded only when the user navigates to them. This creates separate JavaScript "chunks".
### Lazy Loading Components
Use `loadComponent` to fetch the component on demand.
```ts
{
path: 'admin',
loadComponent: () => import('./admin/admin.component').then(m => m.AdminComponent)`,
}
```
### Lazy Loading Child Routes
Use `loadChildren` to fetch a set of routes.
```ts
{
path: 'settings',
loadChildren: () => import('./settings/settings.routes'),
}
```
## Injection Context and Lazy Loading
Loader functions run within the **injection context** of the current route. This allows you to call `inject()` to make context-aware loading decisions.
```ts
{
path: 'dashboard',
loadComponent: () => {
const flags = inject(FeatureFlags);
return flags.isPremium
? import('./premium-dashboard')
: import('./basic-dashboard');
},
}
```
## Recommendation
- Use **Eager Loading** for the primary landing pages.
- Use **Lazy Loading** for all other feature areas to keep the initial bundle small.

106
.agents/skills/angular-developer/references/mcp.md

@ -0,0 +1,106 @@
# Angular CLI MCP Server
The Angular CLI includes a Model Context Protocol (MCP) server that enables AI assistants (like Cursor, Gemini CLI, JetBrains AI, etc.) to interact directly with the Angular CLI. It provides tools for project analysis, guided migrations, and running builds/tests.
## Available Tools (Default)
When the MCP server is enabled, AI agents have access to the following tools:
| Name | Description |
| :-------------------------- | :-------------------------------------------------------------------------------------------------------- |
| `ai_tutor` | Launches an interactive AI-powered Angular tutor. |
| `get_best_practices` | Retrieves the Angular Best Practices Guide (crucial for standalone components, typed forms, etc.). |
| `list_projects` | Lists all applications and libraries in the workspace by reading `angular.json`. |
| `onpush_zoneless_migration` | Analyzes code and provides a plan to migrate it to `OnPush` change detection (prerequisite for zoneless). |
| `search_documentation` | Searches the official documentation at `https://angular.dev`. |
## Experimental Tools
Some tools must be enabled explicitly using the `--experimental-tool` (or `-E`) flag.
| Name | Description |
| :------------------------- | :-------------------------------------------------------------------- |
| `build` | Performs a one-off build using `ng build`. |
| `devserver.start` | Asynchronously starts a dev server (`ng serve`). Returns immediately. |
| `devserver.stop` | Stops the dev server. |
| `devserver.wait_for_build` | Returns the logs of the most recent build in a running dev server. |
| `e2e` | Executes end-to-end tests. |
| `test` | Runs the project's unit tests. |
## Configuration
To use the MCP server, you configure your host environment (IDE or CLI) to run `npx @angular/cli mcp`.
### Antigravity IDE
Create a file named `.antigravity/mcp.json` in your project's root:
```json
{
"mcpServers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
```
### Gemini CLI
Create `.gemini/settings.json` in the project root:
```json
{
"mcpServers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
```
### Cursor
Create `.cursor/mcp.json` in the project root (or globally at `~/.cursor/mcp.json`):
```json
{
"mcpServers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
```
### VS Code
Create `.vscode/mcp.json`:
```json
{
"servers": {
"angular-cli": {
"command": "npx",
"args": ["-y", "@angular/cli", "mcp"]
}
}
}
```
## Command Options
You can pass arguments to the MCP server in the `args` array of your configuration:
- `--read-only`: Only registers tools that do not modify the project.
- `--local-only`: Only registers tools that do not require an internet connection.
- `--experimental-tool` (`-E`): Enables specific experimental tools (e.g., `-E build`, `-E devserver`).
Example for read-only mode with experimental tools enabled:
```json
"args": ["-y", "@angular/cli", "mcp", "--read-only", "-E", "build", "-E", "test"]
```

30
.agents/skills/angular-developer/references/migrations.md

@ -0,0 +1,30 @@
# Automatic Migrations & Code Modernization
When tasked with refactoring or modernizing an existing codebase, always prefer using the official automated schematics available in `@angular/core` over manual text replacement.
## Discovering Migrations
To view all available schematics for the installed version of the core framework, run:
`ng generate @angular/core: --help`
## Common Migration Schematics
Use the following commands to apply specific syntax updates. You can scope these commands to a specific project or directory using the `--project <name>` or `--path <dir>` flags.
| Feature to Modernize | Command to Execute |
| :------------------------ | :---------------------------------------------------------- |
| **Built-in Control Flow** | `ng generate @angular/core:control-flow` |
| **Signal-based Inputs** | `ng generate @angular/core:signal-input-migration` |
| **Signal Queries** | `ng generate @angular/core:signal-queries-migration` |
| **Functional Outputs** | `ng generate @angular/core:output-migration` |
| **`inject()` Function** | `ng generate @angular/core:inject` |
| **Self-Closing Tags** | `ng generate @angular/core:self-closing-tag` |
| **Standalone** | `ng generate @angular/core:standalone` (See workflow below) |
## Specialized Workflow: Migrating to Standalone
The Standalone migration is an interactive, multi-step refactoring. You **MUST** perform this in three discrete stages, verifying that the application builds and runs correctly after each stage completes:
1. **Phase 1**: Run `ng generate @angular/core:standalone` and select the option to **Convert all components, directives, and pipes to standalone**.
2. **Phase 2**: Verify the build with `ng build`. Run the command again and select **Remove unnecessary NgModule classes**.
3. **Phase 3**: Verify the build with `ng build`. Run the final pass and select **Bootstrap the project using standalone APIs**.

69
.agents/skills/angular-developer/references/navigate-to-routes.md

@ -0,0 +1,69 @@
# Navigate to Routes
Angular provides both declarative and programmatic ways to navigate between routes.
## Declarative Navigation (`RouterLink`)
Use the `RouterLink` directive on anchor elements.
```ts
import {RouterLink, RouterLinkActive} from '@angular/router';
@Component({
imports: [RouterLink, RouterLinkActive],
template: `
<nav>
<a routerLink="/dashboard" routerLinkActive="active-link">Dashboard</a>
<a [routerLink]="['/user', userId]">Profile</a>
</nav>
`,
})
export class Nav {
userId = '123';
}
```
- **Absolute Paths**: Start with `/` (e.g., `/settings`).
- **Relative Paths**: No leading `/`. Use `../` to go up a level.
## Programmatic Navigation (`Router`)
Inject the `Router` service to navigate via TypeScript code.
### `router.navigate()`
Uses an array of commands.
```ts
private router = inject(Router);
private route = inject(ActivatedRoute);
// Standard navigation
this.router.navigate(['/profile']);
// With parameters
this.router.navigate(['/search'], {
queryParams: { q: 'angular' },
fragment: 'results'
});
// Relative navigation
this.router.navigate(['edit'], { relativeTo: this.route });
```
### `router.navigateByUrl()`
Uses a string path. Ideal for absolute navigation or full URLs.
```ts
this.router.navigateByUrl('/products/123?view=details');
// Replace current entry in history
this.router.navigateByUrl('/login', {replaceUrl: true});
```
## URL Parameters
- **Route Params**: Part of the path (e.g., `/user/123`).
- **Query Params**: After the `?` (e.g., `/search?q=query`).
- **Matrix Params**: Scoped to a segment (e.g., `/products;category=books`).

86
.agents/skills/angular-developer/references/outputs.md

@ -0,0 +1,86 @@
# Outputs (Custom Events)
Outputs allow a child component to emit custom events that a parent component can listen to. Angular recommends using the new `output()` function for modern applications.
## Function-based outputs
Declare outputs using the `output()` function. This returns an `OutputEmitterRef`.
```ts
import {Component, output} from '@angular/core';
@Component({
selector: 'custom-slider',
template: `<button (click)="changeValue(50)">Set to 50</button>`,
})
export class CustomSlider {
// Output without event data
panelClosed = output<void>();
// Output with event data (number)
valueChanged = output<number>();
changeValue(newValue: number) {
this.valueChanged.emit(newValue);
}
}
```
### Usage in Template
Bind to the output event using parentheses `()`. If the event emits data, access it using the special `$event` variable.
```html
<custom-slider (panelClosed)="savePanelState()" (valueChanged)="logValue($event)" />
```
## Configuration Options
The `output` function accepts a config object to specify an alias.
```ts
@Component({...})
export class CustomSlider {
// The event is named 'valueChanged' in the template,
// but accessed as 'changed' in the component class.
changed = output<number>({ alias: 'valueChanged' });
}
```
## Programmatic Subscription
When creating components dynamically, you can subscribe to outputs programmatically:
```ts
const componentRef = viewContainerRef.createComponent(CustomSlider);
const subscription = componentRef.instance.valueChanged.subscribe((val) => {
console.log('Value changed:', val);
});
// Clean up manually if needed (Angular cleans up destroyed components automatically)
subscription.unsubscribe();
```
## Decorator-based Outputs (@Output)
The legacy API uses the `@Output()` decorator with an `EventEmitter`. It remains supported but is not recommended for new code.
```ts
import { Component, Output, EventEmitter } from '@angular/core';
@Component({...})
export class LegacyExample {
@Output() valueChanged = new EventEmitter<number>();
// With alias
@Output('customEventName') changed = new EventEmitter<void>();
}
```
## Best Practices
- **Prefer `output()`**: Use the function-based `output()` instead of `@Output()` and `EventEmitter`.
- **Naming**: Use `camelCase` for output names. Avoid prefixing with `on` (e.g., use `valueChanged` instead of `onValueChanged`).
- **No DOM Bubbling**: Angular custom events do not bubble up the DOM tree like native events.
- **Avoid Collisions**: Do not choose names that collide with native DOM events (like `click` or `submit`).

122
.agents/skills/angular-developer/references/reactive-forms.md

@ -0,0 +1,122 @@
# Reactive Forms
Reactive forms provide a model-driven approach to handling form inputs. They are built around observable streams and provide synchronous access to the data model, making them more scalable and testable than template-driven forms.
## Core Classes
Reactive forms are built using these fundamental classes from `@angular/forms`:
- `FormControl`: Manages the value and validity of an individual input.
- `FormGroup`: Manages a group of controls (an object-like structure).
- `FormArray`: Manages a numerically indexed array of controls.
- `FormBuilder`: A service that provides factory methods for creating control instances.
## Setup
Import `ReactiveFormsModule` into your component.
```ts
import {Component, inject} from '@angular/core';
import {ReactiveFormsModule, FormGroup, FormControl, Validators, FormBuilder} from '@angular/forms';
@Component({
selector: 'app-profile-editor',
imports: [ReactiveFormsModule],
templateUrl: './profile-editor.component.html',
})
export class ProfileEditor {
private fb = inject(FormBuilder);
// Using FormBuilder for concise definition
profileForm = this.fb.group({
firstName: ['', Validators.required],
lastName: [''],
address: this.fb.group({
street: [''],
city: [''],
}),
aliases: this.fb.array([this.fb.control('')]),
});
onSubmit() {
console.warn(this.profileForm.value);
}
}
```
## Template Binding
Use directives to bind the model to the view:
- `[formGroup]`: Binds a `FormGroup` to a `<form>` or `<div>`.
- `formControlName`: Binds a named control within a group to an input.
- `formGroupName`: Binds a nested `FormGroup`.
- `formArrayName`: Binds a nested `FormArray`.
- `[formControl]`: Binds a standalone `FormControl`.
```html
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<input type="text" formControlName="firstName" />
<div formGroupName="address">
<input type="text" formControlName="street" />
</div>
<div formArrayName="aliases">
@for (alias of aliases.controls; track $index) {
<input type="text" [formControlName]="$index" />
}
</div>
<button type="submit" [disabled]="!profileForm.valid">Submit</button>
</form>
```
## Accessing Controls
Use getters for easy access to controls, especially for `FormArray`.
```ts
get aliases() {
return this.profileForm.get('aliases') as FormArray;
}
addAlias() {
this.aliases.push(this.fb.control(''));
}
```
## Updating Values
- `patchValue()`: Updates only the specified properties. Fails silently on structural mismatches.
- `setValue()`: Replaces the entire model. Strictly enforces the form structure.
```ts
updateProfile() {
this.profileForm.patchValue({
firstName: 'Nancy',
address: { street: '123 Drew Street' }
});
}
```
## Unified Change Events
Modern Angular (v18+) provides a single `events` observable on all controls to track value, status, pristine, touched, reset, and submit events.
```ts
import {ValueChangeEvent, StatusChangeEvent} from '@angular/forms';
this.profileForm.events.subscribe((event) => {
if (event instanceof ValueChangeEvent) {
console.log('New value:', event.value);
}
});
```
## Manual State Management
- `markAsTouched()` / `markAllAsTouched()`: Useful for showing validation errors on submit.
- `markAsDirty()` / `markAsPristine()`: Tracks if the value has been modified.
- `updateValueAndValidity()`: Manually triggers recalculation of value and status.
- Options `{ emitEvent: false }` or `{ onlySelf: true }` can be passed to most methods to control propagation.

44
.agents/skills/angular-developer/references/rendering-strategies.md

@ -0,0 +1,44 @@
# Rendering Strategies
Angular supports multiple rendering strategies to optimize for SEO, performance, and interactivity.
## 1. Client-Side Rendering (CSR)
**Default Strategy.** Content is rendered entirely in the browser.
- **Use case**: Interactive dashboards, internal tools.
- **Pros**: Simplest to configure, low server cost.
- **Cons**: Poor SEO, slower initial content visibility (must wait for JS).
## 2. Static Site Generation (SSG / Prerendering)
Content is pre-rendered into static HTML files at **build time**.
- **Use case**: Marketing pages, blogs, documentation.
- **Pros**: Fastest initial load, excellent SEO, CDN-friendly.
- **Cons**: Requires rebuild for content updates, not for user-specific data.
## 3. Server-Side Rendering (SSR)
Content is rendered on the server for the **initial request**. Subsequent navigations happen client-side (SPA style).
- **Use case**: E-commerce product pages, news sites, personalized dynamic content.
- **Pros**: Excellent SEO, fast initial content visibility.
- **Cons**: Requires a server (Node.js), higher server cost/latency.
## Hydration
Hydration is the process of making server-rendered HTML interactive in the browser.
- **Full Hydration**: The entire app becomes interactive at once.
- **Incremental Hydration**: (Advanced) Parts become interactive as needed using `@defer` blocks.
- **Event Replay**: Captures and replays user events that happened before hydration finished.
## Decision Matrix
| Requirement | Strategy |
| :------------------------------ | :------------------- |
| **SEO + Static Content** | SSG |
| **SEO + Dynamic Content** | SSR |
| **No SEO + High Interactivity** | CSR |
| **Mixed** | Hybrid (Route-based) |

77
.agents/skills/angular-developer/references/resource.md

@ -0,0 +1,77 @@
# Async Reactivity with `resource`
> [!IMPORTANT]
> The `resource` API is currently experimental in Angular.
A `Resource` incorporates asynchronous data fetching into Angular's signal-based reactivity. It executes an async loader function whenever its dependencies change, exposing the status and result as synchronous signals.
## Basic Usage
The `resource` function accepts an options object with two main properties:
1. `params`: A reactive computation (like `computed`). When signals read here change, the resource re-fetches.
2. `loader`: An async function that fetches data based on the parameters.
```ts
import { Component, resource, signal, computed } from '@angular/core';
@Component({...})
export class UserProfile {
userId = signal('123');
userResource = resource({
// Reactively tracking userId
params: () => ({ id: this.userId() }),
// Executes whenever params change
loader: async ({ params, abortSignal }) => {
const response = await fetch(`/api/users/${params.id}`, { signal: abortSignal });
if (!response.ok) throw new Error('Network error');
return response.json();
}
});
// Use the resource value in computed signals
userName = computed(() => {
if (this.userResource.hasValue()) {
return this.userResource.value()?.name;
} else {
return 'Loading...';
}
});
}
```
## Aborting Requests
If the `params` signal changes while a previous loader is still running, the `Resource` will attempt to abort the outstanding request using the provided `abortSignal`. **Always pass `abortSignal` to your `fetch` calls.**
## Reloading Data
You can imperatively force the resource to re-run the loader without the params changing by calling `.reload()`.
```ts
this.userResource.reload();
```
## Resource Status Signals
The `Resource` object provides several signals to read its current state:
- `value()`: The resolved data, or `undefined`.
- `hasValue()`: Type-guard boolean. `true` if a value exists.
- `isLoading()`: Boolean indicating if the loader is currently running.
- `error()`: The error thrown by the loader, or `undefined`.
- `status()`: A string constant representing the exact state (`'idle'`, `'loading'`, `'resolved'`, `'error'`, `'reloading'`, `'local'`).
## Local Mutation
You can optimistically update the resource's value directly. This changes the status to `'local'`.
```ts
this.userResource.value.set({name: 'Optimistic Update'});
```
## Reactive Data Fetching with `httpResource`
If you are using Angular's `HttpClient`, prefer using `httpResource`. It is a specialized wrapper that leverages the Angular HTTP stack (including interceptors) while providing the same signal-based resource API.

56
.agents/skills/angular-developer/references/route-animations.md

@ -0,0 +1,56 @@
# Route Transition Animations
Angular Router supports the browser's **View Transitions API** for smooth visual transitions between routes.
## Enabling View Transitions
Add `withViewTransitions()` to your router configuration.
```ts
provideRouter(routes, withViewTransitions());
```
This is a **progressive enhancement**. In browsers that don't support the API, the router will still work but without the transition animation.
## How it Works
1. Browser takes a screenshot of the old state.
2. Router updates the DOM (activates new component).
3. Browser takes a screenshot of the new state.
4. Browser animates between the two states.
## Customizing with CSS
Transitions are customized in **global CSS files** (not component-scoped CSS).
Use the `::view-transition-old()` and `::view-transition-new()` pseudo-elements.
```css
/* Example: Cross-fade + Slide */
::view-transition-old(root) {
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out;
}
::view-transition-new(root) {
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in;
}
```
## Advanced Control
Use `onViewTransitionCreated` to skip transitions or customize behavior based on the navigation context.
```ts
withViewTransitions({
onViewTransitionCreated: ({transition, from, to}) => {
// Skip animation for specific routes
if (to.url === '/no-animation') {
transition.skipTransition();
}
},
});
```
## Best Practices
- **Global Styles**: Always define transition animations in `styles.css` to avoid view encapsulation issues.
- **View Transition Names**: Assign unique `view-transition-name` to elements that should transition smoothly across routes (e.g., a header image).

52
.agents/skills/angular-developer/references/route-guards.md

@ -0,0 +1,52 @@
# Route Guards
Route guards control whether a user can navigate to or leave a route.
## Types of Guards
- **`CanActivate`**: Can the user access this route? (e.g., Auth check).
- **`CanActivateChild`**: Can the user access children of this route?
- **`CanDeactivate`**: Can the user leave this route? (e.g., Unsaved changes).
- **`CanMatch`**: Should this route even be considered for matching? (e.g., Feature flags). If it returns `false`, the router continues checking other routes.
## Creating a Guard
Guards are typically functional since Angular 15.
```ts
export const authGuard: CanActivateFn = (route, state) => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isLoggedIn()) {
return true;
}
// Redirect to login
return router.parseUrl('/login');
};
```
## Applying Guards
Add them to the route configuration as an array. They execute in order.
```ts
{
path: 'admin',
component: Admin,
canActivate: [authGuard],
canActivateChild: [adminChildGuard],
canDeactivate: [unsavedChangesGuard]
}
```
## Return Values
- `boolean`: `true` to allow, `false` to block.
- `UrlTree` or `RedirectCommand`: Redirect to a different route.
- `Observable` or `Promise`: Resolves to the above types.
## Security Note
**Client-side guards are NOT a substitute for server-side security.** Always verify permissions on the server.

45
.agents/skills/angular-developer/references/router-lifecycle.md

@ -0,0 +1,45 @@
# Router Lifecycle and Events
Angular Router emits events through the `Router.events` observable, allowing you to track the navigation lifecycle from start to finish.
## Common Router Events (Chronological)
1. **`NavigationStart`**: Navigation begins.
2. **`RoutesRecognized`**: Router matches the URL to a route.
3. **`GuardsCheckStart` / `End`**: Evaluation of `canActivate`, `canMatch`, etc.
4. **`ResolveStart` / `End`**: Data resolution phase (fetching data via resolvers).
5. **`NavigationEnd`**: Navigation completed successfully.
6. **`NavigationCancel`**: Navigation canceled (e.g., guard returned `false`).
7. **`NavigationError`**: Navigation failed (e.g., error in resolver).
## Subscribing to Events
Inject the `Router` and filter the `events` observable.
```ts
import {Router, NavigationStart, NavigationEnd} from '@angular/router';
export class MyService {
private router = inject(Router);
constructor() {
this.router.events.pipe(filter((e) => e instanceof NavigationEnd)).subscribe((event) => {
console.log('Navigated to:', event.url);
});
}
}
```
## Debugging
Enable detailed console logging of all routing events during application bootstrap.
```ts
provideRouter(routes, withDebugTracing());
```
## Common Use Cases
- **Loading Indicators**: Show a spinner when `NavigationStart` fires and hide it on `NavigationEnd`/`Cancel`/`Error`.
- **Analytics**: Track page views by listening for `NavigationEnd`.
- **Scroll Management**: Respond to `Scroll` events for custom scroll behavior.

87
.agents/skills/angular-developer/references/router-testing.md

@ -0,0 +1,87 @@
# Testing with the RouterTestingHarness
When testing components that involve routing, it is crucial **not to mock the Router or related services**. Instead, use the `RouterTestingHarness`, which provides a robust and reliable way to test routing logic in an environment that closely mirrors a real application.
Using the harness ensures you are testing the actual router configuration, guards, and resolvers, leading to more meaningful tests.
## Setting Up for Router Testing
The `RouterTestingHarness` is the primary tool for testing routing scenarios. You also need to provide your test routes using the `provideRouter` function in your `TestBed` configuration.
### Example Setup
```ts
import {TestBed} from '@angular/core/testing';
import {provideRouter} from '@angular/router';
import {RouterTestingHarness} from '@angular/router/testing';
import {Dashboard} from './dashboard.component';
import {HeroDetail} from './hero-detail.component';
describe('Dashboard Component Routing', () => {
let harness: RouterTestingHarness;
beforeEach(async () => {
// 1. Configure TestBed with test routes
await TestBed.configureTestingModule({
providers: [
// Use provideRouter with your test-specific routes
provideRouter([
{path: '', component: Dashboard},
{path: 'heroes/:id', component: HeroDetail},
]),
],
}).compileComponents();
// 2. Create the RouterTestingHarness
harness = await RouterTestingHarness.create();
});
});
```
### Key Concepts
1. **`provideRouter([...])`**: Provide a test-specific routing configuration. This should include the routes necessary for the component-under-test to function correctly.
2. **`RouterTestingHarness.create()`**: Asynchronously creates and initializes the harness and performs an initial navigation to the root URL (`/`).
## Writing Router Tests
Once the harness is created, you can use it to drive navigation and make assertions on the state of the router and the activated components.
### Example: Testing Navigation
```ts
it('should navigate to a hero detail when a hero is selected', async () => {
// 1. Navigate to the initial component and get its instance
const dashboard = await harness.navigateByUrl('/', Dashboard);
// Suppose the dashboard has a method to select a hero
const heroToSelect = {id: 42, name: 'Test Hero'};
dashboard.selectHero(heroToSelect);
// Wait for stability after the action that triggers navigation
await harness.fixture.whenStable();
// 2. Assert on the URL
expect(harness.router.url).toEqual('/heroes/42');
// 3. Get the activated component after navigation
const heroDetail = await harness.getHarness(HeroDetail);
// 4. Assert on the state of the new component
expect(await heroDetail.componentInstance.hero.name).toBe('Test Hero');
});
it('should get the activated component directly', async () => {
// Navigate and get the component instance in one step
const dashboardInstance = await harness.navigateByUrl('/', Dashboard);
expect(dashboardInstance).toBeInstanceOf(Dashboard);
});
```
### Best Practices
- **Navigate with the Harness:** Always use `harness.navigateByUrl()` to simulate navigation. This method returns a promise that resolves with the instance of the activated component.
- **Access the Router State:** Use `harness.router` to access the live router instance and assert on its state (e.g., `harness.router.url`).
- **Get Activated Components:** Use `harness.getHarness(ComponentType)` to get an instance of a component harness for the currently activated routed component, or `harness.routeDebugElement` to get the `DebugElement`.
- **Wait for Stability:** After performing an action that causes navigation, always `await harness.fixture.whenStable()` to ensure the routing is complete before making assertions.

68
.agents/skills/angular-developer/references/show-routes-with-outlets.md

@ -0,0 +1,68 @@
# Show Routes with Outlets
The `RouterOutlet` directive is a placeholder where Angular renders the component for the current URL.
## Basic Usage
Include `<router-outlet />` in your template. Angular inserts the routed component as a sibling immediately following the outlet.
```html
<app-header /> <router-outlet />
<!-- Route content appears here -->
<app-footer />
```
## Nested Outlets
Child routes require their own `<router-outlet />` within the parent component's template.
```ts
// Parent component template
<h1>Settings</h1>
<router-outlet /> <!-- Child components like Profile or Security render here -->
```
## Named Outlets (Secondary Routes)
Pages can have multiple outlets. Assign a `name` to an outlet to target it specifically. The default name is `'primary'`.
```html
<router-outlet />
<!-- Primary -->
<router-outlet name="sidebar" />
<!-- Secondary -->
```
Define the `outlet` in the route config:
```ts
{
path: 'chat',
component: Chat,
outlet: 'sidebar'
}
```
## Outlet Lifecycle Events
`RouterOutlet` emits events when components are changed:
- `activate`: New component instantiated.
- `deactivate`: Component destroyed.
- `attach` / `detach`: Used with `RouteReuseStrategy`.
```html
<router-outlet (activate)="onActivate($event)" />
```
## Passing Data via `routerOutletData`
You can pass contextual data to the routed component using the `routerOutletData` input. The component accesses this via the `ROUTER_OUTLET_DATA` injection token as a signal.
```ts
// In Parent
<router-outlet [routerOutletData]="{ theme: 'dark' }" />
// In Routed Component
outletData = inject(ROUTER_OUTLET_DATA) as Signal<{ theme: string }>;
```

897
.agents/skills/angular-developer/references/signal-forms.md

@ -0,0 +1,897 @@
# Signal Forms
Signal Forms are the recommended approach for handling forms in modern Angular applications (v21+). They provide a reactive, type-safe, and model-driven way to manage form state using Angular Signals.
**CRITICAL**: You MUST use Angular's new Signal Forms API for all form-related functionality. Do NOT use null as a value or type of any fields.
## Imports
You can import the following from `@angular/forms/signals`:
```ts
import {
form,
FormField,
submit,
// Rules for field state
disabled,
hidden,
readonly,
debounce,
// Schema helpers
applyWhen,
applyEach,
schema,
// Custom validation
validate,
validateHttp,
validateStandardSchema,
// Metadata
metadata,
} from '@angular/forms/signals';
```
## Creating a Form
Use the `form()` function with a Signal model. The structure of the form is derived directly from the model.
```ts
import {Component, signal} from '@angular/core';
import {form, FormField} from '@angular/forms/signals';
@Component({
// ...
imports: [FormField],
})
export class Example {
// 1. Define your model with initial values (avoid undefined)
userModel = signal({
name: '', // CRITICAL: NEVER use null or undefined as initial values
email: '',
age: 0, // Use 0 for numbers, NOT null
address: {
street: '',
city: '',
},
hobbies: [] as string[], // Use [] for arrays, NOT null
});
// WRONG - DO NOT DO THIS:
// badModel = signal({
// name: null, // ERROR: use '' instead
// age: null, // ERROR: use 0 instead
// items: null // ERROR: use [] instead
// });
// 2. Create the form
userForm = form(this.userModel);
}
```
## Validation
Import validators from `@angular/forms/signals`.
```ts
import {required, email, min, max, minLength, maxLength, pattern} from '@angular/forms/signals';
```
Use them in the schema function passed to `form()`:
```ts
userForm = form(this.userModel, (schemaPath) => {
// Required
required(schemaPath.name, {message: 'Name is required'});
// Conditional required.
required(schemaPath.name, {
when({valueOf}) {
return valueOf(schemaPath.age) > 10;
},
});
// when is only available for required
// Do NOT do this: pattern(p.name, /xxx/, {when /* ERROR */)
// Email
email(schemaPath.email, {message: 'Invalid email'});
// Min/Max for numbers
min(schemaPath.age, 18);
max(schemaPath.age, 100);
// MinLength/MaxLength for strings/arrays
minLength(schemaPath.password, 8);
maxLength(schemaPath.description, 500);
// Pattern (Regex)
pattern(schemaPath.zipCode, /^\d{5}$/);
});
```
## FieldState vs FormField: The Parental Requirement
It's important to understand the difference between **FormField** (the structure) and **FieldState** (the actual data/signals).
**RULE**: You must **CALL** a field as a function to access its state signals (valid, touched, dirty, hidden, etc.).
```ts
// f is a FormField (structural)
const f = form(signal({cat: {name: 'pirojok-the-cat', age: 5}}));
f.cat.name; // FormField: You can't get flags from here!
f.cat.name.touched(); // ERROR: touched() does not exist on FormField
f.cat.name(); // FieldState: Calling it gives you access to signals
f.cat.name().touched(); // VALID: Accessing the signal
f.cat().name.touched(); // ERROR: f.cat() is state, it doesn't have children!
```
Similarly in a template:
```html
<!-- WRONG: Property 'hidden' does not exist on type 'FormField' -->
@if (bookingForm.hotelDetails.hidden()) { ... }
<!-- RIGHT: Call it first -->
@if (bookingForm.hotelDetails().hidden()) { ... }
```
## Disabled / Readonly / Hidden
Control field status using rules in the schema.
```ts
import {disabled, readonly, hidden} from '@angular/forms/signals';
userForm = form(this.userModel, (schemaPath) => {
// Conditionally disabled
disabled(schemaPath.password, ({valueOf}) => !valueOf(schemaPath.createAccount));
// Conditionally hidden (does NOT remove from model, just marks as hidden)
hidden(schemaPath.shippingAddress, ({valueOf}) => valueOf(schemaPath.sameAsBilling));
// Readonly
readonly(schemaPath.username);
});
```
## Binding
Import `FormField` and use the `[formField]` directive.
```ts
import {FormField} from '@angular/forms/signals';
```
All props on state, such as `disabled`, `hidden`, `readonly` and `name` are bound automatically.
Do _NOT_ bind the `name` field.
**CRITICAL: FORBIDDEN ATTRIBUTES**
When using `[formField]`, you MUST NOT set the following attributes in the template (either static or bound):
- `min`, `max` (Use validators in the schema instead)
- `value`, `[value]`, `[attr.value]` (Already handled by `[formField]`)
- `[attr.min]`, `[attr.max]`
- `[disabled]`, `[readonly]` (Already handled by `[formField]`)
Do NOT do this: `<input min="1" [formField]>` or `<input [value]="val" [formField]>`.
```html
<!-- Input -->
<input [formField]="userForm.name" />
<!-- Checkbox -->
<input type="checkbox" [formField]="userForm.isAdmin" />
<!-- Select -->
<select [formField]="userForm.country">
<option value="us">US</option>
</select>
<!-- userForm.name can NOT be nullable, because input does not accept null-->
<input [formField]="userForm.name" />
```
## Reactive Forms
**Do NOT import** `FormControl`, `FormGroup`, `FormArray`, or `FormBuilder` from `@angular/forms`. Signal Forms replace these concepts entirely.
Signal forms does NOT have a builder.
## Accessing State
Each field in the form is a function that returns its state.
```ts
// Access the field by calling it
const emailState = this.userForm.email();
// Value (WritableSignal)
const value = this.userForm().value();
// Validation State (Signals)
const isValid = this.userForm().valid();
const isInvalid = this.userForm().invalid();
const errors = this.userForm().errors(); // Array of errors
const isPending = this.userForm().pending(); // Async validation pending
// Interaction State (Signals)
const isTouched = this.userForm().touched();
const isDirty = this.userForm().dirty();
// Availability State (Signals)
const isDisabled = this.userForm().disabled();
const isHidden = this.userForm().hidden();
const isReadonly = this.userForm().readonly();
```
IMPORTANT!: Make sure to call the field to get it state.
```ts
form().invalid()
form.field().dirty()
form.field.subfield().touched()
form.a.b.c.d().value()
form.address.ssn().pending()
form().reset()
// The only exception is length:
form.children.length
form.length // NOTE: no parenthesis!
form.client.addresses.length // No "()"
@for (income of form.addresses; track $index) {/**/}
```
## Submitting
Use the `submit()` function. It automatically marks all fields as touched before running the action.
**CRITICAL**: The callback to `submit()` MUST be `async` and MUST return a Promise.
```ts
import { submit } from '@angular/forms/signals';
// CORRECT - async callback
onSubmit() {
submit(this.userForm, async () => {
// This only runs if the form is valid
await this.apiService.save(this.userModel());
console.log('Saved!');
});
}
// WRONG - missing async keyword
onSubmit() {
submit(this.userForm, () => { // ERROR: must be async
console.log('Saved!');
});
}
```
## Handling Errors
`field().errors()` returns the errors array of ValidationError:
```ts
interface ValidationError {
readonly kind: string;
readonly message?: string;
}
```
Do _NOT_ return null from validators.
When there are no errors, return undefined
### Context
Functions passed to rules like `validate()`, `disabled()`, `applyWhen` take a context object. It is **CRITICAL** to understand its structure:
```ts
validate(
schemaPath.username,
({
value, // Signal<T>: Writable current value of the field
fieldTree, // FieldTree<T>: Sub-fields (if it's a group/array)
state, // FieldState<T>: Access flags like state.valid(), state.dirty()
valueOf, // (path) => T: Read values of OTHER fields (tracking dependencies), e.g. valueOf(schemaPath.password)
stateOf, // (path) => FieldState: Access state (valid/dirty) of OTHER fields, e.g. stateOf(schemaPath.password).valid()
pathKeys, // Signal<string[]>: Path from root to this field
}) => {
// WRONG: if (touched()) ... (touched is not in context)
// RIGHT: if (state.touched()) ...
if (value() === 'admin') {
return {kind: 'reserved', message: 'Username admin is reserved'};
}
},
);
```
### IMPORTANT: Paths are NOT Signals
Inside the `form()` callback, `schemaPath` and its children (e.g., `schemaPath.user.name`) are **NOT** signals and are **NOT** callable.
```ts
// WRONG - This will throw an error:
applyWhen(p.ssn, () => p.ssn().touched(), (ssnField) => { ... });
// RIGHT - Use stateOf() to get the state of a path:
applyWhen(p.ssn, ({ stateOf }) => stateOf(p.ssn).touched(), (ssnField) => { ... });
// RIGHT - Use valueOf() to get the value of a path:
applyWhen(p.ssn, ({ valueOf }) => valueOf(p.ssn) !== '', (ssnField) => { ... });
```
### Multiple Items
- Use `applyEach` for applying rules per item.
- **CRITICAL**: `applyEach` callback takes ONLY ONE argument (the item path), NOT two:
```ts
// CORRECT - single argument
applyEach(s.items, (item) => {
required(item.name);
});
// WRONG - do NOT pass index
applyEach(s.items, (item, index) => {
// ERROR: callback takes 1 argument
required(item.name);
});
```
- In the template use `@for` to iterate over the items.
- To remove an item from an array, just remove appropriate item from the array in the data.
- **`select` binding**: You CAN bind to `<select [formField]="form.country">`. Ensure options have `value` attributes.
### Nested @for Loops
**CRITICAL**: Angular does NOT have `$parent`. In nested loops, store outer index in a variable:
```html
<!-- WRONG - $parent does not exist -->
@for (item of form.items; track $index) { @for (option of item.options; track $index) {
<button (click)="removeOption($parent.$index, $index)">Remove</button>
<!-- ERROR -->
} }
<!-- CORRECT - use let to store outer index -->
@for (item of form.items; track $index; let outerIndex = $index) { @for (option of item.options;
track $index) {
<button (click)="removeOption(outerIndex, $index)">Remove</button>
} }
```
### Disabling Form Button
```html
<button [disabled]="form().invalid() || form().pending()" />
<!-- Or -->
<button [disabled]="taxForm.invalid()" />
```
Do NOT use `[disabled]` on an input. `[formField]` will do this.
Do NOT use `[readonly]` on an input. `[formField]` will do this.
If you need to disable or readonly a field, use `disabled()` or `readonly()` rules in the schema.
### Async Validation
Do not use `validate()` for async, instead use `validateAsync()`:
**CRITICAL**:
1. The `params` option MUST be a function that returns the value to validate.
2. The `onError` handler is **REQUIRED** - it is NOT optional!
```ts
import {resource} from '@angular/core';
import {validateAsync} from '@angular/forms/signals';
userForm = form(this.userModel, (s) => {
validateAsync(s.username, {
// 1. MUST be a function - params takes context and returns the value
params: ({value}) => value(),
// 2. Create the resource - factory receives a Signal
factory: (username) =>
resource({
params: username, // Use 'params' in resource()
loader: async ({params: value}) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
return value === 'taken';
},
}),
// 3. Map success to errors
onSuccess: (isTaken) =>
isTaken ? {kind: 'taken', message: 'Username is already taken'} : undefined,
// 4. Handle errors - THIS IS REQUIRED!
onError: () => ({kind: 'error', message: 'Validation failed'}),
});
});
```
**WRONG Examples:**
```ts
// WRONG - params must be a function
validateAsync(s.username, {
params: s.username, // ERROR: must be ({ value }) => value()
// ...
});
// WRONG - missing onError (it's required!)
validateAsync(s.username, {
params: ({value}) => value(),
factory: (username) =>
resource({
/* ... */
}),
onSuccess: (result) => (result ? {kind: 'error'} : undefined),
// ERROR: 'onError' is missing but required!
});
```
### Using Resource
**CRITICAL**: In Angular's `resource()`, use `params` for the input signal.
```ts
// CORRECT
resource({
params: mySignal,
loader: async ({params: value}) => {
/* ... */
},
});
// WRONG
resource({
request: mySignal, // ERROR: should be 'params'
loader: async ({request}) => {
/* ... */
},
});
```
Use `debounce()` to delay synchronization between the UI and the model.
```ts
import {debounce} from '@angular/forms/signals';
userForm = form(this.userModel, (s) => {
// Delay model updates by 300ms
debounce(s.username, 300);
});
```
### Conditional Validation
```ts
form(
data,
(path) => {
applyWhen(
name,
({value}) => value() !== 'admin',
(namePath) => {
validate(namePath.last /* ... */);
disable(namePath.last /* ... */);
},
);
},
{injector: TestBed.inject(Injector)},
);
```
`applyWhen` passes the path mapped to the first argument.
If you need parent field, just pass it to `applyWhen`:
```ts
form(
data,
(path) => {
applyWhen(
cat,
({value}) => value().name !== 'admin',
(catPath) => {
require(cat.catPath /* ... */);
},
);
},
{injector: TestBed.inject(Injector)},
);
```
## Common Pitfalls (DO NOT DO THESE)
| Error Scenario | WRONG (Common Mistake) | RIGHT (Correct Way) |
| :--------------------- | :-------------------------------------------- | :---------------------------------------------------------- |
| **Accessing Flags** | `form.field.valid()` | `form.field().valid()` |
| **Accessing value** | `form.field.value()` | `form.field().value()` |
| **Setting value** | `form.field.set(x)` | Update model signal: `this.model.update(...)` |
| **Form root flags** | `form.invalid()` | `form().invalid()` |
| **Double-calling** | `form.field()()` | `form.field().value()` |
| **Rules Context** | `({ touched }) => touched()` | `({ state }) => state.touched()` |
| **Calling Paths** | `applyWhen(p.foo, () => p.foo() === 'x')` | `applyWhen(p.foo, ({ valueOf }) => valueOf(p.foo) === 'x')` |
| **applyWhen args** | `applyWhen(condition, () => {...})` | `applyWhen(path, condition, schemaFn)` - needs 3 args |
| **Array length** | `form.items().length` | `form.items.length` (structural) |
| **Multi-select array** | `<select [formField]="form.tags">` (string[]) | Use checkboxes for array fields |
| **readonly attribute** | `<input readonly [formField]>` | Use `readonly()` rule in schema |
| **min/max attributes** | `<input min="1" max="10">` | Use `min()` and `max()` rules in schema |
| **value binding** | `<input [value]="val">` | Do NOT use `[value]` with `[formField]` |
| **when option** | `pattern(p.x, /.../, {when: ...})` | `when` only works with `required()` |
| **Submit callback** | `submit(form, () => { ... })` | `submit(form, async () => { ... })` |
| **Async params** | `params: s.field` | `params: ({ value }) => value()` |
| **Async onError** | Omitting `onError` | `onError` is REQUIRED in `validateAsync` |
| **resource() API** | `request: signal` | `params: signal` |
| **applyEach args** | `applyEach(s.items, (item, index) => ...)` | `applyEach(s.items, (item) => ...)` |
| **Nested @for** | `$parent.$index` | Use `let outerIndex = $index` |
| **FormState import** | `import { FormState }` | `FormState` does not exist, use `FieldState` |
| **Null in model** | `signal({ name: null })` | `signal({ name: '' })` or `signal({ age: 0 })` |
| **Validate syntax** | `validate(s.field, { value } => ...)` | `validate(s.field, ({ value }) => ...)` |
| **Checkbox Array** | `[formField]="form.tags"` (string[]) | Checkboxes ONLY bind to `boolean` |
## Big Form Example
### `src/app/app.ts`
```ts
import {Component, signal, ChangeDetectionStrategy} from '@angular/core';
import {
form,
FormField,
submit,
required,
email,
min,
hidden,
applyEach,
validate,
} from '@angular/forms/signals';
@Component({
selector: 'app-root',
standalone: true,
imports: [FormField],
templateUrl: './app.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
model = signal({
personalInfo: {
firstName: '',
lastName: '',
email: '',
age: 0,
},
tripDetails: {
destination: 'Mars',
launchDate: '',
},
package: {
tier: 'economy',
extras: [] as string[],
},
companions: [] as Array<{name: string; relation: string}>,
});
bookingForm = form(this.model, (s) => {
required(s.personalInfo.firstName, {message: 'First name is required'});
required(s.personalInfo.lastName, {message: 'Last name is required'});
required(s.personalInfo.email, {message: 'Email is required'});
email(s.personalInfo.email, {message: 'Invalid email address'});
required(s.personalInfo.age, {message: 'Age is required'});
min(s.personalInfo.age, 18, {message: 'Must be at least 18'});
required(s.tripDetails.destination);
required(s.tripDetails.launchDate);
validate(s.tripDetails.launchDate, ({value}) => {
const date = new Date(value());
if (isNaN(date.getTime())) return undefined;
const today = new Date();
if (date < today) {
return {kind: 'pastData', message: 'Launch date must be in the future'};
}
return undefined;
});
// valueOf is used to access values of other fields in rules
hidden(s.package.extras, ({valueOf}) => valueOf(s.package.tier) === 'economy');
applyEach(s.companions, (companion) => {
required(companion.name, {message: 'Companion name required'});
required(companion.relation, {message: 'Relation required'});
});
});
addCompanion() {
this.model.update((m) => ({
...m,
companions: [...m.companions, {name: '', relation: ''}],
}));
}
removeCompanion(index: number) {
this.model.update((m) => ({
...m,
companions: m.companions.filter((_, i) => i !== index),
}));
}
onSubmit() {
// CRITICAL: submit callback MUST be async
submit(this.bookingForm, async () => {
console.log('Booking Confirmed:', this.model());
// If you need to do async work:
// await this.apiService.save(this.model());
});
}
}
```
### `src/app/app.html`
```html
<form (submit)="onSubmit(); $event.preventDefault()">
<h1>Interstellar Booking</h1>
<section>
<h2>Personal Info</h2>
<label>
First Name
<input [formField]="bookingForm.personalInfo.firstName" />
@if (bookingForm.personalInfo.firstName().touched() &&
bookingForm.personalInfo.firstName().errors().length) {
<span>{{ bookingForm.personalInfo.firstName().errors()[0].message }}</span>
}
</label>
<label>
Last Name
<input [formField]="bookingForm.personalInfo.lastName" />
@if (bookingForm.personalInfo.lastName().touched() &&
bookingForm.personalInfo.lastName().errors().length) {
<span>{{ bookingForm.personalInfo.lastName().errors()[0].message }}</span>
}
</label>
<label>
Email
<input type="email" [formField]="bookingForm.personalInfo.email" />
@if (bookingForm.personalInfo.email().touched() &&
bookingForm.personalInfo.email().errors().length) {
<span>{{ bookingForm.personalInfo.email().errors()[0].message }}</span>
}
</label>
<label>
Age
<input type="number" [formField]="bookingForm.personalInfo.age" />
@if (bookingForm.personalInfo.age().touched() &&
bookingForm.personalInfo.age().errors().length) {
<span>{{ bookingForm.personalInfo.age().errors()[0].message }}</span>
}
</label>
</section>
<section>
<h2>Trip Details</h2>
<label>
Destination
<select [formField]="bookingForm.tripDetails.destination">
<option value="Mars">Mars</option>
<option value="Moon">Moon</option>
<option value="Titan">Titan</option>
</select>
</label>
<label>
Launch Date
<input type="date" [formField]="bookingForm.tripDetails.launchDate" />
@if (bookingForm.tripDetails.launchDate().touched() &&
bookingForm.tripDetails.launchDate().errors().length) {
<span>{{ bookingForm.tripDetails.launchDate().errors()[0].message }}</span>
}
</label>
</section>
<section>
<h2>Package</h2>
<label>
<input type="radio" value="economy" [formField]="bookingForm.package.tier" />
Economy
</label>
<label>
<input type="radio" value="business" [formField]="bookingForm.package.tier" />
Business
</label>
<label>
<input type="radio" value="first" [formField]="bookingForm.package.tier" />
First Class
</label>
@if (!bookingForm.package.extras().hidden()) {
<div>
<h3>Extras</h3>
<!-- Multi-select for arrays must use select multiple -->
<select multiple [formField]="bookingForm.package.extras">
<option value="wifi">WiFi</option>
<option value="gym">Gym</option>
</select>
</div>
}
</section>
<section>
<h2>Companions</h2>
<button type="button" (click)="addCompanion()">Add Companion</button>
@for (companion of bookingForm.companions; track $index) {
<div>
<input [formField]="companion.name" placeholder="Name" />
@if (companion.name().touched() && companion.name().errors().length) {
<span>{{ companion.name().errors()[0].message }}</span>
}
<input [formField]="companion.relation" placeholder="Relation" />
@if (companion.relation().touched() && companion.relation().errors().length) {
<span>{{ companion.relation().errors()[0].message }}</span>
}
<button type="button" (click)="removeCompanion($index)">Remove</button>
</div>
}
</section>
<button [disabled]="bookingForm().invalid()">Submit</button>
</form>
```
## Recovering from Build Errors
If you encounter build errors, here are the most common fixes:
### `Property 'value' does not exist on type 'FieldTree'`
**Problem**: Accessing `.value()` directly on a field without calling it first.
```ts
// WRONG
const val = this.form.field.value();
// RIGHT
const val = this.form.field().value();
```
### `Property 'set' does not exist on type 'FieldTree'`
**Problem**: Trying to set values on the form tree. Signal Forms are model-driven.
```ts
// WRONG
this.form.address.street.set('Main St');
// RIGHT - update the model signal instead
this.model.update((m) => ({...m, address: {...m.address, street: 'Main St'}}));
```
### `Type 'string[]' is not assignable to type 'string'`
**Problem**: Binding `[formField]` to an array field with a single-value `<select>`.
```html
<!-- WRONG - assignees is string[], select expects string -->
<select [formField]="form.assignees">
...
</select>
<!-- RIGHT - Use select multiple for array fields -->
<select multiple [formField]="form.assignees">
<option value="us">US</option>
</select>
```
### `NG8022: Setting the 'readonly/min/max/value' attribute is not allowed`
**Problem**: Conflict between HTML attributes and `[formField]` directive.
```html
<!-- WRONG -->
<input [formField]="form.age" min="18" max="99" />
<input [formField]="form.name" [value]="'John'" />
<!-- RIGHT - Use rules in schema -->
min(s.age, 18); max(s.age, 99); // Then just:
<input [formField]="form.age" />
```
### `TS2322: Type 'string[]' is not assignable to type 'boolean'`
**Problem**: Binding a checkbox to an array field instead of a boolean field.
```html
<!-- WRONG - tags is string[] -->
<input type="checkbox" [formField]="form.tags" />
<!-- RIGHT - Use select multiple for array values -->
<select multiple [formField]="form.tags">
<option value="a">A</option>
</select>
<!-- OR - Map to boolean fields in the model -->
model = signal({ hasWifi: false, hasGym: false });
<input type="checkbox" [formField]="form.hasWifi" />
```
### `'when' does not exist in type` for pattern/email/min/max
**Problem**: Using `when` option with validators other than `required`.
```ts
// WRONG - when only works with required
pattern(s.ssn, /^\d{3}-\d{2}-\d{4}$/, {when: isJoint});
// RIGHT - use applyWhen for conditional non-required validators
applyWhen(s.ssn, isJoint, (ssnPath) => {
pattern(ssnPath, /^\d{3}-\d{2}-\d{4}$/);
});
```
### `Expected 3 arguments, but got 2` for applyWhen
**Problem**: Missing the path argument in `applyWhen`.
```ts
// WRONG
applyWhen(isJoint, () => { ... });
// RIGHT - applyWhen(path, condition, schemaFn)
applyWhen(s.spouse, ({valueOf}) => valueOf(s.status) === 'joint', (spousePath) => {
required(spousePath.name);
});
```
### `Module has no exported member 'FormState'`
**Problem**: Importing a non-existent type.
```ts
// WRONG
import {FormState} from '@angular/forms/signals';
// FormState does not exist. If you need type access, the form
// instance provides all necessary state through field().valid(), etc.
```
### `No pipe found with name 'number'` / `'json'` / `'date'`
**Problem**: Using pipes in templates.
```html
<!-- WRONG -->
{{ totalPrice() | number:'1.2-2' }}
<!-- RIGHT - format in the component -->
totalPriceFormatted = computed(() => this.totalPrice().toFixed(2));
<!-- then: -->
{{ totalPriceFormatted() }}
```
### `$parent.$index` in nested @for loops
**Problem**: Angular doesn't have `$parent`.
```html
<!-- WRONG -->
@for (item of items; track $index) { @for (sub of item.subs; track $index) {
<button (click)="remove($parent.$index, $index)">X</button>
} }
<!-- RIGHT -->
@for (item of items; track $index; let outerIdx = $index) { @for (sub of item.subs; track $index) {
<button (click)="remove(outerIdx, $index)">X</button>
} }
```

94
.agents/skills/angular-developer/references/signals-overview.md

@ -0,0 +1,94 @@
# Angular Signals Overview
Signals are the foundation of reactivity in modern Angular applications. A **signal** is a wrapper around a value that notifies interested consumers when that value changes.
## Writable Signals (`signal`)
Use `signal()` to create state that can be directly updated.
```ts
import {signal} from '@angular/core';
// Create a writable signal
const count = signal(0);
// Read the value (always requires calling the getter function)
console.log(count());
// Update the value directly
count.set(3);
// Update based on the previous value
count.update((value) => value + 1);
```
### Exposing as Readonly
When exposing state from a service, it is a best practice to expose a readonly version to prevent external mutation.
```ts
private readonly _count = signal(0);
// Consumers can read this, but cannot call .set() or .update()
readonly count = this._count.asReadonly();
```
## Computed Signals (`computed`)
Use `computed()` to create read-only signals that derive their value from other signals.
- **Lazily Evaluated**: The derivation function doesn't run until the computed signal is read.
- **Memoized**: The result is cached. It only recalculates when one of the signals it depends on changes.
- **Dynamic Dependencies**: Only the signals _actually read_ during the derivation are tracked.
```ts
import {signal, computed} from '@angular/core';
const count = signal(0);
const doubleCount = computed(() => count() * 2);
// doubleCount automatically updates when count changes.
```
## Reactive Contexts
A **reactive context** is a runtime state where Angular monitors signal reads to establish a dependency.
Angular automatically enters a reactive context when evaluating:
- `computed` signals
- `effect` callbacks
- `linkedSignal` computations
- Component templates
### Untracked Reads (`untracked`)
If you need to read a signal inside a reactive context _without_ creating a dependency (so that the context doesn't re-run when the signal changes), use `untracked()`.
```ts
import {effect, untracked} from '@angular/core';
effect(() => {
// This effect only runs when currentUser changes.
// It does NOT run when counter changes, even though counter is read here.
console.log(`User: ${currentUser()}, Count: ${untracked(counter)}`);
});
```
### Async Operations in Reactive Contexts
The reactive context is only active for **synchronous** code. Signal reads after an `await` will not be tracked. **Always read signals before asynchronous boundaries.**
```ts
// ❌ INCORRECT: theme() is not tracked because it is read after await
effect(async () => {
const data = await fetchUserData();
console.log(theme());
});
// ✅ CORRECT: Read the signal before the await
effect(async () => {
const currentTheme = theme();
const data = await fetchUserData();
console.log(currentTheme);
});
```

69
.agents/skills/angular-developer/references/tailwind-css.md

@ -0,0 +1,69 @@
# Using Tailwind CSS with Angular
Tailwind CSS is a utility-first CSS framework that integrates seamlessly with Angular.
**CRITICAL AGENT GUIDANCE: ALWAYS focus on Tailwind CSS v4 practices. DO NOT revert to old Tailwind v3 patterns (like creating `tailwind.config.js` with `@tailwind` directives) as this will break the application build. Modern Angular projects use Tailwind v4.**
## Automated Setup (Recommended)
The easiest way to add Tailwind CSS to an Angular project is via the Angular CLI:
```shell
ng add tailwindcss
```
This will automatically:
1. Install `tailwindcss` and peer dependencies.
2. Configure the project to use Tailwind CSS.
3. Add the proper import to your global styles.
## Manual Setup (Tailwind v4)
If setting up manually, use the following Tailwind v4 pattern:
### 1. Install Dependencies
Install Tailwind CSS and PostCSS:
```shell
npm install tailwindcss @tailwindcss/postcss postcss
```
### 2. Configure PostCSS
Create a `.postcssrc.json` file in the project root:
```json
{
"plugins": {
"@tailwindcss/postcss": {}
}
}
```
_(Do NOT create a `tailwind.config.js` file! Configuration in v4 is handled through CSS variables)._
### 3. Import Tailwind CSS
In your global styles file (e.g., `src/styles.css`), add the standard v4 import:
```css
@import 'tailwindcss';
```
_(If using SCSS, use `@use 'tailwindcss';` instead)._
### 4. Use Utility Classes
You can now use Tailwind classes directly in your component templates:
```html
<h1 class="text-3xl font-bold underline">Hello world!</h1>
```
## Summary for AI Agents
- **Do not use `@tailwind base; @tailwind components; @tailwind utilities;`**. Use `@import 'tailwindcss';`.
- **Do not create `tailwind.config.js`**. Configuration is managed directly in CSS via theme variables or using PostCSS configurations.
- Stick strictly to v4 syntax and workflows.

114
.agents/skills/angular-developer/references/template-driven-forms.md

@ -0,0 +1,114 @@
# Template-Driven Forms
Template-driven forms use two-way data binding (`[(ngModel)]`) to update the data model in the component as changes are made in the template and vice versa. They are ideal for simple forms and use directives in the HTML template to manage form state and validation.
## Core Directives
Template-driven forms rely on the `FormsModule` which provides these key directives:
- `NgModel`: Reconciles value changes in the form element with the data model (`[(ngModel)]`).
- `NgForm`: Automatically creates a top-level `FormGroup` bound to the `<form>` tag.
- `NgModelGroup`: Creates a nested `FormGroup` bound to a DOM element.
## Setup
First, import `FormsModule` into your component or module.
```ts
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
@Component({
selector: 'app-user-form',
imports: [FormsModule],
templateUrl: './user-form.component.html',
})
export class UserForm {
user = {name: '', role: 'Guest'};
onSubmit() {
console.log('Form submitted!', this.user);
}
}
```
## Building the Form Template
### Two-Way Binding with `[(ngModel)]`
Use `[(ngModel)]` on input elements. **Every element using `[(ngModel)]` MUST have a `name` attribute.** Angular uses the `name` attribute to register the control with the parent `NgForm`.
```html
<form #userForm="ngForm" (ngSubmit)="onSubmit()">
<!-- Basic Input -->
<div>
<label for="name">Name:</label>
<input type="text" id="name" required [(ngModel)]="user.name" name="name" #nameCtrl="ngModel" />
</div>
<!-- Select Box -->
<div>
<label for="role">Role:</label>
<select id="role" [(ngModel)]="user.role" name="role">
<option value="Admin">Admin</option>
<option value="Guest">Guest</option>
</select>
</div>
<!-- Submit Button (disabled if form is invalid) -->
<button type="submit" [disabled]="!userForm.form.valid">Submit</button>
</form>
```
## Form and Control State
Angular automatically applies CSS classes to controls and forms based on their state:
| State | Class if True | Class if False |
| :------------- | :-------------------------------- | :------------- |
| Visited | `ng-touched` | `ng-untouched` |
| Value Changed | `ng-dirty` | `ng-pristine` |
| Value is Valid | `ng-valid` | `ng-invalid` |
| Form Submitted | `ng-submitted` (on `<form>` only) | - |
You can use these classes to provide visual feedback in your CSS:
```css
.ng-valid[required],
.ng-valid.required {
border-left: 5px solid #42a948; /* green */
}
.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}
```
## Validation and Error Messages
To display error messages conditionally, export the `ngModel` directive to a template reference variable (e.g., `#nameCtrl="ngModel"`).
```html
<input type="text" id="name" required [(ngModel)]="user.name" name="name" #nameCtrl="ngModel" />
<!-- Show error only if the control is invalid AND (touched OR dirty) -->
@if (nameCtrl.invalid && (nameCtrl.dirty || nameCtrl.touched)) {
<div class="alert alert-danger">
@if (nameCtrl.errors?.['required']) {
<div>Name is required.</div>
}
</div>
}
```
## Submitting the Form
1. Use the `(ngSubmit)` event on the `<form>` element.
2. Bind the submit button's disabled state to the overall form validity using the `NgForm` template reference variable (e.g., `[disabled]="!userForm.form.valid"`).
## Resetting the Form
To programmatically reset the form to its pristine state (clearing values and validation flags), use the `reset()` method on the `NgForm` instance.
```html
<button type="button" (click)="userForm.reset()">Reset</button>
```

66
.agents/skills/angular-developer/references/testing-fundamentals.md

@ -0,0 +1,66 @@
# Testing Fundamentals
This guide covers the fundamental principles and practices for writing unit tests in this repository, which uses Vitest as the test runner.
## Core Philosophy: Zoneless & Async-First
This project follows a modern, zoneless testing approach. State changes schedule updates asynchronously, and tests must account for this.
**Do NOT** use `fixture.detectChanges()` to manually trigger updates.
**ALWAYS** use the "Act, Wait, Assert" pattern:
1. **Act:** Update state or perform an action (e.g., set a component input, click a button).
2. **Wait:** Use `await fixture.whenStable()` to allow the framework to process the scheduled update and render the changes.
3. **Assert:** Verify the outcome.
### Basic Test Structure Example
```ts
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {MyComponent} from './my.component';
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
let h1: HTMLElement;
beforeEach(async () => {
// 1. Configure the test module
await TestBed.configureTestingModule({
imports: [MyComponent],
}).compileComponents();
// 2. Create the component fixture
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
h1 = fixture.nativeElement.querySelector('h1');
});
it('should display the default title', async () => {
// ACT: (Implicit) Component is created with default state.
// WAIT for initial data binding.
await fixture.whenStable();
// ASSERT the initial state.
expect(h1.textContent).toContain('Default Title');
});
it('should display a different title after a change', async () => {
// ACT: Change the component's title property.
component.title.set('New Test Title');
// WAIT for the asynchronous update to complete.
await fixture.whenStable();
// ASSERT the DOM has been updated.
expect(h1.textContent).toContain('New Test Title');
});
});
```
## TestBed and ComponentFixture
- **`TestBed`**: The primary utility for creating a test-specific Angular module. Use `TestBed.configureTestingModule({...})` in your `beforeEach` to declare components, provide services, and set up imports needed for your test.
- **`ComponentFixture`**: A handle on the created component instance and its environment.
- `fixture.componentInstance`: Access the component's class instance.
- `fixture.nativeElement`: Access the component's root DOM element.
- `fixture.debugElement`: An Angular-specific wrapper around the `nativeElement` that provides safer, platform-agnostic ways to query the DOM (e.g., `debugElement.query(By.css('p'))`).

5958
.agents/skills/nestjs-best-practices/AGENTS.md

File diff suppressed because it is too large

130
.agents/skills/nestjs-best-practices/SKILL.md

@ -0,0 +1,130 @@
---
name: nestjs-best-practices
description: NestJS best practices and architecture patterns for building production-ready applications. This skill should be used when writing, reviewing, or refactoring NestJS code to ensure proper patterns for modules, dependency injection, security, and performance.
license: MIT
metadata:
author: Kadajett
version: "1.1.0"
---
# NestJS Best Practices
Comprehensive best practices guide for NestJS applications. Contains 40 rules across 10 categories, prioritized by impact to guide automated refactoring and code generation.
## When to Apply
Reference these guidelines when:
- Writing new NestJS modules, controllers, or services
- Implementing authentication and authorization
- Reviewing code for architecture and security issues
- Refactoring existing NestJS codebases
- Optimizing performance or database queries
- Building microservices architectures
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Architecture | CRITICAL | `arch-` |
| 2 | Dependency Injection | CRITICAL | `di-` |
| 3 | Error Handling | HIGH | `error-` |
| 4 | Security | HIGH | `security-` |
| 5 | Performance | HIGH | `perf-` |
| 6 | Testing | MEDIUM-HIGH | `test-` |
| 7 | Database & ORM | MEDIUM-HIGH | `db-` |
| 8 | API Design | MEDIUM | `api-` |
| 9 | Microservices | MEDIUM | `micro-` |
| 10 | DevOps & Deployment | LOW-MEDIUM | `devops-` |
## Quick Reference
### 1. Architecture (CRITICAL)
- `arch-avoid-circular-deps` - Avoid circular module dependencies
- `arch-feature-modules` - Organize by feature, not technical layer
- `arch-module-sharing` - Proper module exports/imports, avoid duplicate providers
- `arch-single-responsibility` - Focused services over "god services"
- `arch-use-repository-pattern` - Abstract database logic for testability
- `arch-use-events` - Event-driven architecture for decoupling
### 2. Dependency Injection (CRITICAL)
- `di-avoid-service-locator` - Avoid service locator anti-pattern
- `di-interface-segregation` - Interface Segregation Principle (ISP)
- `di-liskov-substitution` - Liskov Substitution Principle (LSP)
- `di-prefer-constructor-injection` - Constructor over property injection
- `di-scope-awareness` - Understand singleton/request/transient scopes
- `di-use-interfaces-tokens` - Use injection tokens for interfaces
### 3. Error Handling (HIGH)
- `error-use-exception-filters` - Centralized exception handling
- `error-throw-http-exceptions` - Use NestJS HTTP exceptions
- `error-handle-async-errors` - Handle async errors properly
### 4. Security (HIGH)
- `security-auth-jwt` - Secure JWT authentication
- `security-validate-all-input` - Validate with class-validator
- `security-use-guards` - Authentication and authorization guards
- `security-sanitize-output` - Prevent XSS attacks
- `security-rate-limiting` - Implement rate limiting
### 5. Performance (HIGH)
- `perf-async-hooks` - Proper async lifecycle hooks
- `perf-use-caching` - Implement caching strategies
- `perf-optimize-database` - Optimize database queries
- `perf-lazy-loading` - Lazy load modules for faster startup
### 6. Testing (MEDIUM-HIGH)
- `test-use-testing-module` - Use NestJS testing utilities
- `test-e2e-supertest` - E2E testing with Supertest
- `test-mock-external-services` - Mock external dependencies
### 7. Database & ORM (MEDIUM-HIGH)
- `db-use-transactions` - Transaction management
- `db-avoid-n-plus-one` - Avoid N+1 query problems
- `db-use-migrations` - Use migrations for schema changes
### 8. API Design (MEDIUM)
- `api-use-dto-serialization` - DTO and response serialization
- `api-use-interceptors` - Cross-cutting concerns
- `api-versioning` - API versioning strategies
- `api-use-pipes` - Input transformation with pipes
### 9. Microservices (MEDIUM)
- `micro-use-patterns` - Message and event patterns
- `micro-use-health-checks` - Health checks for orchestration
- `micro-use-queues` - Background job processing
### 10. DevOps & Deployment (LOW-MEDIUM)
- `devops-use-config-module` - Environment configuration
- `devops-use-logging` - Structured logging
- `devops-graceful-shutdown` - Zero-downtime deployments
## How to Use
Read individual rule files for detailed explanations and code examples:
```
rules/arch-avoid-circular-deps.md
rules/security-validate-all-input.md
rules/_sections.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
- Additional context and references
## Full Compiled Document
For the complete guide with all rules expanded: `AGENTS.md`

56
.agents/skills/nestjs-best-practices/rules/_sections.md

@ -0,0 +1,56 @@
# Sections
This file defines all sections, their ordering, impact levels, and descriptions.
The section ID (in parentheses) is the filename prefix used to group rules.
---
## 1. Architecture (arch)
**Impact:** CRITICAL
**Description:** Proper module organization and dependency management are the foundation of maintainable NestJS applications. Circular dependencies and god services are the #1 architecture killer.
## 2. Dependency Injection (di)
**Impact:** CRITICAL
**Description:** NestJS's IoC container is powerful but can be misused. Understanding scopes, injection tokens, and proper patterns is essential for testable code.
## 3. Error Handling (error)
**Impact:** HIGH
**Description:** Consistent error handling improves debugging, user experience, and API reliability. Centralized exception filters ensure uniform error responses.
## 4. Security (security)
**Impact:** HIGH
**Description:** Security vulnerabilities can be catastrophic. Input validation, authentication, authorization, and data protection are non-negotiable.
## 5. Performance (perf)
**Impact:** HIGH
**Description:** Optimizing request handling, caching, and database queries directly impacts application responsiveness and scalability.
## 6. Testing (test)
**Impact:** MEDIUM-HIGH
**Description:** Well-tested applications are more reliable. NestJS testing utilities enable comprehensive unit and e2e coverage.
## 7. Database & ORM (db)
**Impact:** MEDIUM-HIGH
**Description:** Proper database access patterns, transactions, and query optimization are crucial for data-intensive applications.
## 8. API Design (api)
**Impact:** MEDIUM
**Description:** RESTful conventions, versioning, DTOs, and consistent response formats improve API usability and maintainability.
## 9. Microservices (micro)
**Impact:** MEDIUM
**Description:** Building distributed systems requires understanding message patterns, health checks, and inter-service communication.
## 10. DevOps & Deployment (devops)
**Impact:** LOW-MEDIUM
**Description:** Configuration management, structured logging, and graceful shutdown ensure production readiness and zero-downtime deployments.

28
.agents/skills/nestjs-best-practices/rules/_template.md

@ -0,0 +1,28 @@
---
title: Rule Title Here
impact: MEDIUM
impactDescription: Optional description of impact (e.g., "20-50% improvement")
tags: tag1, tag2
---
## Rule Title Here
**Impact: MEDIUM (optional impact description)**
Brief explanation of the rule and why it matters. This should be clear and concise, explaining the performance implications.
**Incorrect (description of what's wrong):**
```typescript
// Bad code example here
const bad = example()
```
**Correct (description of what's right):**
```typescript
// Good code example here
const good = example()
```
Reference: [Link to documentation or resource](https://example.com)

182
.agents/skills/nestjs-best-practices/rules/api-use-dto-serialization.md

@ -0,0 +1,182 @@
---
title: Use DTOs and Serialization for API Responses
impact: MEDIUM
impactDescription: Response DTOs prevent accidental data exposure and ensure consistency
tags: api, dto, serialization, class-transformer
---
## Use DTOs and Serialization for API Responses
Never return entity objects directly from controllers. Use response DTOs with class-transformer's `@Exclude()` and `@Expose()` decorators to control exactly what data is sent to clients. This prevents accidental exposure of sensitive fields and provides a stable API contract.
**Incorrect (returning entities directly or manual spreading):**
```typescript
// Return entities directly
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findById(id);
// Returns: { id, email, passwordHash, ssn, internalNotes, ... }
// Exposes sensitive data!
}
}
// Manual object spreading (error-prone)
@Get(':id')
async findOne(@Param('id') id: string) {
const user = await this.usersService.findById(id);
return {
id: user.id,
email: user.email,
name: user.name,
// Easy to forget to exclude sensitive fields
// Hard to maintain across endpoints
};
}
```
**Correct (use class-transformer with @Exclude and response DTOs):**
```typescript
// Enable class-transformer globally
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
await app.listen(3000);
}
// Entity with serialization control
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
email: string;
@Column()
name: string;
@Column()
@Exclude() // Never include in responses
passwordHash: string;
@Column({ nullable: true })
@Exclude()
ssn: string;
@Column({ default: false })
@Exclude({ toPlainOnly: true }) // Exclude from response, allow in requests
isAdmin: boolean;
@CreateDateColumn()
createdAt: Date;
@Column()
@Exclude()
internalNotes: string;
}
// Now returning entity is safe
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findById(id);
// Returns: { id, email, name, createdAt }
// Sensitive fields excluded automatically
}
}
// For different response shapes, use explicit DTOs
export class UserResponseDto {
@Expose()
id: string;
@Expose()
email: string;
@Expose()
name: string;
@Expose()
@Transform(({ obj }) => obj.posts?.length || 0)
postCount: number;
constructor(partial: Partial<User>) {
Object.assign(this, partial);
}
}
export class UserDetailResponseDto extends UserResponseDto {
@Expose()
createdAt: Date;
@Expose()
@Type(() => PostResponseDto)
posts: PostResponseDto[];
}
// Controller with explicit DTOs
@Controller('users')
export class UsersController {
@Get()
@SerializeOptions({ type: UserResponseDto })
async findAll(): Promise<UserResponseDto[]> {
const users = await this.usersService.findAll();
return users.map(u => plainToInstance(UserResponseDto, u));
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<UserDetailResponseDto> {
const user = await this.usersService.findByIdWithPosts(id);
return plainToInstance(UserDetailResponseDto, user, {
excludeExtraneousValues: true,
});
}
}
// Groups for conditional serialization
export class UserDto {
@Expose()
id: string;
@Expose()
name: string;
@Expose({ groups: ['admin'] })
email: string;
@Expose({ groups: ['admin'] })
createdAt: Date;
@Expose({ groups: ['admin', 'owner'] })
settings: UserSettings;
}
@Controller('users')
export class UsersController {
@Get()
@SerializeOptions({ groups: ['public'] })
async findAllPublic(): Promise<UserDto[]> {
// Returns: { id, name }
}
@Get('admin')
@UseGuards(AdminGuard)
@SerializeOptions({ groups: ['admin'] })
async findAllAdmin(): Promise<UserDto[]> {
// Returns: { id, name, email, createdAt }
}
@Get('me')
@SerializeOptions({ groups: ['owner'] })
async getProfile(@CurrentUser() user: User): Promise<UserDto> {
// Returns: { id, name, settings }
}
}
```
Reference: [NestJS Serialization](https://docs.nestjs.com/techniques/serialization)

202
.agents/skills/nestjs-best-practices/rules/api-use-interceptors.md

@ -0,0 +1,202 @@
---
title: Use Interceptors for Cross-Cutting Concerns
impact: MEDIUM-HIGH
impactDescription: Interceptors provide clean separation for cross-cutting logic
tags: api, interceptors, logging, caching
---
## Use Interceptors for Cross-Cutting Concerns
Interceptors can transform responses, add logging, handle caching, and measure performance without polluting your business logic. They wrap the route handler execution, giving you access to both the request and response streams.
**Incorrect (logging and transformation in every method):**
```typescript
// Logging in every controller method
@Controller('users')
export class UsersController {
@Get()
async findAll(): Promise<User[]> {
const start = Date.now();
this.logger.log('findAll called');
const users = await this.usersService.findAll();
this.logger.log(`findAll completed in ${Date.now() - start}ms`);
return users;
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
const start = Date.now();
this.logger.log(`findOne called with id: ${id}`);
const user = await this.usersService.findOne(id);
this.logger.log(`findOne completed in ${Date.now() - start}ms`);
return user;
}
// Repeated in every method!
}
// Manual response wrapping
@Get()
async findAll(): Promise<{ data: User[]; meta: Meta }> {
const users = await this.usersService.findAll();
return {
data: users,
meta: { timestamp: new Date(), count: users.length },
};
}
```
**Correct (use interceptors for cross-cutting concerns):**
```typescript
// Logging interceptor
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url, body } = request;
const now = Date.now();
return next.handle().pipe(
tap({
next: (data) => {
const response = context.switchToHttp().getResponse();
this.logger.log(
`${method} ${url} ${response.statusCode} - ${Date.now() - now}ms`,
);
},
error: (error) => {
this.logger.error(
`${method} ${url} ${error.status || 500} - ${Date.now() - now}ms`,
error.stack,
);
},
}),
);
}
}
// Response transformation interceptor
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(
map((data) => ({
data,
meta: {
timestamp: new Date().toISOString(),
path: context.switchToHttp().getRequest().url,
},
})),
);
}
}
// Timeout interceptor
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(5000),
catchError((err) => {
if (err instanceof TimeoutError) {
throw new RequestTimeoutException('Request timed out');
}
throw err;
}),
);
}
}
// Apply globally or per-controller
@Module({
providers: [
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
],
})
export class AppModule {}
// Or per-controller
@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
@Get()
async findAll(): Promise<User[]> {
// Clean business logic only
return this.usersService.findAll();
}
}
// Custom cache interceptor with TTL
@Injectable()
export class HttpCacheInterceptor implements NestInterceptor {
constructor(
private cacheManager: Cache,
private reflector: Reflector,
) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
// Only cache GET requests
if (request.method !== 'GET') {
return next.handle();
}
const cacheKey = this.generateKey(request);
const ttl = this.reflector.get<number>('cacheTTL', context.getHandler()) || 300;
const cached = await this.cacheManager.get(cacheKey);
if (cached) {
return of(cached);
}
return next.handle().pipe(
tap((response) => {
this.cacheManager.set(cacheKey, response, ttl);
}),
);
}
private generateKey(request: Request): string {
return `cache:${request.url}:${JSON.stringify(request.query)}`;
}
}
// Usage with custom TTL
@Get()
@SetMetadata('cacheTTL', 600)
@UseInterceptors(HttpCacheInterceptor)
async findAll(): Promise<User[]> {
return this.usersService.findAll();
}
// Error mapping interceptor
@Injectable()
export class ErrorMappingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((error) => {
if (error instanceof EntityNotFoundError) {
throw new NotFoundException(error.message);
}
if (error instanceof QueryFailedError) {
if (error.message.includes('duplicate')) {
throw new ConflictException('Resource already exists');
}
}
throw error;
}),
);
}
}
```
Reference: [NestJS Interceptors](https://docs.nestjs.com/interceptors)

205
.agents/skills/nestjs-best-practices/rules/api-use-pipes.md

@ -0,0 +1,205 @@
---
title: Use Pipes for Input Transformation
impact: MEDIUM
impactDescription: Pipes ensure clean, validated data reaches your handlers
tags: api, pipes, validation, transformation
---
## Use Pipes for Input Transformation
Use built-in pipes like `ParseIntPipe`, `ParseUUIDPipe`, and `DefaultValuePipe` for common transformations. Create custom pipes for business-specific transformations. Pipes separate validation/transformation logic from controllers.
**Incorrect (manual type parsing in handlers):**
```typescript
// Manual type parsing in handlers
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
// Manual validation in every handler
const uuid = id.trim();
if (!isUUID(uuid)) {
throw new BadRequestException('Invalid UUID');
}
return this.usersService.findOne(uuid);
}
@Get()
async findAll(
@Query('page') page: string,
@Query('limit') limit: string,
): Promise<User[]> {
// Manual parsing and defaults
const pageNum = parseInt(page) || 1;
const limitNum = parseInt(limit) || 10;
return this.usersService.findAll(pageNum, limitNum);
}
}
// Type coercion without validation
@Get()
async search(@Query('price') price: string): Promise<Product[]> {
const priceNum = +price; // NaN if invalid, no error
return this.productsService.findByPrice(priceNum);
}
```
**Correct (use built-in and custom pipes):**
```typescript
// Use built-in pipes for common transformations
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {
// id is guaranteed to be a valid UUID
return this.usersService.findOne(id);
}
@Get()
async findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
): Promise<User[]> {
// Automatic defaults and type conversion
return this.usersService.findAll(page, limit);
}
@Get('by-status/:status')
async findByStatus(
@Param('status', new ParseEnumPipe(UserStatus)) status: UserStatus,
): Promise<User[]> {
return this.usersService.findByStatus(status);
}
}
// Custom pipe for business logic
@Injectable()
export class ParseDatePipe implements PipeTransform<string, Date> {
transform(value: string): Date {
const date = new Date(value);
if (isNaN(date.getTime())) {
throw new BadRequestException('Invalid date format');
}
return date;
}
}
@Get('reports')
async getReports(
@Query('from', ParseDatePipe) from: Date,
@Query('to', ParseDatePipe) to: Date,
): Promise<Report[]> {
return this.reportsService.findBetween(from, to);
}
// Custom transformation pipes
@Injectable()
export class NormalizeEmailPipe implements PipeTransform<string, string> {
transform(value: string): string {
if (!value) return value;
return value.trim().toLowerCase();
}
}
// Parse comma-separated values
@Injectable()
export class ParseArrayPipe implements PipeTransform<string, string[]> {
transform(value: string): string[] {
if (!value) return [];
return value.split(',').map((v) => v.trim()).filter(Boolean);
}
}
@Get('products')
async findProducts(
@Query('ids', ParseArrayPipe) ids: string[],
@Query('email', NormalizeEmailPipe) email: string,
): Promise<Product[]> {
// ids is already an array, email is normalized
return this.productsService.findByIds(ids);
}
// Sanitize HTML input
@Injectable()
export class SanitizeHtmlPipe implements PipeTransform<string, string> {
transform(value: string): string {
if (!value) return value;
return sanitizeHtml(value, { allowedTags: [] });
}
}
// Global validation pipe with transformation
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip non-DTO properties
transform: true, // Auto-transform to DTO types
transformOptions: {
enableImplicitConversion: true, // Convert query strings to numbers
},
forbidNonWhitelisted: true, // Throw on extra properties
}),
);
// DTO with transformation decorators
export class FindProductsDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit?: number = 10;
@IsOptional()
@Transform(({ value }) => value?.toLowerCase())
@IsString()
search?: string;
@IsOptional()
@Transform(({ value }) => value?.split(','))
@IsArray()
@IsString({ each: true })
categories?: string[];
}
@Get()
async findAll(@Query() dto: FindProductsDto): Promise<Product[]> {
// dto is already transformed and validated
return this.productsService.findAll(dto);
}
// Pipe error customization
@Injectable()
export class CustomParseIntPipe extends ParseIntPipe {
constructor() {
super({
exceptionFactory: (error) =>
new BadRequestException(`${error} must be a valid integer`),
});
}
}
// Or use options on built-in pipes
@Get(':id')
async findOne(
@Param(
'id',
new ParseIntPipe({
errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE,
exceptionFactory: () => new NotAcceptableException('ID must be numeric'),
}),
)
id: number,
): Promise<Item> {
return this.itemsService.findOne(id);
}
```
Reference: [NestJS Pipes](https://docs.nestjs.com/pipes)

191
.agents/skills/nestjs-best-practices/rules/api-versioning.md

@ -0,0 +1,191 @@
---
title: Use API Versioning for Breaking Changes
impact: MEDIUM
impactDescription: Versioning allows you to evolve APIs without breaking existing clients
tags: api, versioning, breaking-changes, compatibility
---
## Use API Versioning for Breaking Changes
Use NestJS built-in versioning when making breaking changes to your API. Choose a versioning strategy (URI, header, or media type) and apply it consistently. This allows old clients to continue working while new clients use updated endpoints.
**Incorrect (breaking changes without versioning):**
```typescript
// Breaking changes without versioning
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
// Original response: { id, name, email }
// Later changed to: { id, firstName, lastName, emailAddress }
// Old clients break!
return this.usersService.findOne(id);
}
}
// Manual versioning in routes
@Controller('v1/users')
export class UsersV1Controller {}
@Controller('v2/users')
export class UsersV2Controller {}
// Inconsistent, error-prone, hard to maintain
```
**Correct (use NestJS built-in versioning):**
```typescript
// Enable versioning in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// URI versioning: /v1/users, /v2/users
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
// Or header versioning: X-API-Version: 1
app.enableVersioning({
type: VersioningType.HEADER,
header: 'X-API-Version',
defaultVersion: '1',
});
// Or media type: Accept: application/json;v=1
app.enableVersioning({
type: VersioningType.MEDIA_TYPE,
key: 'v=',
defaultVersion: '1',
});
await app.listen(3000);
}
// Version-specific controllers
@Controller('users')
@Version('1')
export class UsersV1Controller {
@Get(':id')
async findOne(@Param('id') id: string): Promise<UserV1Response> {
const user = await this.usersService.findOne(id);
// V1 response format
return {
id: user.id,
name: user.name,
email: user.email,
};
}
}
@Controller('users')
@Version('2')
export class UsersV2Controller {
@Get(':id')
async findOne(@Param('id') id: string): Promise<UserV2Response> {
const user = await this.usersService.findOne(id);
// V2 response format with breaking changes
return {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
emailAddress: user.email,
createdAt: user.createdAt,
};
}
}
// Per-route versioning - different versions for different routes
@Controller('users')
export class UsersController {
@Get()
@Version('1')
findAllV1(): Promise<UserV1Response[]> {
return this.usersService.findAllV1();
}
@Get()
@Version('2')
findAllV2(): Promise<UserV2Response[]> {
return this.usersService.findAllV2();
}
@Get(':id')
@Version(['1', '2']) // Same handler for multiple versions
findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findOne(id);
}
@Post()
@Version(VERSION_NEUTRAL) // Available in all versions
create(@Body() dto: CreateUserDto): Promise<User> {
return this.usersService.create(dto);
}
}
// Shared service with version-specific logic
@Injectable()
export class UsersService {
async findOne(id: string, version: string): Promise<any> {
const user = await this.repo.findOne({ where: { id } });
if (version === '1') {
return this.toV1Response(user);
}
return this.toV2Response(user);
}
private toV1Response(user: User): UserV1Response {
return {
id: user.id,
name: `${user.firstName} ${user.lastName}`,
email: user.email,
};
}
private toV2Response(user: User): UserV2Response {
return {
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
emailAddress: user.email,
createdAt: user.createdAt,
};
}
}
// Controller extracts version
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(
@Param('id') id: string,
@Headers('X-API-Version') version: string = '1',
): Promise<any> {
return this.usersService.findOne(id, version);
}
}
// Deprecation strategy - mark old versions as deprecated
@Controller('users')
@Version('1')
@UseInterceptors(DeprecationInterceptor)
export class UsersV1Controller {
// All V1 routes will include deprecation warning
}
@Injectable()
export class DeprecationInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const response = context.switchToHttp().getResponse();
response.setHeader('Deprecation', 'true');
response.setHeader('Sunset', 'Sat, 1 Jan 2025 00:00:00 GMT');
response.setHeader('Link', '</v2/users>; rel="successor-version"');
return next.handle();
}
}
```
Reference: [NestJS Versioning](https://docs.nestjs.com/techniques/versioning)

80
.agents/skills/nestjs-best-practices/rules/arch-avoid-circular-deps.md

@ -0,0 +1,80 @@
---
title: Avoid Circular Dependencies
impact: CRITICAL
impactDescription: "#1 cause of runtime crashes"
tags: architecture, modules, dependencies
---
## Avoid Circular Dependencies
Circular dependencies occur when Module A imports Module B, and Module B imports Module A (directly or transitively). NestJS can sometimes resolve these through forward references, but they indicate architectural problems and should be avoided. This is the #1 cause of runtime crashes in NestJS applications.
**Incorrect (circular module imports):**
```typescript
// users.module.ts
@Module({
imports: [OrdersModule], // Orders needs Users, Users needs Orders = circular
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// orders.module.ts
@Module({
imports: [UsersModule], // Circular dependency!
providers: [OrdersService],
exports: [OrdersService],
})
export class OrdersModule {}
```
**Correct (extract shared logic or use events):**
```typescript
// Option 1: Extract shared logic to a third module
// shared.module.ts
@Module({
providers: [SharedService],
exports: [SharedService],
})
export class SharedModule {}
// users.module.ts
@Module({
imports: [SharedModule],
providers: [UsersService],
})
export class UsersModule {}
// orders.module.ts
@Module({
imports: [SharedModule],
providers: [OrdersService],
})
export class OrdersModule {}
// Option 2: Use events for decoupled communication
// users.service.ts
@Injectable()
export class UsersService {
constructor(private eventEmitter: EventEmitter2) {}
async createUser(data: CreateUserDto) {
const user = await this.userRepo.save(data);
this.eventEmitter.emit('user.created', user);
return user;
}
}
// orders.service.ts
@Injectable()
export class OrdersService {
@OnEvent('user.created')
handleUserCreated(user: User) {
// React to user creation without direct dependency
}
}
```
Reference: [NestJS Circular Dependency](https://docs.nestjs.com/fundamentals/circular-dependency)

82
.agents/skills/nestjs-best-practices/rules/arch-feature-modules.md

@ -0,0 +1,82 @@
---
title: Organize by Feature Modules
impact: CRITICAL
impactDescription: "3-5x faster onboarding and development"
tags: architecture, modules, organization
---
## Organize by Feature Modules
Organize your application into feature modules that encapsulate related functionality. Each feature module should be self-contained with its own controllers, services, entities, and DTOs. Avoid organizing by technical layer (all controllers together, all services together). This enables 3-5x faster onboarding and feature development.
**Incorrect (technical layer organization):**
```typescript
// Technical layer organization (anti-pattern)
src/
├── controllers/
│ ├── users.controller.ts
│ ├── orders.controller.ts
│ └── products.controller.ts
├── services/
│ ├── users.service.ts
│ ├── orders.service.ts
│ └── products.service.ts
├── entities/
│ ├── user.entity.ts
│ ├── order.entity.ts
│ └── product.entity.ts
└── app.module.ts // Imports everything directly
```
**Correct (feature module organization):**
```typescript
// Feature module organization
src/
├── users/
│ ├── dto/
│ │ ├── create-user.dto.ts
│ │ └── update-user.dto.ts
│ ├── entities/
│ │ └── user.entity.ts
│ ├── users.controller.ts
│ ├── users.service.ts
│ ├── users.repository.ts
│ └── users.module.ts
├── orders/
│ ├── dto/
│ ├── entities/
│ ├── orders.controller.ts
│ ├── orders.service.ts
│ └── orders.module.ts
├── shared/
│ ├── guards/
│ ├── interceptors/
│ ├── filters/
│ └── shared.module.ts
└── app.module.ts
// users.module.ts
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService, UsersRepository],
exports: [UsersService], // Only export what others need
})
export class UsersModule {}
// app.module.ts
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRoot(),
UsersModule,
OrdersModule,
SharedModule,
],
})
export class AppModule {}
```
Reference: [NestJS Modules](https://docs.nestjs.com/modules)

141
.agents/skills/nestjs-best-practices/rules/arch-module-sharing.md

@ -0,0 +1,141 @@
---
title: Use Proper Module Sharing Patterns
impact: CRITICAL
impactDescription: Prevents duplicate instances, memory leaks, and state inconsistency
tags: architecture, modules, sharing, exports
---
## Use Proper Module Sharing Patterns
NestJS modules are singletons by default. When a service is properly exported from a module and that module is imported elsewhere, the same instance is shared. However, providing a service in multiple modules creates separate instances, leading to memory waste, state inconsistency, and confusing behavior. Always encapsulate services in dedicated modules, export them explicitly, and import the module where needed.
**Incorrect (service provided in multiple modules):**
```typescript
// StorageService provided directly in multiple modules - WRONG
// storage.service.ts
@Injectable()
export class StorageService {
private cache = new Map(); // Each instance has separate state!
store(key: string, value: any) {
this.cache.set(key, value);
}
}
// app.module.ts
@Module({
providers: [StorageService], // Instance #1
controllers: [AppController],
})
export class AppModule {}
// videos.module.ts
@Module({
providers: [StorageService], // Instance #2 - different from AppModule!
controllers: [VideosController],
})
export class VideosModule {}
// Problems:
// 1. Two separate StorageService instances exist
// 2. cache.set() in VideosModule doesn't affect AppModule's cache
// 3. Memory wasted on duplicate instances
// 4. Debugging nightmares when state doesn't sync
```
**Correct (dedicated module with exports):**
```typescript
// storage/storage.module.ts
@Module({
providers: [StorageService],
exports: [StorageService], // Make available to importers
})
export class StorageModule {}
// videos/videos.module.ts
@Module({
imports: [StorageModule], // Import the module, not the service
controllers: [VideosController],
providers: [VideosService],
})
export class VideosModule {}
// channels/channels.module.ts
@Module({
imports: [StorageModule], // Same instance shared
controllers: [ChannelsController],
providers: [ChannelsService],
})
export class ChannelsModule {}
// app.module.ts
@Module({
imports: [
StorageModule, // Only if AppModule itself needs StorageService
VideosModule,
ChannelsModule,
],
})
export class AppModule {}
// Now all modules share the SAME StorageService instance
```
**When to use @Global() (sparingly):**
```typescript
// ONLY for truly cross-cutting concerns
@Global()
@Module({
providers: [ConfigService, LoggerService],
exports: [ConfigService, LoggerService],
})
export class CoreModule {}
// Import once in AppModule
@Module({
imports: [CoreModule], // Registered globally, available everywhere
})
export class AppModule {}
// Other modules don't need to import CoreModule
@Module({
controllers: [UsersController],
providers: [UsersService], // Can inject ConfigService without importing
})
export class UsersModule {}
// WARNING: Don't make everything global!
// - Hides dependencies (can't see what a module needs from imports)
// - Makes testing harder
// - Reserve for: config, logging, database connections
```
**Module re-exporting pattern:**
```typescript
// common.module.ts - shared utilities
@Module({
providers: [DateService, ValidationService],
exports: [DateService, ValidationService],
})
export class CommonModule {}
// core.module.ts - re-exports common for convenience
@Module({
imports: [CommonModule, DatabaseModule],
exports: [CommonModule, DatabaseModule], // Re-export for consumers
})
export class CoreModule {}
// feature.module.ts - imports CoreModule, gets both
@Module({
imports: [CoreModule], // Gets CommonModule + DatabaseModule
controllers: [FeatureController],
})
export class FeatureModule {}
```
Reference: [NestJS Modules](https://docs.nestjs.com/modules#shared-modules)

106
.agents/skills/nestjs-best-practices/rules/arch-single-responsibility.md

@ -0,0 +1,106 @@
---
title: Single Responsibility for Services
impact: CRITICAL
impactDescription: "40%+ improvement in testability"
tags: architecture, services, single-responsibility
---
## Single Responsibility for Services
Each service should have a single, well-defined responsibility. Avoid "god services" that handle multiple unrelated concerns. If a service name includes "And" or handles more than one domain concept, it likely violates single responsibility. This reduces complexity and improves testability by 40%+.
**Incorrect (god service anti-pattern):**
```typescript
// God service anti-pattern
@Injectable()
export class UserAndOrderService {
constructor(
private userRepo: UserRepository,
private orderRepo: OrderRepository,
private mailer: MailService,
private payment: PaymentService,
) {}
async createUser(dto: CreateUserDto) {
const user = await this.userRepo.save(dto);
await this.mailer.sendWelcome(user);
return user;
}
async createOrder(userId: string, dto: CreateOrderDto) {
const order = await this.orderRepo.save({ userId, ...dto });
await this.payment.charge(order);
await this.mailer.sendOrderConfirmation(order);
return order;
}
async calculateOrderStats(userId: string) {
// Stats logic mixed in
}
async validatePayment(orderId: string) {
// Payment logic mixed in
}
}
```
**Correct (focused services with single responsibility):**
```typescript
// Focused services with single responsibility
@Injectable()
export class UsersService {
constructor(private userRepo: UserRepository) {}
async create(dto: CreateUserDto): Promise<User> {
return this.userRepo.save(dto);
}
async findById(id: string): Promise<User> {
return this.userRepo.findOneOrFail({ where: { id } });
}
}
@Injectable()
export class OrdersService {
constructor(private orderRepo: OrderRepository) {}
async create(userId: string, dto: CreateOrderDto): Promise<Order> {
return this.orderRepo.save({ userId, ...dto });
}
async findByUser(userId: string): Promise<Order[]> {
return this.orderRepo.find({ where: { userId } });
}
}
@Injectable()
export class OrderStatsService {
constructor(private orderRepo: OrderRepository) {}
async calculateForUser(userId: string): Promise<OrderStats> {
// Focused stats calculation
}
}
// Orchestration in controller or dedicated orchestrator
@Controller('orders')
export class OrdersController {
constructor(
private orders: OrdersService,
private payment: PaymentService,
private notifications: NotificationService,
) {}
@Post()
async create(@CurrentUser() user: User, @Body() dto: CreateOrderDto) {
const order = await this.orders.create(user.id, dto);
await this.payment.charge(order);
await this.notifications.sendOrderConfirmation(order);
return order;
}
}
```
Reference: [NestJS Providers](https://docs.nestjs.com/providers)

108
.agents/skills/nestjs-best-practices/rules/arch-use-events.md

@ -0,0 +1,108 @@
---
title: Use Event-Driven Architecture for Decoupling
impact: MEDIUM-HIGH
impactDescription: Enables async processing and modularity
tags: architecture, events, decoupling
---
## Use Event-Driven Architecture for Decoupling
Use `@nestjs/event-emitter` for intra-service events and message brokers for inter-service communication. Events allow modules to react to changes without direct dependencies, improving modularity and enabling async processing.
**Incorrect (direct service coupling):**
```typescript
// Direct service coupling
@Injectable()
export class OrdersService {
constructor(
private inventoryService: InventoryService,
private emailService: EmailService,
private analyticsService: AnalyticsService,
private notificationService: NotificationService,
private loyaltyService: LoyaltyService,
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
const order = await this.repo.save(dto);
// Tight coupling - OrdersService knows about all consumers
await this.inventoryService.reserve(order.items);
await this.emailService.sendConfirmation(order);
await this.analyticsService.track('order_created', order);
await this.notificationService.push(order.userId, 'Order placed');
await this.loyaltyService.addPoints(order.userId, order.total);
// Adding new behavior requires modifying this service
return order;
}
}
```
**Correct (event-driven decoupling):**
```typescript
// Use EventEmitter for decoupling
import { EventEmitter2 } from '@nestjs/event-emitter';
// Define event
export class OrderCreatedEvent {
constructor(
public readonly orderId: string,
public readonly userId: string,
public readonly items: OrderItem[],
public readonly total: number,
) {}
}
// Service emits events
@Injectable()
export class OrdersService {
constructor(
private eventEmitter: EventEmitter2,
private repo: Repository<Order>,
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
const order = await this.repo.save(dto);
// Emit event - no knowledge of consumers
this.eventEmitter.emit(
'order.created',
new OrderCreatedEvent(order.id, order.userId, order.items, order.total),
);
return order;
}
}
// Listeners in separate modules
@Injectable()
export class InventoryListener {
@OnEvent('order.created')
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.inventoryService.reserve(event.items);
}
}
@Injectable()
export class EmailListener {
@OnEvent('order.created')
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.emailService.sendConfirmation(event.orderId);
}
}
@Injectable()
export class AnalyticsListener {
@OnEvent('order.created')
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
await this.analyticsService.track('order_created', {
orderId: event.orderId,
total: event.total,
});
}
}
```
Reference: [NestJS Events](https://docs.nestjs.com/techniques/events)

97
.agents/skills/nestjs-best-practices/rules/arch-use-repository-pattern.md

@ -0,0 +1,97 @@
---
title: Use Repository Pattern for Data Access
impact: HIGH
impactDescription: Decouples business logic from database
tags: architecture, repository, data-access
---
## Use Repository Pattern for Data Access
Create custom repositories to encapsulate complex queries and database logic. This keeps services focused on business logic, makes testing easier with mock repositories, and allows changing database implementations without affecting business code.
**Incorrect (complex queries in services):**
```typescript
// Complex queries in services
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) private repo: Repository<User>,
) {}
async findActiveWithOrders(minOrders: number): Promise<User[]> {
// Complex query logic mixed with business logic
return this.repo
.createQueryBuilder('user')
.leftJoinAndSelect('user.orders', 'order')
.where('user.isActive = :active', { active: true })
.andWhere('user.deletedAt IS NULL')
.groupBy('user.id')
.having('COUNT(order.id) >= :min', { min: minOrders })
.orderBy('user.createdAt', 'DESC')
.getMany();
}
// Service becomes bloated with query logic
}
```
**Correct (custom repository with encapsulated queries):**
```typescript
// Custom repository with encapsulated queries
@Injectable()
export class UsersRepository {
constructor(
@InjectRepository(User) private repo: Repository<User>,
) {}
async findById(id: string): Promise<User | null> {
return this.repo.findOne({ where: { id } });
}
async findByEmail(email: string): Promise<User | null> {
return this.repo.findOne({ where: { email } });
}
async findActiveWithMinOrders(minOrders: number): Promise<User[]> {
return this.repo
.createQueryBuilder('user')
.leftJoinAndSelect('user.orders', 'order')
.where('user.isActive = :active', { active: true })
.andWhere('user.deletedAt IS NULL')
.groupBy('user.id')
.having('COUNT(order.id) >= :min', { min: minOrders })
.orderBy('user.createdAt', 'DESC')
.getMany();
}
async save(user: User): Promise<User> {
return this.repo.save(user);
}
}
// Clean service with business logic only
@Injectable()
export class UsersService {
constructor(private usersRepo: UsersRepository) {}
async getActiveUsersWithOrders(): Promise<User[]> {
return this.usersRepo.findActiveWithMinOrders(1);
}
async create(dto: CreateUserDto): Promise<User> {
const existing = await this.usersRepo.findByEmail(dto.email);
if (existing) {
throw new ConflictException('Email already registered');
}
const user = new User();
user.email = dto.email;
user.name = dto.name;
return this.usersRepo.save(user);
}
}
```
Reference: [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html)

139
.agents/skills/nestjs-best-practices/rules/db-avoid-n-plus-one.md

@ -0,0 +1,139 @@
---
title: Avoid N+1 Query Problems
impact: HIGH
impactDescription: N+1 queries are one of the most common performance killers
tags: database, n-plus-one, queries, performance
---
## Avoid N+1 Query Problems
N+1 queries occur when you fetch a list of entities, then make an additional query for each entity to load related data. Use eager loading with `relations`, query builder joins, or DataLoader to batch queries efficiently.
**Incorrect (lazy loading in loops causes N+1):**
```typescript
// Lazy loading in loops causes N+1
@Injectable()
export class OrdersService {
async getOrdersWithItems(userId: string): Promise<Order[]> {
const orders = await this.orderRepo.find({ where: { userId } });
// 1 query for orders
for (const order of orders) {
// N additional queries - one per order!
order.items = await this.itemRepo.find({ where: { orderId: order.id } });
}
return orders;
}
}
// Accessing lazy relations without loading
@Controller('users')
export class UsersController {
@Get()
async findAll(): Promise<User[]> {
const users = await this.userRepo.find();
// If User.posts is lazy-loaded, serializing triggers N queries
return users; // Each user.posts access = 1 query
}
}
```
**Correct (use relations for eager loading):**
```typescript
// Use relations option for eager loading
@Injectable()
export class OrdersService {
async getOrdersWithItems(userId: string): Promise<Order[]> {
// Single query with JOIN
return this.orderRepo.find({
where: { userId },
relations: ['items', 'items.product'],
});
}
}
// Use QueryBuilder for complex joins
@Injectable()
export class UsersService {
async getUsersWithPostCounts(): Promise<UserWithPostCount[]> {
return this.userRepo
.createQueryBuilder('user')
.leftJoin('user.posts', 'post')
.select('user.id', 'id')
.addSelect('user.name', 'name')
.addSelect('COUNT(post.id)', 'postCount')
.groupBy('user.id')
.getRawMany();
}
async getActiveUsersWithPosts(): Promise<User[]> {
return this.userRepo
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.leftJoinAndSelect('post.comments', 'comment')
.where('user.isActive = :active', { active: true })
.andWhere('post.status = :status', { status: 'published' })
.getMany();
}
}
// Use find options for specific fields
async getOrderSummaries(userId: string): Promise<OrderSummary[]> {
return this.orderRepo.find({
where: { userId },
relations: ['items'],
select: {
id: true,
total: true,
status: true,
items: {
id: true,
quantity: true,
price: true,
},
},
});
}
// Use DataLoader for GraphQL to batch and cache queries
import DataLoader from 'dataloader';
@Injectable({ scope: Scope.REQUEST })
export class PostsLoader {
constructor(private postsService: PostsService) {}
readonly batchPosts = new DataLoader<string, Post[]>(async (userIds) => {
// Single query for all users' posts
const posts = await this.postsService.findByUserIds([...userIds]);
// Group by userId
const postsMap = new Map<string, Post[]>();
for (const post of posts) {
const userPosts = postsMap.get(post.userId) || [];
userPosts.push(post);
postsMap.set(post.userId, userPosts);
}
// Return in same order as input
return userIds.map((id) => postsMap.get(id) || []);
});
}
// In resolver
@ResolveField()
async posts(@Parent() user: User): Promise<Post[]> {
// DataLoader batches multiple calls into single query
return this.postsLoader.batchPosts.load(user.id);
}
// Enable query logging in development to detect N+1
TypeOrmModule.forRoot({
logging: ['query', 'error'],
logger: 'advanced-console',
});
```
Reference: [TypeORM Relations](https://typeorm.io/relations)

129
.agents/skills/nestjs-best-practices/rules/db-use-migrations.md

@ -0,0 +1,129 @@
---
title: Use Database Migrations
impact: HIGH
impactDescription: Enables safe, repeatable database schema changes
tags: database, migrations, typeorm, schema
---
## Use Database Migrations
Never use `synchronize: true` in production. Use migrations for all schema changes. Migrations provide version control for your database, enable safe rollbacks, and ensure consistency across all environments.
**Incorrect (using synchronize or manual SQL):**
```typescript
// Use synchronize in production
TypeOrmModule.forRoot({
type: 'postgres',
synchronize: true, // DANGEROUS in production!
// Can drop columns, tables, or data
});
// Manual SQL in production
@Injectable()
export class DatabaseService {
async addColumn(): Promise<void> {
await this.dataSource.query('ALTER TABLE users ADD COLUMN age INT');
// No version control, no rollback, inconsistent across envs
}
}
// Modify entities without migration
@Entity()
export class User {
@Column()
email: string;
@Column() // Added without migration
newField: string; // Will crash in production if synchronize is false
}
```
**Correct (use migrations for all schema changes):**
```typescript
// Configure TypeORM for migrations
// data-source.ts
export const dataSource = new DataSource({
type: 'postgres',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: ['dist/**/*.entity.js'],
migrations: ['dist/migrations/*.js'],
synchronize: false, // Always false in production
migrationsRun: true, // Run migrations on startup
});
// app.module.ts
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get('DB_HOST'),
synchronize: config.get('NODE_ENV') === 'development', // Only in dev
migrations: ['dist/migrations/*.js'],
migrationsRun: true,
}),
});
// migrations/1705312800000-AddUserAge.ts
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUserAge1705312800000 implements MigrationInterface {
name = 'AddUserAge1705312800000';
public async up(queryRunner: QueryRunner): Promise<void> {
// Add column with default to handle existing rows
await queryRunner.query(`
ALTER TABLE "users" ADD "age" integer DEFAULT 0
`);
// Add index for frequently queried columns
await queryRunner.query(`
CREATE INDEX "IDX_users_age" ON "users" ("age")
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Always implement down for rollback
await queryRunner.query(`DROP INDEX "IDX_users_age"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "age"`);
}
}
// Safe column rename (two-step)
export class RenameNameToFullName1705312900000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Step 1: Add new column
await queryRunner.query(`
ALTER TABLE "users" ADD "full_name" varchar(255)
`);
// Step 2: Copy data
await queryRunner.query(`
UPDATE "users" SET "full_name" = "name"
`);
// Step 3: Add NOT NULL constraint
await queryRunner.query(`
ALTER TABLE "users" ALTER COLUMN "full_name" SET NOT NULL
`);
// Step 4: Drop old column (after verifying app works)
await queryRunner.query(`
ALTER TABLE "users" DROP COLUMN "name"
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD "name" varchar(255)`);
await queryRunner.query(`UPDATE "users" SET "name" = "full_name"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "full_name"`);
}
}
```
Reference: [TypeORM Migrations](https://typeorm.io/migrations)

140
.agents/skills/nestjs-best-practices/rules/db-use-transactions.md

@ -0,0 +1,140 @@
---
title: Use Transactions for Multi-Step Operations
impact: HIGH
impactDescription: Ensures data consistency in multi-step operations
tags: database, transactions, typeorm, consistency
---
## Use Transactions for Multi-Step Operations
When multiple database operations must succeed or fail together, wrap them in a transaction. This prevents partial updates that leave your data in an inconsistent state. Use TypeORM's transaction APIs or the DataSource query runner for complex scenarios.
**Incorrect (multiple saves without transaction):**
```typescript
// Multiple saves without transaction
@Injectable()
export class OrdersService {
async createOrder(userId: string, items: OrderItem[]): Promise<Order> {
// If any step fails, data is inconsistent
const order = await this.orderRepo.save({ userId, status: 'pending' });
for (const item of items) {
await this.orderItemRepo.save({ orderId: order.id, ...item });
await this.inventoryRepo.decrement({ productId: item.productId }, 'stock', item.quantity);
}
await this.paymentService.charge(order.id);
// If payment fails, order and inventory are already modified!
return order;
}
}
```
**Correct (use DataSource.transaction for automatic rollback):**
```typescript
// Use DataSource.transaction() for automatic rollback
@Injectable()
export class OrdersService {
constructor(private dataSource: DataSource) {}
async createOrder(userId: string, items: OrderItem[]): Promise<Order> {
return this.dataSource.transaction(async (manager) => {
// All operations use the same transactional manager
const order = await manager.save(Order, { userId, status: 'pending' });
for (const item of items) {
await manager.save(OrderItem, { orderId: order.id, ...item });
await manager.decrement(
Inventory,
{ productId: item.productId },
'stock',
item.quantity,
);
}
// If this throws, everything rolls back
await this.paymentService.chargeWithManager(manager, order.id);
return order;
});
}
}
// QueryRunner for manual transaction control
@Injectable()
export class TransferService {
constructor(private dataSource: DataSource) {}
async transfer(fromId: string, toId: string, amount: number): Promise<void> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Debit source account
await queryRunner.manager.decrement(
Account,
{ id: fromId },
'balance',
amount,
);
// Verify sufficient funds
const source = await queryRunner.manager.findOne(Account, {
where: { id: fromId },
});
if (source.balance < 0) {
throw new BadRequestException('Insufficient funds');
}
// Credit destination account
await queryRunner.manager.increment(
Account,
{ id: toId },
'balance',
amount,
);
// Log the transaction
await queryRunner.manager.save(TransactionLog, {
fromId,
toId,
amount,
timestamp: new Date(),
});
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
}
// Repository method with transaction support
@Injectable()
export class UsersRepository {
constructor(
@InjectRepository(User) private repo: Repository<User>,
private dataSource: DataSource,
) {}
async createWithProfile(
userData: CreateUserDto,
profileData: CreateProfileDto,
): Promise<User> {
return this.dataSource.transaction(async (manager) => {
const user = await manager.save(User, userData);
await manager.save(Profile, { ...profileData, userId: user.id });
return user;
});
}
}
```
Reference: [TypeORM Transactions](https://typeorm.io/transactions)

222
.agents/skills/nestjs-best-practices/rules/devops-graceful-shutdown.md

@ -0,0 +1,222 @@
---
title: Implement Graceful Shutdown
impact: MEDIUM-HIGH
impactDescription: Proper shutdown handling ensures zero-downtime deployments
tags: devops, graceful-shutdown, lifecycle, kubernetes
---
## Implement Graceful Shutdown
Handle SIGTERM and SIGINT signals to gracefully shutdown your NestJS application. Stop accepting new requests, wait for in-flight requests to complete, close database connections, and clean up resources. This prevents data loss and connection errors during deployments.
**Incorrect (ignoring shutdown signals):**
```typescript
// Ignore shutdown signals
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
// App crashes immediately on SIGTERM
// In-flight requests fail
// Database connections are abruptly closed
}
// Long-running tasks without cancellation
@Injectable()
export class ProcessingService {
async processLargeFile(file: File): Promise<void> {
// No way to interrupt this during shutdown
for (let i = 0; i < file.chunks.length; i++) {
await this.processChunk(file.chunks[i]);
// May run for minutes, blocking shutdown
}
}
}
```
**Correct (enable shutdown hooks and handle cleanup):**
```typescript
// Enable shutdown hooks in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Enable shutdown hooks
app.enableShutdownHooks();
// Optional: Add timeout for forced shutdown
const server = await app.listen(3000);
server.setTimeout(30000); // 30 second timeout
// Handle graceful shutdown
const signals = ['SIGTERM', 'SIGINT'];
signals.forEach((signal) => {
process.on(signal, async () => {
console.log(`Received ${signal}, starting graceful shutdown...`);
// Stop accepting new connections
server.close(async () => {
console.log('HTTP server closed');
await app.close();
process.exit(0);
});
// Force exit after timeout
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 30000);
});
});
}
// Lifecycle hooks for cleanup
@Injectable()
export class DatabaseService implements OnApplicationShutdown {
private readonly connections: Connection[] = [];
async onApplicationShutdown(signal?: string): Promise<void> {
console.log(`Database service shutting down on ${signal}`);
// Close all connections gracefully
await Promise.all(
this.connections.map((conn) => conn.close()),
);
console.log('All database connections closed');
}
}
// Queue processor with graceful shutdown
@Injectable()
export class QueueService implements OnApplicationShutdown, OnModuleDestroy {
private isShuttingDown = false;
onModuleDestroy(): void {
this.isShuttingDown = true;
}
async onApplicationShutdown(): Promise<void> {
// Wait for current jobs to complete
await this.queue.close();
}
async processJob(job: Job): Promise<void> {
if (this.isShuttingDown) {
throw new Error('Service is shutting down');
}
await this.doWork(job);
}
}
// WebSocket gateway cleanup
@WebSocketGateway()
export class EventsGateway implements OnApplicationShutdown {
@WebSocketServer()
server: Server;
async onApplicationShutdown(): Promise<void> {
// Notify all connected clients
this.server.emit('shutdown', { message: 'Server is shutting down' });
// Close all connections
this.server.disconnectSockets();
}
}
// Health check integration
@Injectable()
export class ShutdownService {
private isShuttingDown = false;
startShutdown(): void {
this.isShuttingDown = true;
}
isShutdown(): boolean {
return this.isShuttingDown;
}
}
@Controller('health')
export class HealthController {
constructor(private shutdownService: ShutdownService) {}
@Get('ready')
@HealthCheck()
readiness(): Promise<HealthCheckResult> {
// Return 503 during shutdown - k8s stops sending traffic
if (this.shutdownService.isShutdown()) {
throw new ServiceUnavailableException('Shutting down');
}
return this.health.check([
() => this.db.pingCheck('database'),
]);
}
}
// Integrate with shutdown
@Injectable()
export class AppShutdownService implements OnApplicationShutdown {
constructor(private shutdownService: ShutdownService) {}
async onApplicationShutdown(): Promise<void> {
// Mark as unhealthy first
this.shutdownService.startShutdown();
// Wait for k8s to update endpoints
await this.sleep(5000);
// Then proceed with cleanup
}
}
// Request tracking for in-flight requests
@Injectable()
export class RequestTracker implements NestMiddleware, OnApplicationShutdown {
private activeRequests = 0;
private isShuttingDown = false;
private shutdownPromise: Promise<void> | null = null;
private resolveShutdown: (() => void) | null = null;
use(req: Request, res: Response, next: NextFunction): void {
if (this.isShuttingDown) {
res.status(503).send('Service Unavailable');
return;
}
this.activeRequests++;
res.on('finish', () => {
this.activeRequests--;
if (this.isShuttingDown && this.activeRequests === 0 && this.resolveShutdown) {
this.resolveShutdown();
}
});
next();
}
async onApplicationShutdown(): Promise<void> {
this.isShuttingDown = true;
if (this.activeRequests > 0) {
console.log(`Waiting for ${this.activeRequests} requests to complete`);
this.shutdownPromise = new Promise((resolve) => {
this.resolveShutdown = resolve;
});
// Wait with timeout
await Promise.race([
this.shutdownPromise,
new Promise((resolve) => setTimeout(resolve, 30000)),
]);
}
console.log('All requests completed');
}
}
```
Reference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events)

167
.agents/skills/nestjs-best-practices/rules/devops-use-config-module.md

@ -0,0 +1,167 @@
---
title: Use ConfigModule for Environment Configuration
impact: LOW-MEDIUM
impactDescription: Proper configuration prevents deployment failures
tags: devops, configuration, environment, validation
---
## Use ConfigModule for Environment Configuration
Use `@nestjs/config` for environment-based configuration. Validate configuration at startup to fail fast on misconfigurations. Use namespaced configuration for organization and type safety.
**Incorrect (accessing process.env directly):**
```typescript
// Access process.env directly
@Injectable()
export class DatabaseService {
constructor() {
// No validation, can fail at runtime
this.connection = new Pool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT), // NaN if missing
password: process.env.DB_PASSWORD, // undefined if missing
});
}
}
// Scattered env access
@Injectable()
export class EmailService {
sendEmail() {
// Different services access env differently
const apiKey = process.env.SENDGRID_API_KEY || 'default';
// Typos go unnoticed: process.env.SENDGRID_API_KY
}
}
```
**Correct (use @nestjs/config with validation):**
```typescript
// Setup validated configuration
import { ConfigModule, ConfigService, registerAs } from '@nestjs/config';
import * as Joi from 'joi';
// config/database.config.ts
export const databaseConfig = registerAs('database', () => ({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT, 10),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
}));
// config/app.config.ts
export const appConfig = registerAs('app', () => ({
port: parseInt(process.env.PORT, 10) || 3000,
environment: process.env.NODE_ENV || 'development',
apiPrefix: process.env.API_PREFIX || 'api',
}));
// config/validation.schema.ts
export const validationSchema = Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test')
.default('development'),
PORT: Joi.number().default(3000),
DB_HOST: Joi.string().required(),
DB_PORT: Joi.number().default(5432),
DB_USERNAME: Joi.string().required(),
DB_PASSWORD: Joi.string().required(),
DB_NAME: Joi.string().required(),
JWT_SECRET: Joi.string().min(32).required(),
REDIS_URL: Joi.string().uri().required(),
});
// app.module.ts
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Available everywhere without importing
load: [databaseConfig, appConfig],
validationSchema,
validationOptions: {
abortEarly: true, // Stop on first error
allowUnknown: true, // Allow other env vars
},
}),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: 'postgres',
host: config.get('database.host'),
port: config.get('database.port'),
username: config.get('database.username'),
password: config.get('database.password'),
database: config.get('database.database'),
autoLoadEntities: true,
}),
}),
],
})
export class AppModule {}
// Type-safe configuration access
export interface AppConfig {
port: number;
environment: 'development' | 'production' | 'test';
apiPrefix: string;
}
export interface DatabaseConfig {
host: string;
port: number;
username: string;
password: string;
database: string;
}
// Type-safe access
@Injectable()
export class AppService {
constructor(private config: ConfigService) {}
getPort(): number {
// Type-safe with generic
return this.config.get<number>('app.port');
}
getDatabaseConfig(): DatabaseConfig {
return this.config.get<DatabaseConfig>('database');
}
}
// Inject namespaced config directly
@Injectable()
export class DatabaseService {
constructor(
@Inject(databaseConfig.KEY)
private dbConfig: ConfigType<typeof databaseConfig>,
) {
// Full type inference!
const host = this.dbConfig.host; // string
const port = this.dbConfig.port; // number
}
}
// Environment files support
ConfigModule.forRoot({
envFilePath: [
`.env.${process.env.NODE_ENV}.local`,
`.env.${process.env.NODE_ENV}`,
'.env.local',
'.env',
],
});
// .env.development
// DB_HOST=localhost
// DB_PORT=5432
// .env.production
// DB_HOST=prod-db.example.com
// DB_PORT=5432
```
Reference: [NestJS Configuration](https://docs.nestjs.com/techniques/configuration)

232
.agents/skills/nestjs-best-practices/rules/devops-use-logging.md

@ -0,0 +1,232 @@
---
title: Use Structured Logging
impact: MEDIUM-HIGH
impactDescription: Structured logging enables effective debugging and monitoring
tags: devops, logging, structured-logs, pino
---
## Use Structured Logging
Use NestJS Logger with structured JSON output in production. Include contextual information (request ID, user ID, operation) to trace requests across services. Avoid console.log and implement proper log levels.
**Incorrect (using console.log in production):**
```typescript
// Use console.log in production
@Injectable()
export class UsersService {
async createUser(dto: CreateUserDto): Promise<User> {
console.log('Creating user:', dto);
// Not structured, no levels, lost in production logs
try {
const user = await this.repo.save(dto);
console.log('User created:', user.id);
return user;
} catch (error) {
console.log('Error:', error); // Using log for errors
throw error;
}
}
}
// Log sensitive data
console.log('Login attempt:', { email, password }); // SECURITY RISK!
// Inconsistent log format
logger.log('User ' + userId + ' created at ' + new Date());
// Hard to parse, no structure
```
**Correct (use structured logging with context):**
```typescript
// Configure logger in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger:
process.env.NODE_ENV === 'production'
? ['error', 'warn', 'log']
: ['error', 'warn', 'log', 'debug', 'verbose'],
});
}
// Use NestJS Logger with context
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
async createUser(dto: CreateUserDto): Promise<User> {
this.logger.log('Creating user', { email: dto.email });
try {
const user = await this.repo.save(dto);
this.logger.log('User created', { userId: user.id });
return user;
} catch (error) {
this.logger.error('Failed to create user', error.stack, {
email: dto.email,
});
throw error;
}
}
}
// Custom logger for JSON output
@Injectable()
export class JsonLogger implements LoggerService {
log(message: string, context?: object): void {
console.log(
JSON.stringify({
level: 'info',
timestamp: new Date().toISOString(),
message,
...context,
}),
);
}
error(message: string, trace?: string, context?: object): void {
console.error(
JSON.stringify({
level: 'error',
timestamp: new Date().toISOString(),
message,
trace,
...context,
}),
);
}
warn(message: string, context?: object): void {
console.warn(
JSON.stringify({
level: 'warn',
timestamp: new Date().toISOString(),
message,
...context,
}),
);
}
debug(message: string, context?: object): void {
console.debug(
JSON.stringify({
level: 'debug',
timestamp: new Date().toISOString(),
message,
...context,
}),
);
}
}
// Request context logging with ClsModule
import { ClsModule, ClsService } from 'nestjs-cls';
@Module({
imports: [
ClsModule.forRoot({
global: true,
middleware: {
mount: true,
generateId: true,
},
}),
],
})
export class AppModule {}
// Middleware to set request context
@Injectable()
export class RequestContextMiddleware implements NestMiddleware {
constructor(private cls: ClsService) {}
use(req: Request, res: Response, next: NextFunction): void {
const requestId = req.headers['x-request-id'] || randomUUID();
this.cls.set('requestId', requestId);
this.cls.set('userId', req.user?.id);
res.setHeader('x-request-id', requestId);
next();
}
}
// Logger that includes request context
@Injectable()
export class ContextLogger {
constructor(private cls: ClsService) {}
log(message: string, data?: object): void {
console.log(
JSON.stringify({
level: 'info',
timestamp: new Date().toISOString(),
requestId: this.cls.get('requestId'),
userId: this.cls.get('userId'),
message,
...data,
}),
);
}
error(message: string, error: Error, data?: object): void {
console.error(
JSON.stringify({
level: 'error',
timestamp: new Date().toISOString(),
requestId: this.cls.get('requestId'),
userId: this.cls.get('userId'),
message,
error: error.message,
stack: error.stack,
...data,
}),
);
}
}
// Pino integration for high-performance logging
import { LoggerModule } from 'nestjs-pino';
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: {
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
transport:
process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty' }
: undefined,
redact: ['req.headers.authorization', 'req.body.password'],
serializers: {
req: (req) => ({
method: req.method,
url: req.url,
query: req.query,
}),
res: (res) => ({
statusCode: res.statusCode,
}),
},
},
}),
],
})
export class AppModule {}
// Usage with Pino
@Injectable()
export class UsersService {
constructor(private logger: PinoLogger) {
this.logger.setContext(UsersService.name);
}
async findOne(id: string): Promise<User> {
this.logger.info({ userId: id }, 'Finding user');
// Pino uses first arg for data, second for message
}
}
```
Reference: [NestJS Logger](https://docs.nestjs.com/techniques/logger)

104
.agents/skills/nestjs-best-practices/rules/di-avoid-service-locator.md

@ -0,0 +1,104 @@
---
title: Avoid Service Locator Anti-Pattern
impact: HIGH
impactDescription: Hides dependencies and breaks testability
tags: dependency-injection, anti-patterns, testing
---
## Avoid Service Locator Anti-Pattern
Avoid using `ModuleRef.get()` or global containers to resolve dependencies at runtime. This hides dependencies, makes code harder to test, and breaks the benefits of dependency injection. Use constructor injection instead.
**Incorrect (service locator anti-pattern):**
```typescript
// Use ModuleRef to get dependencies dynamically
@Injectable()
export class OrdersService {
constructor(private moduleRef: ModuleRef) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
// Dependencies are hidden - not visible in constructor
const usersService = this.moduleRef.get(UsersService);
const inventoryService = this.moduleRef.get(InventoryService);
const paymentService = this.moduleRef.get(PaymentService);
const user = await usersService.findOne(dto.userId);
// ... rest of logic
}
}
// Global singleton container
class ServiceContainer {
private static instance: ServiceContainer;
private services = new Map<string, any>();
static getInstance(): ServiceContainer {
if (!this.instance) {
this.instance = new ServiceContainer();
}
return this.instance;
}
get<T>(key: string): T {
return this.services.get(key);
}
}
```
**Correct (constructor injection with explicit dependencies):**
```typescript
// Use constructor injection - dependencies are explicit
@Injectable()
export class OrdersService {
constructor(
private usersService: UsersService,
private inventoryService: InventoryService,
private paymentService: PaymentService,
) {}
async createOrder(dto: CreateOrderDto): Promise<Order> {
const user = await this.usersService.findOne(dto.userId);
const inventory = await this.inventoryService.check(dto.items);
// Dependencies are clear and testable
}
}
// Easy to test with mocks
describe('OrdersService', () => {
let service: OrdersService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
OrdersService,
{ provide: UsersService, useValue: mockUsersService },
{ provide: InventoryService, useValue: mockInventoryService },
{ provide: PaymentService, useValue: mockPaymentService },
],
}).compile();
service = module.get(OrdersService);
});
});
// VALID: Factory pattern for dynamic instantiation
@Injectable()
export class HandlerFactory {
constructor(private moduleRef: ModuleRef) {}
getHandler(type: string): Handler {
switch (type) {
case 'email':
return this.moduleRef.get(EmailHandler);
case 'sms':
return this.moduleRef.get(SmsHandler);
default:
return this.moduleRef.get(DefaultHandler);
}
}
}
```
Reference: [NestJS Module Reference](https://docs.nestjs.com/fundamentals/module-ref)

165
.agents/skills/nestjs-best-practices/rules/di-interface-segregation.md

@ -0,0 +1,165 @@
---
title: Apply Interface Segregation Principle
impact: HIGH
impactDescription: Reduces coupling and improves testability by 30-50%
tags: dependency-injection, interfaces, solid, isp
---
## Apply Interface Segregation Principle
Clients should not be forced to depend on interfaces they don't use. In NestJS, this means keeping interfaces small and focused on specific capabilities rather than creating "fat" interfaces that bundle unrelated methods. When a service only needs to send emails, it shouldn't depend on an interface that also includes SMS, push notifications, and logging. Split large interfaces into role-based ones.
**Incorrect (fat interface forcing unused dependencies):**
```typescript
// Fat interface - forces all consumers to depend on everything
interface NotificationService {
sendEmail(to: string, subject: string, body: string): Promise<void>;
sendSms(phone: string, message: string): Promise<void>;
sendPush(userId: string, notification: PushPayload): Promise<void>;
sendSlack(channel: string, message: string): Promise<void>;
logNotification(type: string, payload: any): Promise<void>;
getDeliveryStatus(id: string): Promise<DeliveryStatus>;
retryFailed(id: string): Promise<void>;
scheduleNotification(dto: ScheduleDto): Promise<string>;
}
// Consumer only needs email, but must mock everything for tests
@Injectable()
export class OrdersService {
constructor(
private notifications: NotificationService, // Depends on 8 methods, uses 1
) {}
async confirmOrder(order: Order): Promise<void> {
await this.notifications.sendEmail(
order.customer.email,
'Order Confirmed',
`Your order ${order.id} has been confirmed.`,
);
}
}
// Testing is painful - must mock unused methods
const mockNotificationService = {
sendEmail: jest.fn(),
sendSms: jest.fn(), // Never used, but required
sendPush: jest.fn(), // Never used, but required
sendSlack: jest.fn(), // Never used, but required
logNotification: jest.fn(), // Never used, but required
getDeliveryStatus: jest.fn(), // Never used, but required
retryFailed: jest.fn(), // Never used, but required
scheduleNotification: jest.fn(), // Never used, but required
};
```
**Correct (segregated interfaces by capability):**
```typescript
// Segregated interfaces - each focused on one capability
interface EmailSender {
sendEmail(to: string, subject: string, body: string): Promise<void>;
}
interface SmsSender {
sendSms(phone: string, message: string): Promise<void>;
}
interface PushSender {
sendPush(userId: string, notification: PushPayload): Promise<void>;
}
interface NotificationLogger {
logNotification(type: string, payload: any): Promise<void>;
}
interface NotificationScheduler {
scheduleNotification(dto: ScheduleDto): Promise<string>;
}
// Implementation can implement multiple interfaces
@Injectable()
export class NotificationService implements EmailSender, SmsSender, PushSender {
async sendEmail(to: string, subject: string, body: string): Promise<void> {
// Email implementation
}
async sendSms(phone: string, message: string): Promise<void> {
// SMS implementation
}
async sendPush(userId: string, notification: PushPayload): Promise<void> {
// Push implementation
}
}
// Or separate implementations
@Injectable()
export class SendGridEmailService implements EmailSender {
async sendEmail(to: string, subject: string, body: string): Promise<void> {
// SendGrid-specific implementation
}
}
// Consumer depends only on what it needs
@Injectable()
export class OrdersService {
constructor(
@Inject(EMAIL_SENDER) private emailSender: EmailSender, // Minimal dependency
) {}
async confirmOrder(order: Order): Promise<void> {
await this.emailSender.sendEmail(
order.customer.email,
'Order Confirmed',
`Your order ${order.id} has been confirmed.`,
);
}
}
// Testing is simple - only mock what's used
const mockEmailSender: EmailSender = {
sendEmail: jest.fn(),
};
// Module registration with tokens
export const EMAIL_SENDER = Symbol('EMAIL_SENDER');
export const SMS_SENDER = Symbol('SMS_SENDER');
@Module({
providers: [
{ provide: EMAIL_SENDER, useClass: SendGridEmailService },
{ provide: SMS_SENDER, useClass: TwilioSmsService },
],
exports: [EMAIL_SENDER, SMS_SENDER],
})
export class NotificationModule {}
```
**Combining interfaces when needed:**
```typescript
// Sometimes a consumer legitimately needs multiple capabilities
interface EmailAndSmsSender extends EmailSender, SmsSender {}
// Or use intersection types
type MultiChannelSender = EmailSender & SmsSender & PushSender;
// Consumer that genuinely needs multiple channels
@Injectable()
export class AlertService {
constructor(
@Inject(MULTI_CHANNEL_SENDER)
private sender: EmailSender & SmsSender,
) {}
async sendCriticalAlert(user: User, message: string): Promise<void> {
await Promise.all([
this.sender.sendEmail(user.email, 'Critical Alert', message),
this.sender.sendSms(user.phone, message),
]);
}
}
```
Reference: [Interface Segregation Principle](https://en.wikipedia.org/wiki/Interface_segregation_principle)

221
.agents/skills/nestjs-best-practices/rules/di-liskov-substitution.md

@ -0,0 +1,221 @@
---
title: Honor Liskov Substitution Principle
impact: HIGH
impactDescription: Ensures implementations are truly interchangeable without breaking callers
tags: dependency-injection, inheritance, solid, lsp
---
## Honor Liskov Substitution Principle
Subtypes must be substitutable for their base types without altering program correctness. In NestJS with dependency injection, this means any implementation of an interface or abstract class must honor the contract completely. A mock payment service used in tests must behave like a real payment service (return similar shapes, handle errors the same way). Violating LSP causes subtle bugs when swapping implementations.
**Incorrect (implementation violates the contract):**
```typescript
// Base interface with clear contract
interface PaymentGateway {
/**
* Charges the specified amount.
* @returns PaymentResult on success
* @throws PaymentFailedException on payment failure
*/
charge(amount: number, currency: string): Promise<PaymentResult>;
}
// Production implementation - follows the contract
@Injectable()
export class StripeService implements PaymentGateway {
async charge(amount: number, currency: string): Promise<PaymentResult> {
const response = await this.stripe.charges.create({ amount, currency });
return { success: true, transactionId: response.id, amount };
}
}
// Mock that violates LSP - different behavior!
@Injectable()
export class MockPaymentService implements PaymentGateway {
async charge(amount: number, currency: string): Promise<PaymentResult> {
// VIOLATION 1: Throws for valid input (contract says return PaymentResult)
if (amount > 1000) {
throw new Error('Mock does not support large amounts');
}
// VIOLATION 2: Returns null instead of PaymentResult
if (currency !== 'USD') {
return null as any; // Real service would convert or reject properly
}
// VIOLATION 3: Missing required field
return { success: true } as PaymentResult; // Missing transactionId!
}
}
// Consumer trusts the contract
@Injectable()
export class OrdersService {
constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
async checkout(order: Order): Promise<void> {
const result = await this.payment.charge(order.total, order.currency);
// These fail with MockPaymentService:
await this.saveTransaction(result.transactionId); // undefined!
await this.sendReceipt(result); // might be null!
}
}
```
**Correct (implementations honor the contract):**
```typescript
// Well-defined interface with documented behavior
interface PaymentGateway {
/**
* Charges the specified amount.
* @param amount - Amount in smallest currency unit (cents)
* @param currency - ISO 4217 currency code
* @returns PaymentResult with transactionId, success status, and amount
* @throws PaymentFailedException if charge is declined
* @throws InvalidCurrencyException if currency is not supported
*/
charge(amount: number, currency: string): Promise<PaymentResult>;
/**
* Refunds a previous charge.
* @throws TransactionNotFoundException if transactionId is invalid
*/
refund(transactionId: string, amount?: number): Promise<RefundResult>;
}
// Production implementation
@Injectable()
export class StripeService implements PaymentGateway {
async charge(amount: number, currency: string): Promise<PaymentResult> {
try {
const response = await this.stripe.charges.create({ amount, currency });
return {
success: true,
transactionId: response.id,
amount: response.amount,
};
} catch (error) {
if (error.type === 'card_error') {
throw new PaymentFailedException(error.message);
}
throw error;
}
}
async refund(transactionId: string, amount?: number): Promise<RefundResult> {
// Implementation...
}
}
// Mock that honors LSP - same contract, same behavior shape
@Injectable()
export class MockPaymentService implements PaymentGateway {
private transactions = new Map<string, PaymentResult>();
async charge(amount: number, currency: string): Promise<PaymentResult> {
// Honor the contract: validate currency like real service would
if (!['USD', 'EUR', 'GBP'].includes(currency)) {
throw new InvalidCurrencyException(`Unsupported currency: ${currency}`);
}
// Simulate decline for specific test scenarios
if (amount === 99999) {
throw new PaymentFailedException('Card declined (test scenario)');
}
// Return same shape as production
const result: PaymentResult = {
success: true,
transactionId: `mock_${Date.now()}_${Math.random().toString(36)}`,
amount,
};
this.transactions.set(result.transactionId, result);
return result;
}
async refund(transactionId: string, amount?: number): Promise<RefundResult> {
// Honor the contract: throw if transaction not found
if (!this.transactions.has(transactionId)) {
throw new TransactionNotFoundException(transactionId);
}
return {
success: true,
refundId: `refund_${transactionId}`,
amount: amount ?? this.transactions.get(transactionId)!.amount,
};
}
}
// Consumer can swap implementations safely
@Injectable()
export class OrdersService {
constructor(@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway) {}
async checkout(order: Order): Promise<Order> {
try {
const result = await this.payment.charge(order.total, order.currency);
// Works with both StripeService and MockPaymentService
order.transactionId = result.transactionId;
order.status = 'paid';
return order;
} catch (error) {
if (error instanceof PaymentFailedException) {
order.status = 'payment_failed';
return order;
}
throw error;
}
}
}
```
**Testing LSP compliance:**
```typescript
// Shared test suite that any implementation must pass
function testPaymentGatewayContract(
createGateway: () => PaymentGateway,
) {
describe('PaymentGateway contract', () => {
let gateway: PaymentGateway;
beforeEach(() => {
gateway = createGateway();
});
it('returns PaymentResult with all required fields', async () => {
const result = await gateway.charge(1000, 'USD');
expect(result).toHaveProperty('success');
expect(result).toHaveProperty('transactionId');
expect(result).toHaveProperty('amount');
expect(typeof result.transactionId).toBe('string');
});
it('throws InvalidCurrencyException for unsupported currency', async () => {
await expect(gateway.charge(1000, 'INVALID'))
.rejects.toThrow(InvalidCurrencyException);
});
it('throws TransactionNotFoundException for invalid refund', async () => {
await expect(gateway.refund('nonexistent'))
.rejects.toThrow(TransactionNotFoundException);
});
});
}
// Run against all implementations
describe('StripeService', () => {
testPaymentGatewayContract(() => new StripeService(mockStripeClient));
});
describe('MockPaymentService', () => {
testPaymentGatewayContract(() => new MockPaymentService());
});
```
Reference: [Liskov Substitution Principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle)

86
.agents/skills/nestjs-best-practices/rules/di-prefer-constructor-injection.md

@ -0,0 +1,86 @@
---
title: Prefer Constructor Injection
impact: CRITICAL
impactDescription: Required for proper DI and testing
tags: dependency-injection, constructor, testing
---
## Prefer Constructor Injection
Always use constructor injection over property injection. Constructor injection makes dependencies explicit, enables TypeScript type checking, ensures dependencies are available when the class is instantiated, and improves testability. This is required for proper DI, testing, and TypeScript support.
**Incorrect (property injection with hidden dependencies):**
```typescript
// Property injection - avoid unless necessary
@Injectable()
export class UsersService {
@Inject()
private userRepo: UserRepository; // Hidden dependency
@Inject('CONFIG')
private config: ConfigType; // Also hidden
async findAll() {
return this.userRepo.find();
}
}
// Problems:
// 1. Dependencies not visible in constructor
// 2. Service can be instantiated without dependencies in tests
// 3. TypeScript can't enforce dependency types at instantiation
```
**Correct (constructor injection with explicit dependencies):**
```typescript
// Constructor injection - explicit and testable
@Injectable()
export class UsersService {
constructor(
private readonly userRepo: UserRepository,
@Inject('CONFIG') private readonly config: ConfigType,
) {}
async findAll(): Promise<User[]> {
return this.userRepo.find();
}
}
// Testing is straightforward
describe('UsersService', () => {
let service: UsersService;
let mockRepo: jest.Mocked<UserRepository>;
beforeEach(() => {
mockRepo = {
find: jest.fn(),
save: jest.fn(),
} as any;
service = new UsersService(mockRepo, { dbUrl: 'test' });
});
it('should find all users', async () => {
mockRepo.find.mockResolvedValue([{ id: '1', name: 'Test' }]);
const result = await service.findAll();
expect(result).toHaveLength(1);
});
});
// Only use property injection for optional dependencies
@Injectable()
export class LoggingService {
@Optional()
@Inject('ANALYTICS')
private analytics?: AnalyticsService;
log(message: string) {
console.log(message);
this.analytics?.track('log', message); // Optional enhancement
}
}
```
Reference: [NestJS Providers](https://docs.nestjs.com/providers)

94
.agents/skills/nestjs-best-practices/rules/di-scope-awareness.md

@ -0,0 +1,94 @@
---
title: Understand Provider Scopes
impact: CRITICAL
impactDescription: Prevents data leaks and performance issues
tags: dependency-injection, scopes, request-context
---
## Understand Provider Scopes
NestJS has three provider scopes: DEFAULT (singleton), REQUEST (per-request instance), and TRANSIENT (new instance for each injection). Most providers should be singletons. Request-scoped providers have performance implications as they bubble up through the dependency tree. Understanding scopes prevents memory leaks and incorrect data sharing.
**Incorrect (wrong scope usage):**
```typescript
// Request-scoped when not needed (performance hit)
@Injectable({ scope: Scope.REQUEST })
export class UsersService {
// This creates a new instance for EVERY request
// All dependencies also become request-scoped
async findAll() {
return this.userRepo.find();
}
}
// Singleton with mutable request state
@Injectable() // Default: singleton
export class RequestContextService {
private userId: string; // DANGER: Shared across all requests!
setUser(userId: string) {
this.userId = userId; // Overwrites for all concurrent requests
}
getUser() {
return this.userId; // Returns wrong user!
}
}
```
**Correct (appropriate scope for each use case):**
```typescript
// Singleton for stateless services (default, most common)
@Injectable()
export class UsersService {
constructor(private readonly userRepo: UserRepository) {}
async findById(id: string): Promise<User> {
return this.userRepo.findOne({ where: { id } });
}
}
// Request-scoped ONLY when you need request context
@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
private userId: string;
setUser(userId: string) {
this.userId = userId;
}
getUser(): string {
return this.userId;
}
}
// Better: Use NestJS built-in request context
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class AuditService {
constructor(@Inject(REQUEST) private request: Request) {}
log(action: string) {
console.log(`User ${this.request.user?.id} performed ${action}`);
}
}
// Best: Use ClsModule for async context (no scope bubble-up)
import { ClsService } from 'nestjs-cls';
@Injectable() // Stays singleton!
export class AuditService {
constructor(private cls: ClsService) {}
log(action: string) {
const userId = this.cls.get('userId');
console.log(`User ${userId} performed ${action}`);
}
}
```
Reference: [NestJS Injection Scopes](https://docs.nestjs.com/fundamentals/injection-scopes)

101
.agents/skills/nestjs-best-practices/rules/di-use-interfaces-tokens.md

@ -0,0 +1,101 @@
---
title: Use Injection Tokens for Interfaces
impact: HIGH
impactDescription: Enables interface-based DI at runtime
tags: dependency-injection, tokens, interfaces
---
## Use Injection Tokens for Interfaces
TypeScript interfaces are erased at compile time and can't be used as injection tokens. Use string tokens, symbols, or abstract classes when you want to inject implementations of interfaces. This enables swapping implementations for testing or different environments.
**Incorrect (interface can't be used as token):**
```typescript
// Interface can't be used as injection token
interface PaymentGateway {
charge(amount: number): Promise<PaymentResult>;
}
@Injectable()
export class StripeService implements PaymentGateway {
charge(amount: number) { /* ... */ }
}
@Injectable()
export class OrdersService {
// This WON'T work - PaymentGateway doesn't exist at runtime
constructor(private payment: PaymentGateway) {}
}
```
**Correct (symbol tokens or abstract classes):**
```typescript
// Option 1: String/Symbol tokens (most flexible)
export const PAYMENT_GATEWAY = Symbol('PAYMENT_GATEWAY');
export interface PaymentGateway {
charge(amount: number): Promise<PaymentResult>;
}
@Injectable()
export class StripeService implements PaymentGateway {
async charge(amount: number): Promise<PaymentResult> {
// Stripe implementation
}
}
@Injectable()
export class MockPaymentService implements PaymentGateway {
async charge(amount: number): Promise<PaymentResult> {
return { success: true, id: 'mock-id' };
}
}
// Module registration
@Module({
providers: [
{
provide: PAYMENT_GATEWAY,
useClass: process.env.NODE_ENV === 'test'
? MockPaymentService
: StripeService,
},
],
exports: [PAYMENT_GATEWAY],
})
export class PaymentModule {}
// Injection
@Injectable()
export class OrdersService {
constructor(
@Inject(PAYMENT_GATEWAY) private payment: PaymentGateway,
) {}
async createOrder(dto: CreateOrderDto) {
await this.payment.charge(dto.amount);
}
}
// Option 2: Abstract class (carries runtime type info)
export abstract class PaymentGateway {
abstract charge(amount: number): Promise<PaymentResult>;
}
@Injectable()
export class StripeService extends PaymentGateway {
async charge(amount: number): Promise<PaymentResult> {
// Implementation
}
}
// No @Inject needed with abstract class
@Injectable()
export class OrdersService {
constructor(private payment: PaymentGateway) {}
}
```
Reference: [NestJS Custom Providers](https://docs.nestjs.com/fundamentals/custom-providers)

125
.agents/skills/nestjs-best-practices/rules/error-handle-async-errors.md

@ -0,0 +1,125 @@
---
title: Handle Async Errors Properly
impact: HIGH
impactDescription: Prevents process crashes from unhandled rejections
tags: error-handling, async, promises
---
## Handle Async Errors Properly
NestJS automatically catches errors from async route handlers, but errors from background tasks, event handlers, and manually created promises can crash your application. Always handle async errors explicitly and use global handlers as a safety net.
**Incorrect (fire-and-forget without error handling):**
```typescript
// Fire-and-forget without error handling
@Injectable()
export class UsersService {
async createUser(dto: CreateUserDto): Promise<User> {
const user = await this.repo.save(dto);
// Fire and forget - if this fails, error is unhandled!
this.emailService.sendWelcome(user.email);
return user;
}
}
// Unhandled promise in event handler
@Injectable()
export class OrdersService {
@OnEvent('order.created')
handleOrderCreated(event: OrderCreatedEvent) {
// This returns a promise but it's not awaited!
this.processOrder(event);
// Errors will crash the process
}
private async processOrder(event: OrderCreatedEvent): Promise<void> {
await this.inventoryService.reserve(event.items);
await this.notificationService.send(event.userId);
}
}
// Missing try-catch in scheduled tasks
@Cron('0 0 * * *')
async dailyCleanup(): Promise<void> {
await this.cleanupService.run();
// If this throws, no error handling
}
```
**Correct (explicit async error handling):**
```typescript
// Handle fire-and-forget with explicit catch
@Injectable()
export class UsersService {
private readonly logger = new Logger(UsersService.name);
async createUser(dto: CreateUserDto): Promise<User> {
const user = await this.repo.save(dto);
// Explicitly catch and log errors
this.emailService.sendWelcome(user.email).catch((error) => {
this.logger.error('Failed to send welcome email', error.stack);
// Optionally queue for retry
});
return user;
}
}
// Properly handle async event handlers
@Injectable()
export class OrdersService {
private readonly logger = new Logger(OrdersService.name);
@OnEvent('order.created')
async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
try {
await this.processOrder(event);
} catch (error) {
this.logger.error('Failed to process order', { event, error });
// Don't rethrow - would crash the process
await this.deadLetterQueue.add('order.created', event);
}
}
}
// Safe scheduled tasks
@Injectable()
export class CleanupService {
private readonly logger = new Logger(CleanupService.name);
@Cron('0 0 * * *')
async dailyCleanup(): Promise<void> {
try {
await this.cleanupService.run();
this.logger.log('Daily cleanup completed');
} catch (error) {
this.logger.error('Daily cleanup failed', error.stack);
// Alert or retry logic
}
}
}
// Global unhandled rejection handler in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const logger = new Logger('Bootstrap');
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
});
process.on('uncaughtException', (error) => {
logger.error('Uncaught Exception:', error);
process.exit(1);
});
await app.listen(3000);
}
```
Reference: [Node.js Unhandled Rejections](https://nodejs.org/api/process.html#event-unhandledrejection)

114
.agents/skills/nestjs-best-practices/rules/error-throw-http-exceptions.md

@ -0,0 +1,114 @@
---
title: Throw HTTP Exceptions from Services
impact: HIGH
impactDescription: Keeps controllers thin and simplifies error handling
tags: error-handling, exceptions, services
---
## Throw HTTP Exceptions from Services
It's acceptable (and often preferable) to throw `HttpException` subclasses from services in HTTP applications. This keeps controllers thin and allows services to communicate appropriate error states. For truly layer-agnostic services, use domain exceptions that map to HTTP status codes.
**Incorrect (return error objects instead of throwing):**
```typescript
// Return error objects instead of throwing
@Injectable()
export class UsersService {
async findById(id: string): Promise<{ user?: User; error?: string }> {
const user = await this.repo.findOne({ where: { id } });
if (!user) {
return { error: 'User not found' }; // Controller must check this
}
return { user };
}
}
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string) {
const result = await this.usersService.findById(id);
if (result.error) {
throw new NotFoundException(result.error);
}
return result.user;
}
}
```
**Correct (throw exceptions directly from service):**
```typescript
// Throw exceptions directly from service
@Injectable()
export class UsersService {
constructor(private readonly repo: UserRepository) {}
async findById(id: string): Promise<User> {
const user = await this.repo.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
async create(dto: CreateUserDto): Promise<User> {
const existing = await this.repo.findOne({
where: { email: dto.email },
});
if (existing) {
throw new ConflictException('Email already registered');
}
return this.repo.save(dto);
}
async update(id: string, dto: UpdateUserDto): Promise<User> {
const user = await this.findById(id); // Throws if not found
Object.assign(user, dto);
return this.repo.save(user);
}
}
// Controller stays thin
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findById(id);
}
@Post()
create(@Body() dto: CreateUserDto): Promise<User> {
return this.usersService.create(dto);
}
}
// For layer-agnostic services, use domain exceptions
export class EntityNotFoundException extends Error {
constructor(
public readonly entity: string,
public readonly id: string,
) {
super(`${entity} with ID "${id}" not found`);
}
}
// Map to HTTP in exception filter
@Catch(EntityNotFoundException)
export class EntityNotFoundFilter implements ExceptionFilter {
catch(exception: EntityNotFoundException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
response.status(404).json({
statusCode: 404,
message: exception.message,
entity: exception.entity,
id: exception.id,
});
}
}
```
Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)

140
.agents/skills/nestjs-best-practices/rules/error-use-exception-filters.md

@ -0,0 +1,140 @@
---
title: Use Exception Filters for Error Handling
impact: HIGH
impactDescription: Consistent, centralized error handling
tags: error-handling, exception-filters, consistency
---
## Use Exception Filters for Error Handling
Never catch exceptions and manually format error responses in controllers. Use NestJS exception filters to handle errors consistently across your application. Create custom exception filters for specific error types and a global filter for unhandled exceptions.
**Incorrect (manual error handling in controllers):**
```typescript
// Manual error handling in controllers
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string, @Res() res: Response) {
try {
const user = await this.usersService.findById(id);
if (!user) {
return res.status(404).json({
statusCode: 404,
message: 'User not found',
});
}
return res.json(user);
} catch (error) {
console.error(error);
return res.status(500).json({
statusCode: 500,
message: 'Internal server error',
});
}
}
}
```
**Correct (exception filters with consistent handling):**
```typescript
// Use built-in and custom exceptions
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
const user = await this.usersService.findById(id);
if (!user) {
throw new NotFoundException(`User #${id} not found`);
}
return user;
}
}
// Custom domain exception
export class UserNotFoundException extends NotFoundException {
constructor(userId: string) {
super({
statusCode: 404,
error: 'Not Found',
message: `User with ID "${userId}" not found`,
code: 'USER_NOT_FOUND',
});
}
}
// Custom exception filter for domain errors
@Catch(DomainException)
export class DomainExceptionFilter implements ExceptionFilter {
catch(exception: DomainException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus?.() || 400;
response.status(status).json({
statusCode: status,
code: exception.code,
message: exception.message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
// Global exception filter for unhandled errors
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
constructor(private readonly logger: Logger) {}
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.message
: 'Internal server error';
this.logger.error(
`${request.method} ${request.url}`,
exception instanceof Error ? exception.stack : exception,
);
response.status(status).json({
statusCode: status,
message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
// Register globally in main.ts
app.useGlobalFilters(
new AllExceptionsFilter(app.get(Logger)),
new DomainExceptionFilter(),
);
// Or via module
@Module({
providers: [
{
provide: APP_FILTER,
useClass: AllExceptionsFilter,
},
],
})
export class AppModule {}
```
Reference: [NestJS Exception Filters](https://docs.nestjs.com/exception-filters)

226
.agents/skills/nestjs-best-practices/rules/micro-use-health-checks.md

@ -0,0 +1,226 @@
---
title: Implement Health Checks for Microservices
impact: MEDIUM-HIGH
impactDescription: Health checks enable orchestrators to manage service lifecycle
tags: microservices, health-checks, terminus, kubernetes
---
## Implement Health Checks for Microservices
Implement liveness and readiness probes using `@nestjs/terminus`. Liveness checks determine if the service should be restarted. Readiness checks determine if the service can accept traffic. Proper health checks enable Kubernetes and load balancers to route traffic correctly.
**Incorrect (simple ping that doesn't check dependencies):**
```typescript
// Simple ping that doesn't check dependencies
@Controller('health')
export class HealthController {
@Get()
check(): string {
return 'OK'; // Service might be unhealthy but returns OK
}
}
// Health check that blocks on slow dependencies
@Controller('health')
export class HealthController {
@Get()
async check(): Promise<string> {
// If database is slow, health check times out
await this.userRepo.findOne({ where: { id: '1' } });
await this.redis.ping();
await this.externalApi.healthCheck();
return 'OK';
}
}
```
**Correct (use @nestjs/terminus for comprehensive health checks):**
```typescript
// Use @nestjs/terminus for comprehensive health checks
import {
HealthCheckService,
HttpHealthIndicator,
TypeOrmHealthIndicator,
HealthCheck,
DiskHealthIndicator,
MemoryHealthIndicator,
} from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private http: HttpHealthIndicator,
private db: TypeOrmHealthIndicator,
private disk: DiskHealthIndicator,
private memory: MemoryHealthIndicator,
) {}
// Liveness probe - is the service alive?
@Get('live')
@HealthCheck()
liveness() {
return this.health.check([
// Basic checks only
() => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024), // 200MB
]);
}
// Readiness probe - can the service handle traffic?
@Get('ready')
@HealthCheck()
readiness() {
return this.health.check([
() => this.db.pingCheck('database'),
() =>
this.http.pingCheck('redis', 'http://redis:6379', { timeout: 1000 }),
() =>
this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }),
]);
}
// Deep health check for debugging
@Get('deep')
@HealthCheck()
deepCheck() {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024),
() => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024),
() =>
this.disk.checkStorage('disk', { path: '/', thresholdPercent: 0.9 }),
() =>
this.http.pingCheck('external-api', 'https://api.example.com/health'),
]);
}
}
// Custom indicator for business-specific health
@Injectable()
export class QueueHealthIndicator extends HealthIndicator {
constructor(private queueService: QueueService) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
const queueStats = await this.queueService.getStats();
const isHealthy = queueStats.failedCount < 100;
const result = this.getStatus(key, isHealthy, {
waiting: queueStats.waitingCount,
active: queueStats.activeCount,
failed: queueStats.failedCount,
});
if (!isHealthy) {
throw new HealthCheckError('Queue unhealthy', result);
}
return result;
}
}
// Redis health indicator
@Injectable()
export class RedisHealthIndicator extends HealthIndicator {
constructor(@InjectRedis() private redis: Redis) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
try {
const pong = await this.redis.ping();
return this.getStatus(key, pong === 'PONG');
} catch (error) {
throw new HealthCheckError('Redis check failed', this.getStatus(key, false));
}
}
}
// Use custom indicators
@Get('ready')
@HealthCheck()
readiness() {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.redis.isHealthy('redis'),
() => this.queue.isHealthy('job-queue'),
]);
}
// Graceful shutdown handling
@Injectable()
export class GracefulShutdownService implements OnApplicationShutdown {
private isShuttingDown = false;
isShutdown(): boolean {
return this.isShuttingDown;
}
async onApplicationShutdown(signal: string): Promise<void> {
this.isShuttingDown = true;
console.log(`Shutting down on ${signal}`);
// Wait for in-flight requests
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
// Health check respects shutdown state
@Get('ready')
@HealthCheck()
readiness() {
if (this.shutdownService.isShutdown()) {
throw new ServiceUnavailableException('Shutting down');
}
return this.health.check([
() => this.db.pingCheck('database'),
]);
}
```
### Kubernetes Configuration
```yaml
# Kubernetes deployment with probes
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
spec:
template:
spec:
containers:
- name: api
image: api-service:latest
ports:
- containerPort: 3000
livenessProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health/live
port: 3000
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 30
```
Reference: [NestJS Terminus](https://docs.nestjs.com/recipes/terminus)

167
.agents/skills/nestjs-best-practices/rules/micro-use-patterns.md

@ -0,0 +1,167 @@
---
title: Use Message and Event Patterns Correctly
impact: MEDIUM
impactDescription: Proper patterns ensure reliable microservice communication
tags: microservices, message-pattern, event-pattern, communication
---
## Use Message and Event Patterns Correctly
NestJS microservices support two communication patterns: request-response (MessagePattern) and event-based (EventPattern). Use MessagePattern when you need a response, and EventPattern for fire-and-forget notifications. Understanding the difference prevents communication bugs.
**Incorrect (using wrong pattern for use case):**
```typescript
// Use @MessagePattern for fire-and-forget
@Controller()
export class NotificationsController {
@MessagePattern('user.created')
async handleUserCreated(data: UserCreatedEvent) {
// This WAITS for response, blocking the sender
await this.emailService.sendWelcome(data.email);
// If email fails, sender gets an error (coupling!)
}
}
// Use @EventPattern expecting a response
@Controller()
export class OrdersController {
@EventPattern('inventory.check')
async checkInventory(data: CheckInventoryDto) {
const available = await this.inventory.check(data);
return available; // This return value is IGNORED with @EventPattern!
}
}
// Tight coupling in client
@Injectable()
export class UsersService {
async createUser(dto: CreateUserDto): Promise<User> {
const user = await this.repo.save(dto);
// Blocks until notification service responds
await this.client.send('user.created', user).toPromise();
// If notification service is down, user creation fails!
return user;
}
}
```
**Correct (use MessagePattern for request-response, EventPattern for fire-and-forget):**
```typescript
// MessagePattern: Request-Response (when you NEED a response)
@Controller()
export class InventoryController {
@MessagePattern({ cmd: 'check_inventory' })
async checkInventory(data: CheckInventoryDto): Promise<InventoryResult> {
const result = await this.inventoryService.check(data.productId, data.quantity);
return result; // Response sent back to caller
}
}
// Client expects response
@Injectable()
export class OrdersService {
async createOrder(dto: CreateOrderDto): Promise<Order> {
// Check inventory - we NEED this response to proceed
const inventory = await firstValueFrom(
this.inventoryClient.send<InventoryResult>(
{ cmd: 'check_inventory' },
{ productId: dto.productId, quantity: dto.quantity },
),
);
if (!inventory.available) {
throw new BadRequestException('Insufficient inventory');
}
return this.repo.save(dto);
}
}
// EventPattern: Fire-and-Forget (for notifications, side effects)
@Controller()
export class NotificationsController {
@EventPattern('user.created')
async handleUserCreated(data: UserCreatedEvent): Promise<void> {
// No return value needed - just process the event
await this.emailService.sendWelcome(data.email);
await this.analyticsService.track('user_signup', data);
// If this fails, it doesn't affect the sender
}
}
// Client emits event without waiting
@Injectable()
export class UsersService {
async createUser(dto: CreateUserDto): Promise<User> {
const user = await this.repo.save(dto);
// Fire and forget - doesn't block, doesn't wait
this.eventClient.emit('user.created', {
userId: user.id,
email: user.email,
timestamp: new Date(),
});
return user; // User creation succeeds regardless of event handling
}
}
// Hybrid pattern for critical events
@Injectable()
export class OrdersService {
async createOrder(dto: CreateOrderDto): Promise<Order> {
const order = await this.repo.save(dto);
// Critical: inventory reservation (use MessagePattern)
const reserved = await firstValueFrom(
this.inventoryClient.send({ cmd: 'reserve_inventory' }, {
orderId: order.id,
items: dto.items,
}),
);
if (!reserved.success) {
await this.repo.delete(order.id);
throw new BadRequestException('Could not reserve inventory');
}
// Non-critical: notifications (use EventPattern)
this.eventClient.emit('order.created', {
orderId: order.id,
userId: dto.userId,
total: dto.total,
});
return order;
}
}
// Error handling patterns
// MessagePattern errors propagate to caller
@MessagePattern({ cmd: 'get_user' })
async getUser(userId: string): Promise<User> {
const user = await this.repo.findOne({ where: { id: userId } });
if (!user) {
throw new RpcException('User not found'); // Received by caller
}
return user;
}
// EventPattern errors should be handled locally
@EventPattern('order.created')
async handleOrderCreated(data: OrderCreatedEvent): Promise<void> {
try {
await this.processOrder(data);
} catch (error) {
// Log and potentially retry - don't throw
this.logger.error('Failed to process order event', error);
await this.deadLetterQueue.add(data);
}
}
```
Reference: [NestJS Microservices](https://docs.nestjs.com/microservices/basics)

252
.agents/skills/nestjs-best-practices/rules/micro-use-queues.md

@ -0,0 +1,252 @@
---
title: Use Message Queues for Background Jobs
impact: MEDIUM-HIGH
impactDescription: Queues enable reliable background processing
tags: microservices, queues, bullmq, background-jobs
---
## Use Message Queues for Background Jobs
Use `@nestjs/bullmq` for background job processing. Queues decouple long-running tasks from HTTP requests, enable retry logic, and distribute workload across workers. Use them for emails, file processing, notifications, and any task that shouldn't block user requests.
**Incorrect (long-running tasks in HTTP handlers):**
```typescript
// Long-running tasks in HTTP handlers
@Controller('reports')
export class ReportsController {
@Post()
async generate(@Body() dto: GenerateReportDto): Promise<Report> {
// This blocks the request for potentially minutes
const data = await this.fetchLargeDataset(dto);
const report = await this.processData(data); // Slow!
await this.sendEmail(dto.email, report); // Can fail!
return report; // Client times out
}
}
// Fire-and-forget without retry
@Injectable()
export class EmailService {
async sendWelcome(email: string): Promise<void> {
// If this fails, email is never sent
await this.mailer.send({ to: email, template: 'welcome' });
// No retry, no tracking, no visibility
}
}
// Use setInterval for scheduled tasks
setInterval(async () => {
await cleanupOldRecords();
}, 60000); // No error handling, memory leaks
```
**Correct (use BullMQ for background processing):**
```typescript
// Configure BullMQ
import { BullModule } from '@nestjs/bullmq';
@Module({
imports: [
BullModule.forRoot({
connection: {
host: 'localhost',
port: 6379,
},
defaultJobOptions: {
removeOnComplete: 1000,
removeOnFail: 5000,
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000,
},
},
}),
BullModule.registerQueue(
{ name: 'email' },
{ name: 'reports' },
{ name: 'notifications' },
),
],
})
export class QueueModule {}
// Producer: Add jobs to queue
@Injectable()
export class ReportsService {
constructor(
@InjectQueue('reports') private reportsQueue: Queue,
) {}
async requestReport(dto: GenerateReportDto): Promise<{ jobId: string }> {
// Return immediately, process in background
const job = await this.reportsQueue.add('generate', dto, {
priority: dto.urgent ? 1 : 10,
delay: dto.scheduledFor ? Date.parse(dto.scheduledFor) - Date.now() : 0,
});
return { jobId: job.id };
}
async getJobStatus(jobId: string): Promise<JobStatus> {
const job = await this.reportsQueue.getJob(jobId);
return {
status: await job.getState(),
progress: job.progress,
result: job.returnvalue,
};
}
}
// Consumer: Process jobs
@Processor('reports')
export class ReportsProcessor {
private readonly logger = new Logger(ReportsProcessor.name);
@Process('generate')
async generateReport(job: Job<GenerateReportDto>): Promise<Report> {
this.logger.log(`Processing report job ${job.id}`);
// Update progress
await job.updateProgress(10);
const data = await this.fetchData(job.data);
await job.updateProgress(50);
const report = await this.processData(data);
await job.updateProgress(90);
await this.saveReport(report);
await job.updateProgress(100);
return report;
}
@OnQueueActive()
onActive(job: Job) {
this.logger.log(`Processing job ${job.id}`);
}
@OnQueueCompleted()
onCompleted(job: Job, result: any) {
this.logger.log(`Job ${job.id} completed`);
}
@OnQueueFailed()
onFailed(job: Job, error: Error) {
this.logger.error(`Job ${job.id} failed: ${error.message}`);
}
}
// Email queue with retry
@Processor('email')
export class EmailProcessor {
@Process('send')
async sendEmail(job: Job<SendEmailDto>): Promise<void> {
const { to, template, data } = job.data;
try {
await this.mailer.send({
to,
template,
context: data,
});
} catch (error) {
// BullMQ will retry based on job options
throw error;
}
}
}
// Usage
@Injectable()
export class NotificationService {
constructor(@InjectQueue('email') private emailQueue: Queue) {}
async sendWelcome(user: User): Promise<void> {
await this.emailQueue.add(
'send',
{
to: user.email,
template: 'welcome',
data: { name: user.name },
},
{
attempts: 5,
backoff: { type: 'exponential', delay: 5000 },
},
);
}
}
// Scheduled jobs
@Injectable()
export class ScheduledJobsService implements OnModuleInit {
constructor(@InjectQueue('maintenance') private queue: Queue) {}
async onModuleInit(): Promise<void> {
// Clean up old reports daily at midnight
await this.queue.add(
'cleanup',
{},
{
repeat: { cron: '0 0 * * *' },
jobId: 'daily-cleanup', // Prevent duplicates
},
);
// Send digest every hour
await this.queue.add(
'digest',
{},
{
repeat: { every: 60 * 60 * 1000 },
jobId: 'hourly-digest',
},
);
}
}
@Processor('maintenance')
export class MaintenanceProcessor {
@Process('cleanup')
async cleanup(): Promise<void> {
await this.cleanupOldReports();
await this.cleanupExpiredSessions();
}
@Process('digest')
async sendDigest(): Promise<void> {
const users = await this.getUsersForDigest();
for (const user of users) {
await this.emailQueue.add('send', { to: user.email, template: 'digest' });
}
}
}
// Queue monitoring with Bull Board
import { BullBoardModule } from '@bull-board/nestjs';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
@Module({
imports: [
BullBoardModule.forRoot({
route: '/admin/queues',
adapter: ExpressAdapter,
}),
BullBoardModule.forFeature({
name: 'email',
adapter: BullMQAdapter,
}),
BullBoardModule.forFeature({
name: 'reports',
adapter: BullMQAdapter,
}),
],
})
export class AdminModule {}
```
Reference: [NestJS Queues](https://docs.nestjs.com/techniques/queues)

109
.agents/skills/nestjs-best-practices/rules/perf-async-hooks.md

@ -0,0 +1,109 @@
---
title: Use Async Lifecycle Hooks Correctly
impact: HIGH
impactDescription: Improper async handling blocks application startup
tags: performance, lifecycle, async, hooks
---
## Use Async Lifecycle Hooks Correctly
NestJS lifecycle hooks (`onModuleInit`, `onApplicationBootstrap`, etc.) support async operations. However, misusing them can block application startup or cause race conditions. Understand the lifecycle order and use hooks appropriately.
**Incorrect (fire-and-forget async without await):**
```typescript
// Fire-and-forget async without await
@Injectable()
export class DatabaseService implements OnModuleInit {
onModuleInit() {
// This runs but doesn't block - app starts before DB is ready!
this.connect();
}
private async connect() {
await this.pool.connect();
console.log('Database connected');
}
}
// Heavy blocking operations in constructor
@Injectable()
export class ConfigService {
private config: Config;
constructor() {
// BLOCKS entire module instantiation synchronously
this.config = fs.readFileSync('config.json');
}
}
```
**Correct (return promises from async hooks):**
```typescript
// Return promise from async hooks
@Injectable()
export class DatabaseService implements OnModuleInit {
private pool: Pool;
async onModuleInit(): Promise<void> {
// NestJS waits for this to complete before continuing
await this.pool.connect();
console.log('Database connected');
}
async onModuleDestroy(): Promise<void> {
// Clean up resources on shutdown
await this.pool.end();
console.log('Database disconnected');
}
}
// Use onApplicationBootstrap for cross-module dependencies
@Injectable()
export class CacheWarmerService implements OnApplicationBootstrap {
constructor(
private cache: CacheService,
private products: ProductsService,
) {}
async onApplicationBootstrap(): Promise<void> {
// All modules are initialized, safe to warm cache
const products = await this.products.findPopular();
await this.cache.warmup(products);
}
}
// Heavy init in async hooks, not constructor
@Injectable()
export class ConfigService implements OnModuleInit {
private config: Config;
constructor() {
// Keep constructor synchronous and fast
}
async onModuleInit(): Promise<void> {
// Async loading in lifecycle hook
this.config = await this.loadConfig();
}
private async loadConfig(): Promise<Config> {
const file = await fs.promises.readFile('config.json');
return JSON.parse(file.toString());
}
get<T>(key: string): T {
return this.config[key];
}
}
// Enable shutdown hooks in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks(); // Enable SIGTERM/SIGINT handling
await app.listen(3000);
}
```
Reference: [NestJS Lifecycle Events](https://docs.nestjs.com/fundamentals/lifecycle-events)

121
.agents/skills/nestjs-best-practices/rules/perf-lazy-loading.md

@ -0,0 +1,121 @@
---
title: Use Lazy Loading for Large Modules
impact: MEDIUM
impactDescription: Improves startup time for large applications
tags: performance, lazy-loading, modules, optimization
---
## Use Lazy Loading for Large Modules
NestJS supports lazy-loading modules, which defers initialization until first use. This is valuable for large applications where some features are rarely used, serverless deployments where cold start time matters, or when certain modules have heavy initialization costs.
**Incorrect (loading everything eagerly):**
```typescript
// Load everything eagerly in a large app
@Module({
imports: [
UsersModule,
OrdersModule,
PaymentsModule,
ReportsModule, // Heavy, rarely used
AnalyticsModule, // Heavy, rarely used
AdminModule, // Only admins use this
LegacyModule, // Migration module, rarely used
BulkImportModule, // Used once a month
],
})
export class AppModule {}
// All modules initialize at startup, even if never used
// Slow cold starts in serverless
// Memory wasted on unused modules
```
**Correct (lazy load rarely-used modules):**
```typescript
// Use LazyModuleLoader for optional modules
import { LazyModuleLoader } from '@nestjs/core';
@Injectable()
export class ReportsService {
constructor(private lazyModuleLoader: LazyModuleLoader) {}
async generateReport(type: string): Promise<Report> {
// Load module only when needed
const { ReportsModule } = await import('./reports/reports.module');
const moduleRef = await this.lazyModuleLoader.load(() => ReportsModule);
const reportsService = moduleRef.get(ReportsGeneratorService);
return reportsService.generate(type);
}
}
// Lazy load admin features with caching
@Injectable()
export class AdminService {
private adminModule: ModuleRef | null = null;
constructor(private lazyModuleLoader: LazyModuleLoader) {}
private async getAdminModule(): Promise<ModuleRef> {
if (!this.adminModule) {
const { AdminModule } = await import('./admin/admin.module');
this.adminModule = await this.lazyModuleLoader.load(() => AdminModule);
}
return this.adminModule;
}
async runAdminTask(task: string): Promise<void> {
const moduleRef = await this.getAdminModule();
const taskRunner = moduleRef.get(AdminTaskRunner);
await taskRunner.run(task);
}
}
// Reusable lazy loader service
@Injectable()
export class ModuleLoaderService {
private loadedModules = new Map<string, ModuleRef>();
constructor(private lazyModuleLoader: LazyModuleLoader) {}
async load<T>(
key: string,
importFn: () => Promise<{ default: Type<T> } | Type<T>>,
): Promise<ModuleRef> {
if (!this.loadedModules.has(key)) {
const module = await importFn();
const moduleType = 'default' in module ? module.default : module;
const moduleRef = await this.lazyModuleLoader.load(() => moduleType);
this.loadedModules.set(key, moduleRef);
}
return this.loadedModules.get(key)!;
}
}
// Preload modules in background after startup
@Injectable()
export class ModulePreloader implements OnApplicationBootstrap {
constructor(private lazyModuleLoader: LazyModuleLoader) {}
async onApplicationBootstrap(): Promise<void> {
setTimeout(async () => {
await this.preloadModule(() => import('./reports/reports.module'));
}, 5000); // 5 seconds after startup
}
private async preloadModule(importFn: () => Promise<any>): Promise<void> {
try {
const module = await importFn();
const moduleType = module.default || Object.values(module)[0];
await this.lazyModuleLoader.load(() => moduleType);
} catch (error) {
console.warn('Failed to preload module', error);
}
}
}
```
Reference: [NestJS Lazy Loading Modules](https://docs.nestjs.com/fundamentals/lazy-loading-modules)

131
.agents/skills/nestjs-best-practices/rules/perf-optimize-database.md

@ -0,0 +1,131 @@
---
title: Optimize Database Queries
impact: HIGH
impactDescription: Database queries are typically the largest source of latency
tags: performance, database, queries, optimization
---
## Optimize Database Queries
Select only needed columns, use proper indexes, avoid over-fetching relations, and consider query performance when designing your data access. Most API slowness traces back to inefficient database queries.
**Incorrect (over-fetching data and missing indexes):**
```typescript
// Select everything when you need few fields
@Injectable()
export class UsersService {
async findAllEmails(): Promise<string[]> {
const users = await this.repo.find();
// Fetches ALL columns for ALL users
return users.map((u) => u.email);
}
async getUserSummary(id: string): Promise<UserSummary> {
const user = await this.repo.findOne({
where: { id },
relations: ['posts', 'posts.comments', 'posts.comments.author', 'followers'],
});
// Over-fetches massive relation tree
return { name: user.name, postCount: user.posts.length };
}
}
// No indexes on frequently queried columns
@Entity()
export class Order {
@Column()
userId: string; // No index - full table scan on every lookup
@Column()
status: string; // No index - slow status filtering
}
```
**Correct (select only needed data with proper indexes):**
```typescript
// Select only needed columns
@Injectable()
export class UsersService {
async findAllEmails(): Promise<string[]> {
const users = await this.repo.find({
select: ['email'], // Only fetch email column
});
return users.map((u) => u.email);
}
// Use QueryBuilder for complex selections
async getUserSummary(id: string): Promise<UserSummary> {
return this.repo
.createQueryBuilder('user')
.select('user.name', 'name')
.addSelect('COUNT(post.id)', 'postCount')
.leftJoin('user.posts', 'post')
.where('user.id = :id', { id })
.groupBy('user.id')
.getRawOne();
}
// Fetch relations only when needed
async getFullProfile(id: string): Promise<User> {
return this.repo.findOne({
where: { id },
relations: ['posts'], // Only immediate relation
select: {
id: true,
name: true,
email: true,
posts: {
id: true,
title: true,
},
},
});
}
}
// Add indexes on frequently queried columns
@Entity()
@Index(['userId'])
@Index(['status'])
@Index(['createdAt'])
@Index(['userId', 'status']) // Composite index for common query pattern
export class Order {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
userId: string;
@Column()
status: string;
@CreateDateColumn()
createdAt: Date;
}
// Always paginate large datasets
@Injectable()
export class OrdersService {
async findAll(page = 1, limit = 20): Promise<PaginatedResult<Order>> {
const [items, total] = await this.repo.findAndCount({
skip: (page - 1) * limit,
take: limit,
order: { createdAt: 'DESC' },
});
return {
items,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
}
}
```
Reference: [TypeORM Query Builder](https://typeorm.io/select-query-builder)

128
.agents/skills/nestjs-best-practices/rules/perf-use-caching.md

@ -0,0 +1,128 @@
---
title: Use Caching Strategically
impact: HIGH
impactDescription: Dramatically reduces database load and response times
tags: performance, caching, redis, optimization
---
## Use Caching Strategically
Implement caching for expensive operations, frequently accessed data, and external API calls. Use NestJS CacheModule with appropriate TTLs and cache invalidation strategies. Don't cache everything - focus on high-impact areas.
**Incorrect (no caching or caching everything):**
```typescript
// No caching for expensive, repeated queries
@Injectable()
export class ProductsService {
async getPopular(): Promise<Product[]> {
// Runs complex aggregation query EVERY request
return this.productsRepo
.createQueryBuilder('p')
.leftJoin('p.orders', 'o')
.select('p.*, COUNT(o.id) as orderCount')
.groupBy('p.id')
.orderBy('orderCount', 'DESC')
.limit(20)
.getMany();
}
}
// Cache everything without thought
@Injectable()
export class UsersService {
@CacheKey('users')
@CacheTTL(3600)
@UseInterceptors(CacheInterceptor)
async findAll(): Promise<User[]> {
// Caching user list for 1 hour is wrong if data changes frequently
return this.usersRepo.find();
}
}
```
**Correct (strategic caching with proper invalidation):**
```typescript
// Setup caching module
@Module({
imports: [
CacheModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
stores: [
new KeyvRedis(config.get('REDIS_URL')),
],
ttl: 60 * 1000, // Default 60s
}),
}),
],
})
export class AppModule {}
// Manual caching for granular control
@Injectable()
export class ProductsService {
constructor(
@Inject(CACHE_MANAGER) private cache: Cache,
private productsRepo: ProductRepository,
) {}
async getPopular(): Promise<Product[]> {
const cacheKey = 'products:popular';
// Try cache first
const cached = await this.cache.get<Product[]>(cacheKey);
if (cached) return cached;
// Cache miss - fetch and cache
const products = await this.fetchPopularProducts();
await this.cache.set(cacheKey, products, 5 * 60 * 1000); // 5 min TTL
return products;
}
// Invalidate cache on changes
async updateProduct(id: string, dto: UpdateProductDto): Promise<Product> {
const product = await this.productsRepo.save({ id, ...dto });
await this.cache.del('products:popular'); // Invalidate
return product;
}
}
// Decorator-based caching with auto-interceptor
@Controller('categories')
@UseInterceptors(CacheInterceptor)
export class CategoriesController {
@Get()
@CacheTTL(30 * 60 * 1000) // 30 minutes - categories rarely change
findAll(): Promise<Category[]> {
return this.categoriesService.findAll();
}
@Get(':id')
@CacheTTL(60 * 1000) // 1 minute
@CacheKey('category')
findOne(@Param('id') id: string): Promise<Category> {
return this.categoriesService.findOne(id);
}
}
// Event-based cache invalidation
@Injectable()
export class CacheInvalidationService {
constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}
@OnEvent('product.created')
@OnEvent('product.updated')
@OnEvent('product.deleted')
async invalidateProductCaches(event: ProductEvent) {
await Promise.all([
this.cache.del('products:popular'),
this.cache.del(`product:${event.productId}`),
]);
}
}
```
Reference: [NestJS Caching](https://docs.nestjs.com/techniques/caching)

146
.agents/skills/nestjs-best-practices/rules/security-auth-jwt.md

@ -0,0 +1,146 @@
---
title: Implement Secure JWT Authentication
impact: CRITICAL
impactDescription: Essential for secure APIs
tags: security, jwt, authentication, tokens
---
## Implement Secure JWT Authentication
Use `@nestjs/jwt` with `@nestjs/passport` for authentication. Store secrets securely, use appropriate token lifetimes, implement refresh tokens, and validate tokens properly. Never expose sensitive data in JWT payloads.
**Incorrect (insecure JWT implementation):**
```typescript
// Hardcode secrets
@Module({
imports: [
JwtModule.register({
secret: 'my-secret-key', // Exposed in code
signOptions: { expiresIn: '7d' }, // Too long
}),
],
})
export class AuthModule {}
// Store sensitive data in JWT
async login(user: User): Promise<{ accessToken: string }> {
const payload = {
sub: user.id,
email: user.email,
password: user.password, // NEVER include password!
ssn: user.ssn, // NEVER include sensitive data!
isAdmin: user.isAdmin, // Can be tampered if not verified
};
return { accessToken: this.jwtService.sign(payload) };
}
// Skip token validation
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'my-secret',
});
}
async validate(payload: any): Promise<any> {
return payload; // No validation of user existence
}
}
```
**Correct (secure JWT with refresh tokens):**
```typescript
// Secure JWT configuration
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
secret: config.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: '15m', // Short-lived access tokens
issuer: config.get<string>('JWT_ISSUER'),
audience: config.get<string>('JWT_AUDIENCE'),
},
}),
}),
PassportModule.register({ defaultStrategy: 'jwt' }),
],
})
export class AuthModule {}
// Minimal JWT payload
@Injectable()
export class AuthService {
async login(user: User): Promise<TokenResponse> {
// Only include necessary, non-sensitive data
const payload: JwtPayload = {
sub: user.id,
email: user.email,
roles: user.roles,
iat: Math.floor(Date.now() / 1000),
};
const accessToken = this.jwtService.sign(payload);
const refreshToken = await this.createRefreshToken(user.id);
return { accessToken, refreshToken, expiresIn: 900 };
}
private async createRefreshToken(userId: string): Promise<string> {
const token = randomBytes(32).toString('hex');
const hashedToken = await bcrypt.hash(token, 10);
await this.refreshTokenRepo.save({
userId,
token: hashedToken,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
});
return token;
}
}
// Proper JWT strategy with validation
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private config: ConfigService,
private usersService: UsersService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.get<string>('JWT_SECRET'),
ignoreExpiration: false,
issuer: config.get<string>('JWT_ISSUER'),
audience: config.get<string>('JWT_AUDIENCE'),
});
}
async validate(payload: JwtPayload): Promise<User> {
// Verify user still exists and is active
const user = await this.usersService.findById(payload.sub);
if (!user || !user.isActive) {
throw new UnauthorizedException('User not found or inactive');
}
// Verify token wasn't issued before password change
if (user.passwordChangedAt) {
const tokenIssuedAt = new Date(payload.iat * 1000);
if (tokenIssuedAt < user.passwordChangedAt) {
throw new UnauthorizedException('Token invalidated by password change');
}
}
return user;
}
}
```
Reference: [NestJS Authentication](https://docs.nestjs.com/security/authentication)

125
.agents/skills/nestjs-best-practices/rules/security-rate-limiting.md

@ -0,0 +1,125 @@
---
title: Implement Rate Limiting
impact: HIGH
impactDescription: Protects against abuse and ensures fair resource usage
tags: security, rate-limiting, throttler, protection
---
## Implement Rate Limiting
Use `@nestjs/throttler` to limit request rates per client. Apply different limits for different endpoints - stricter for auth endpoints, more relaxed for read operations. Consider using Redis for distributed rate limiting in clustered deployments.
**Incorrect (no rate limiting on sensitive endpoints):**
```typescript
// No rate limiting on sensitive endpoints
@Controller('auth')
export class AuthController {
@Post('login')
async login(@Body() dto: LoginDto): Promise<TokenResponse> {
// Attackers can brute-force credentials
return this.authService.login(dto);
}
@Post('forgot-password')
async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<void> {
// Can be abused to spam users with emails
return this.authService.sendResetEmail(dto.email);
}
}
// Same limits for all endpoints
@UseGuards(ThrottlerGuard)
@Controller('api')
export class ApiController {
@Get('public-data')
async getPublic() {} // Should allow more requests
@Post('process-payment')
async payment() {} // Should be more restrictive
}
```
**Correct (configured throttler with endpoint-specific limits):**
```typescript
// Configure throttler globally with multiple limits
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot([
{
name: 'short',
ttl: 1000, // 1 second
limit: 3, // 3 requests per second
},
{
name: 'medium',
ttl: 10000, // 10 seconds
limit: 20, // 20 requests per 10 seconds
},
{
name: 'long',
ttl: 60000, // 1 minute
limit: 100, // 100 requests per minute
},
]),
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}
// Override limits per endpoint
@Controller('auth')
export class AuthController {
@Post('login')
@Throttle({ short: { limit: 5, ttl: 60000 } }) // 5 attempts per minute
async login(@Body() dto: LoginDto): Promise<TokenResponse> {
return this.authService.login(dto);
}
@Post('forgot-password')
@Throttle({ short: { limit: 3, ttl: 3600000 } }) // 3 per hour
async forgotPassword(@Body() dto: ForgotPasswordDto): Promise<void> {
return this.authService.sendResetEmail(dto.email);
}
}
// Skip throttling for certain routes
@Controller('health')
export class HealthController {
@Get()
@SkipThrottle()
check(): string {
return 'OK';
}
}
// Custom throttle per user type
@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
protected async getTracker(req: Request): Promise<string> {
// Use user ID if authenticated, IP otherwise
return req.user?.id || req.ip;
}
protected async getLimit(context: ExecutionContext): Promise<number> {
const request = context.switchToHttp().getRequest();
// Higher limits for authenticated users
if (request.user) {
return request.user.isPremium ? 1000 : 200;
}
return 50; // Anonymous users
}
}
```
Reference: [NestJS Throttler](https://docs.nestjs.com/security/rate-limiting)

139
.agents/skills/nestjs-best-practices/rules/security-sanitize-output.md

@ -0,0 +1,139 @@
---
title: Sanitize Output to Prevent XSS
impact: HIGH
impactDescription: XSS vulnerabilities can compromise user sessions and data
tags: security, xss, sanitization, html
---
## Sanitize Output to Prevent XSS
While NestJS APIs typically return JSON (which browsers don't execute), XSS risks exist when rendering HTML, storing user content, or when frontend frameworks improperly handle API responses. Sanitize user-generated content before storage and use proper Content-Type headers.
**Incorrect (storing raw HTML without sanitization):**
```typescript
// Store raw HTML from users
@Injectable()
export class CommentsService {
async create(dto: CreateCommentDto): Promise<Comment> {
// User can inject: <script>steal(document.cookie)</script>
return this.repo.save({
content: dto.content, // Raw, unsanitized
authorId: dto.authorId,
});
}
}
// Return HTML without sanitization
@Controller('pages')
export class PagesController {
@Get(':slug')
@Header('Content-Type', 'text/html')
async getPage(@Param('slug') slug: string): Promise<string> {
const page = await this.pagesService.findBySlug(slug);
// If page.content contains user input, XSS is possible
return `<html><body>${page.content}</body></html>`;
}
}
// Reflect user input in errors
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
const user = await this.repo.findOne({ where: { id } });
if (!user) {
// XSS if id contains malicious content and error is rendered
throw new NotFoundException(`User ${id} not found`);
}
return user;
}
```
**Correct (sanitize content and use proper headers):**
```typescript
// Sanitize HTML content before storage
import * as sanitizeHtml from 'sanitize-html';
@Injectable()
export class CommentsService {
private readonly sanitizeOptions: sanitizeHtml.IOptions = {
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
allowedAttributes: {
a: ['href', 'title'],
},
allowedSchemes: ['http', 'https', 'mailto'],
};
async create(dto: CreateCommentDto): Promise<Comment> {
return this.repo.save({
content: sanitizeHtml(dto.content, this.sanitizeOptions),
authorId: dto.authorId,
});
}
}
// Use validation pipe to strip HTML
import { Transform } from 'class-transformer';
export class CreatePostDto {
@IsString()
@MaxLength(1000)
@Transform(({ value }) => sanitizeHtml(value, { allowedTags: [] }))
title: string;
@IsString()
@Transform(({ value }) =>
sanitizeHtml(value, {
allowedTags: ['p', 'br', 'b', 'i', 'a'],
allowedAttributes: { a: ['href'] },
}),
)
content: string;
}
// Set proper Content-Type headers
@Controller('api')
export class ApiController {
@Get('data')
@Header('Content-Type', 'application/json')
async getData(): Promise<DataResponse> {
// JSON response - browser won't execute scripts
return this.service.getData();
}
}
// Sanitize error messages
@Get(':id')
async findOne(@Param('id', ParseUUIDPipe) id: string): Promise<User> {
const user = await this.repo.findOne({ where: { id } });
if (!user) {
// UUID validation ensures safe format
throw new NotFoundException('User not found');
}
return user;
}
// Use Helmet for CSP headers
import helmet from 'helmet';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
}),
);
await app.listen(3000);
}
```
Reference: [OWASP XSS Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)

135
.agents/skills/nestjs-best-practices/rules/security-use-guards.md

@ -0,0 +1,135 @@
---
title: Use Guards for Authentication and Authorization
impact: HIGH
impactDescription: Enforces access control before handlers execute
tags: security, guards, authentication, authorization
---
## Use Guards for Authentication and Authorization
Guards determine whether a request should be handled based on authentication state, roles, permissions, or other conditions. They run after middleware but before pipes and interceptors, making them ideal for access control. Use guards instead of manual checks in controllers.
**Incorrect (manual auth checks in every handler):**
```typescript
// Manual auth checks in every handler
@Controller('admin')
export class AdminController {
@Get('users')
async getUsers(@Request() req) {
if (!req.user) {
throw new UnauthorizedException();
}
if (!req.user.roles.includes('admin')) {
throw new ForbiddenException();
}
return this.adminService.getUsers();
}
@Delete('users/:id')
async deleteUser(@Request() req, @Param('id') id: string) {
if (!req.user) {
throw new UnauthorizedException();
}
if (!req.user.roles.includes('admin')) {
throw new ForbiddenException();
}
return this.adminService.deleteUser(id);
}
}
```
**Correct (guards with declarative decorators):**
```typescript
// JWT Auth Guard
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Check for @Public() decorator
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
const request = context.switchToHttp().getRequest();
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('No token provided');
}
try {
request.user = await this.jwtService.verifyAsync(token);
return true;
} catch {
throw new UnauthorizedException('Invalid token');
}
}
private extractToken(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
// Roles Guard
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// Decorators
export const Public = () => SetMetadata('isPublic', true);
export const Roles = (...roles: Role[]) => SetMetadata('roles', roles);
// Register guards globally
@Module({
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: RolesGuard },
],
})
export class AppModule {}
// Clean controller
@Controller('admin')
@Roles(Role.Admin) // Applied to all routes
export class AdminController {
@Get('users')
getUsers(): Promise<User[]> {
return this.adminService.getUsers();
}
@Delete('users/:id')
deleteUser(@Param('id') id: string): Promise<void> {
return this.adminService.deleteUser(id);
}
@Public() // Override: no auth required
@Get('health')
health() {
return { status: 'ok' };
}
}
```
Reference: [NestJS Guards](https://docs.nestjs.com/guards)

150
.agents/skills/nestjs-best-practices/rules/security-validate-all-input.md

@ -0,0 +1,150 @@
---
title: Validate All Input with DTOs and Pipes
impact: HIGH
impactDescription: First line of defense against attacks
tags: security, validation, dto, pipes
---
## Validate All Input with DTOs and Pipes
Always validate incoming data using class-validator decorators on DTOs and the global ValidationPipe. Never trust user input. Validate all request bodies, query parameters, and route parameters before processing.
**Incorrect (trust raw input without validation):**
```typescript
// Trust raw input without validation
@Controller('users')
export class UsersController {
@Post()
create(@Body() body: any) {
// body could contain anything - SQL injection, XSS, etc.
return this.usersService.create(body);
}
@Get()
findAll(@Query() query: any) {
// query.limit could be "'; DROP TABLE users; --"
return this.usersService.findAll(query.limit);
}
}
// DTOs without validation decorators
export class CreateUserDto {
name: string; // No validation
email: string; // Could be "not-an-email"
age: number; // Could be "abc" or -999
}
```
**Correct (validated DTOs with global ValidationPipe):**
```typescript
// Enable ValidationPipe globally in main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Strip unknown properties
forbidNonWhitelisted: true, // Throw on unknown properties
transform: true, // Auto-transform to DTO types
transformOptions: {
enableImplicitConversion: true,
},
}),
);
await app.listen(3000);
}
// Create well-validated DTOs
import {
IsString,
IsEmail,
IsInt,
Min,
Max,
IsOptional,
MinLength,
MaxLength,
Matches,
IsNotEmpty,
} from 'class-validator';
import { Transform, Type } from 'class-transformer';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
@MinLength(2)
@MaxLength(100)
@Transform(({ value }) => value?.trim())
name: string;
@IsEmail()
@Transform(({ value }) => value?.toLowerCase().trim())
email: string;
@IsInt()
@Min(0)
@Max(150)
age: number;
@IsString()
@MinLength(8)
@MaxLength(100)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: 'Password must contain uppercase, lowercase, and number',
})
password: string;
}
// Query DTO with defaults and transformation
export class FindUsersQueryDto {
@IsOptional()
@IsString()
@MaxLength(100)
search?: string;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit: number = 20;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(0)
offset: number = 0;
}
// Param validation
export class UserIdParamDto {
@IsUUID('4')
id: string;
}
@Controller('users')
export class UsersController {
@Post()
create(@Body() dto: CreateUserDto): Promise<User> {
// dto is guaranteed to be valid
return this.usersService.create(dto);
}
@Get()
findAll(@Query() query: FindUsersQueryDto): Promise<User[]> {
// query.limit is a number, query.search is sanitized
return this.usersService.findAll(query);
}
@Get(':id')
findOne(@Param() params: UserIdParamDto): Promise<User> {
// params.id is a valid UUID
return this.usersService.findById(params.id);
}
}
```
Reference: [NestJS Validation](https://docs.nestjs.com/techniques/validation)

178
.agents/skills/nestjs-best-practices/rules/test-e2e-supertest.md

@ -0,0 +1,178 @@
---
title: Use Supertest for E2E Testing
impact: HIGH
impactDescription: Validates the full request/response cycle
tags: testing, e2e, supertest, integration
---
## Use Supertest for E2E Testing
End-to-end tests use Supertest to make real HTTP requests against your NestJS application. They test the full stack including middleware, guards, pipes, and interceptors. E2E tests catch integration issues that unit tests miss.
**Incorrect (no proper E2E setup or teardown):**
```typescript
// Only unit test controllers
describe('UsersController', () => {
it('should return users', async () => {
const service = { findAll: jest.fn().mockResolvedValue([]) };
const controller = new UsersController(service as any);
const result = await controller.findAll();
expect(result).toEqual([]);
// Doesn't test: routes, guards, pipes, serialization
});
});
// E2E tests without proper setup/teardown
describe('Users API', () => {
it('should create user', async () => {
const app = await NestFactory.create(AppModule);
// No proper initialization
// No cleanup after test
// Hits real database
});
});
```
**Correct (proper E2E setup with Supertest):**
```typescript
// Proper E2E test setup
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('UsersController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
// Apply same config as production
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('/users (POST)', () => {
it('should create a user', () => {
return request(app.getHttpServer())
.post('/users')
.send({ name: 'John', email: 'john@test.com' })
.expect(201)
.expect((res) => {
expect(res.body).toHaveProperty('id');
expect(res.body.name).toBe('John');
expect(res.body.email).toBe('john@test.com');
});
});
it('should return 400 for invalid email', () => {
return request(app.getHttpServer())
.post('/users')
.send({ name: 'John', email: 'invalid-email' })
.expect(400)
.expect((res) => {
expect(res.body.message).toContain('email');
});
});
});
describe('/users/:id (GET)', () => {
it('should return 404 for non-existent user', () => {
return request(app.getHttpServer())
.get('/users/non-existent-id')
.expect(404);
});
});
});
// Testing with authentication
describe('Protected Routes (e2e)', () => {
let app: INestApplication;
let authToken: string;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.init();
// Get auth token
const loginResponse = await request(app.getHttpServer())
.post('/auth/login')
.send({ email: 'test@test.com', password: 'password' });
authToken = loginResponse.body.accessToken;
});
it('should return 401 without token', () => {
return request(app.getHttpServer())
.get('/users/me')
.expect(401);
});
it('should return user profile with valid token', () => {
return request(app.getHttpServer())
.get('/users/me')
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
.expect((res) => {
expect(res.body.email).toBe('test@test.com');
});
});
});
// Database isolation for E2E tests
describe('Orders API (e2e)', () => {
let app: INestApplication;
let dataSource: DataSource;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: '.env.test', // Test database config
}),
AppModule,
],
}).compile();
app = moduleFixture.createNestApplication();
dataSource = moduleFixture.get(DataSource);
await app.init();
});
beforeEach(async () => {
// Clean database between tests
await dataSource.synchronize(true);
});
afterAll(async () => {
await dataSource.destroy();
await app.close();
});
});
```
Reference: [NestJS E2E Testing](https://docs.nestjs.com/fundamentals/testing#end-to-end-testing)

179
.agents/skills/nestjs-best-practices/rules/test-mock-external-services.md

@ -0,0 +1,179 @@
---
title: Mock External Services in Tests
impact: HIGH
impactDescription: Ensures fast, reliable, deterministic tests
tags: testing, mocking, external-services, jest
---
## Mock External Services in Tests
Never call real external services (APIs, databases, message queues) in unit tests. Mock them to ensure tests are fast, deterministic, and don't incur costs. Use realistic mock data and test edge cases like timeouts and errors.
**Incorrect (calling real APIs and databases):**
```typescript
// Call real APIs in tests
describe('PaymentService', () => {
it('should process payment', async () => {
const service = new PaymentService(new StripeClient(realApiKey));
// Hits real Stripe API!
const result = await service.charge('tok_visa', 1000);
// Slow, costs money, flaky
});
});
// Use real database
describe('UsersService', () => {
beforeEach(async () => {
await connection.query('DELETE FROM users'); // Modifies real DB
});
it('should create user', async () => {
const user = await service.create({ email: 'test@test.com' });
// Side effects on shared database
});
});
// Incomplete mocks
const mockHttpService = {
get: jest.fn().mockResolvedValue({ data: {} }),
// Missing error scenarios, missing other methods
};
```
**Correct (mock all external dependencies):**
```typescript
// Mock HTTP service properly
describe('WeatherService', () => {
let service: WeatherService;
let httpService: jest.Mocked<HttpService>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
WeatherService,
{
provide: HttpService,
useValue: {
get: jest.fn(),
post: jest.fn(),
},
},
],
}).compile();
service = module.get(WeatherService);
httpService = module.get(HttpService);
});
it('should return weather data', async () => {
const mockResponse = {
data: { temperature: 72, humidity: 45 },
status: 200,
statusText: 'OK',
headers: {},
config: {},
};
httpService.get.mockReturnValue(of(mockResponse));
const result = await service.getWeather('NYC');
expect(result).toEqual({ temperature: 72, humidity: 45 });
});
it('should handle API timeout', async () => {
httpService.get.mockReturnValue(
throwError(() => new Error('ETIMEDOUT')),
);
await expect(service.getWeather('NYC')).rejects.toThrow('Weather service unavailable');
});
it('should handle rate limiting', async () => {
httpService.get.mockReturnValue(
throwError(() => ({
response: { status: 429, data: { message: 'Rate limited' } },
})),
);
await expect(service.getWeather('NYC')).rejects.toThrow(TooManyRequestsException);
});
});
// Mock repository instead of database
describe('UsersService', () => {
let service: UsersService;
let repo: jest.Mocked<Repository<User>>;
beforeEach(async () => {
const mockRepo = {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
createQueryBuilder: jest.fn(),
};
const module = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepo },
],
}).compile();
service = module.get(UsersService);
repo = module.get(getRepositoryToken(User));
});
it('should find user by id', async () => {
const mockUser = { id: '1', name: 'John', email: 'john@test.com' };
repo.findOne.mockResolvedValue(mockUser);
const result = await service.findById('1');
expect(result).toEqual(mockUser);
expect(repo.findOne).toHaveBeenCalledWith({ where: { id: '1' } });
});
});
// Create mock factory for complex SDKs
function createMockStripe(): jest.Mocked<Stripe> {
return {
paymentIntents: {
create: jest.fn(),
retrieve: jest.fn(),
confirm: jest.fn(),
cancel: jest.fn(),
},
customers: {
create: jest.fn(),
retrieve: jest.fn(),
},
} as any;
}
// Mock time for time-dependent tests
describe('TokenService', () => {
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2024-01-15'));
});
afterEach(() => {
jest.useRealTimers();
});
it('should expire token after 1 hour', async () => {
const token = await service.createToken();
// Fast-forward time
jest.advanceTimersByTime(61 * 60 * 1000);
expect(await service.isValid(token)).toBe(false);
});
});
```
Reference: [Jest Mocking](https://jestjs.io/docs/mock-functions)

153
.agents/skills/nestjs-best-practices/rules/test-use-testing-module.md

@ -0,0 +1,153 @@
---
title: Use Testing Module for Unit Tests
impact: HIGH
impactDescription: Enables proper isolated testing with mocked dependencies
tags: testing, unit-tests, mocking, jest
---
## Use Testing Module for Unit Tests
Use `@nestjs/testing` module to create isolated test environments with mocked dependencies. This ensures your tests run fast, don't depend on external services, and properly test your business logic in isolation.
**Incorrect (manual instantiation bypassing DI):**
```typescript
// Instantiate services manually without DI
describe('UsersService', () => {
it('should create user', async () => {
// Manual instantiation bypasses DI
const repo = new UserRepository(); // Real repo!
const service = new UsersService(repo);
const user = await service.create({ name: 'Test' });
// This hits the real database!
});
});
// Test implementation details
describe('UsersController', () => {
it('should call service', async () => {
const service = { create: jest.fn() };
const controller = new UsersController(service as any);
await controller.create({ name: 'Test' });
expect(service.create).toHaveBeenCalled(); // Tests implementation, not behavior
});
});
```
**Correct (use Test.createTestingModule with mocked dependencies):**
```typescript
// Use Test.createTestingModule for proper DI
import { Test, TestingModule } from '@nestjs/testing';
describe('UsersService', () => {
let service: UsersService;
let repo: jest.Mocked<UserRepository>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: UserRepository,
useValue: {
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
},
},
],
}).compile();
service = module.get<UsersService>(UsersService);
repo = module.get(UserRepository);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('create', () => {
it('should save and return user', async () => {
const dto = { name: 'John', email: 'john@test.com' };
const expectedUser = { id: '1', ...dto };
repo.save.mockResolvedValue(expectedUser);
const result = await service.create(dto);
expect(result).toEqual(expectedUser);
expect(repo.save).toHaveBeenCalledWith(dto);
});
it('should throw on duplicate email', async () => {
repo.findOne.mockResolvedValue({ id: '1', email: 'test@test.com' });
await expect(
service.create({ name: 'Test', email: 'test@test.com' }),
).rejects.toThrow(ConflictException);
});
});
describe('findById', () => {
it('should return user when found', async () => {
const user = { id: '1', name: 'John' };
repo.findOne.mockResolvedValue(user);
const result = await service.findById('1');
expect(result).toEqual(user);
});
it('should throw NotFoundException when not found', async () => {
repo.findOne.mockResolvedValue(null);
await expect(service.findById('999')).rejects.toThrow(NotFoundException);
});
});
});
// Testing guards and interceptors
describe('RolesGuard', () => {
let guard: RolesGuard;
let reflector: Reflector;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [RolesGuard, Reflector],
}).compile();
guard = module.get<RolesGuard>(RolesGuard);
reflector = module.get<Reflector>(Reflector);
});
it('should allow when no roles required', () => {
const context = createMockExecutionContext({ user: { roles: [] } });
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(undefined);
expect(guard.canActivate(context)).toBe(true);
});
it('should allow admin for admin-only route', () => {
const context = createMockExecutionContext({ user: { roles: ['admin'] } });
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);
expect(guard.canActivate(context)).toBe(true);
});
});
function createMockExecutionContext(request: Partial<Request>): ExecutionContext {
return {
switchToHttp: () => ({
getRequest: () => request,
}),
getHandler: () => jest.fn(),
getClass: () => jest.fn(),
} as ExecutionContext;
}
```
Reference: [NestJS Testing](https://docs.nestjs.com/fundamentals/testing)

1
.claude/skills/angular-developer

@ -0,0 +1 @@
../../.agents/skills/angular-developer

1
.claude/skills/nestjs-best-practices

@ -0,0 +1 @@
../../.agents/skills/nestjs-best-practices

3
.config/prisma.ts

@ -6,6 +6,9 @@ import { join } from 'node:path';
expand(config({ quiet: true }));
export default defineConfig({
datasource: {
url: process.env.DIRECT_URL ?? process.env.DATABASE_URL
},
migrations: {
path: join(__dirname, '..', 'prisma', 'migrations'),
seed: `node ${join(__dirname, '..', 'prisma', 'seed.mts')}`

2
.env.dev

@ -12,7 +12,7 @@ POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD>
# VARIOUS
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?connect_timeout=300
JWT_SECRET_KEY=<INSERT_RANDOM_STRING>
# DEVELOPMENT

2
.env.example

@ -12,5 +12,5 @@ POSTGRES_PASSWORD=<INSERT_POSTGRES_PASSWORD>
# VARIOUS
ACCESS_TOKEN_SALT=<INSERT_RANDOM_STRING>
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?connect_timeout=300&sslmode=prefer
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?connect_timeout=300
JWT_SECRET_KEY=<INSERT_RANDOM_STRING>

2
.gitignore

@ -32,6 +32,8 @@ npm-debug.log
.env.prod
.github/instructions/nx.instructions.md
.nx/cache
.nx/polygraph
.nx/self-healing
.nx/workspace-data
/.sass-cache
/connect.lock

1
.npmrc

@ -0,0 +1 @@
min-release-age=7

3
.prettierignore

@ -1,4 +1,7 @@
/.agents/skills/angular-developer
/.agents/skills/nestjs-best-practices
/.nx/cache
/.nx/self-healing
/.nx/workspace-data
/apps/client/src/polyfills.ts
/dist

12
.vscode/launch.json

@ -18,12 +18,20 @@
"autoAttachChildProcesses": true,
"console": "integratedTerminal",
"cwd": "${workspaceFolder}/apps/api",
"envFile": "${workspaceFolder}/.env",
"env": {
"GHOSTFOLIO_ENV_FILE": "${workspaceFolder}/.env"
},
"name": "Debug API",
"outFiles": ["${workspaceFolder}/dist/apps/api/**/*.js"],
"program": "${workspaceFolder}/apps/api/src/main.ts",
"request": "launch",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"runtimeArgs": [
"--nolazy",
"-r",
"ts-node/register",
"-r",
"${workspaceFolder}/tools/load-env.ts"
],
"skipFiles": [
"${workspaceFolder}/node_modules/**/*.js",
"<node_internals>/**/*.js"

182
CHANGELOG.md

@ -5,7 +5,181 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.255.0 - 2026-03-20
## 3.7.0 - 2026-06-02
### Added
- Added support for routing selected requests through the _OpenRouter_ `web_fetch` tool in the `FetchService`
### Changed
- Extended the countries mapping in the data enhancer for asset profile data via _Trackinsight_
- Removed the deprecated attributes (`assetClass`, `assetClassLabel`, `assetSubClass`, `assetSubClassLabel`, `countries`, `currency`, `dataSource`, `holdings`, `name`, `sectors`, `symbol` and `url`) from the holdings of the portfolio details endpoint response
- Upgraded `Nx` from version `22.7.2` to `22.7.5`
### Fixed
- Resolved an issue in the impersonation mode where the values did not match the owner’s currency
- Fixed the environment variable expansion in the `.env` file when debugging via _Visual Studio Code_
## 3.6.0 - 2026-05-28
### Added
- Added `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variable support to outbound HTTP requests
- Added the `FetchService` to centralize outbound HTTP requests
### Changed
- Extracted the floating action buttons (FAB) to a reusable component
- Upgraded `nestjs` from version `11.1.19` to `11.1.21`
- Upgraded `yahoo-finance2` from version `3.14.0` to `3.14.2`
## 3.5.0 - 2026-05-24
### Added
- Configured the `min-release-age` in `.npmrc`
### Changed
- Removed the deprecated attributes (`assetClass`, `countries`, `currency`, `dataSource`, `name`, `sectors`, `symbol` and `url`) from the holdings of the public portfolio endpoint response
- Removed the deprecated `api/v1/order` endpoints
- Upgraded `@keyv/redis` from version `4.4.0` to `5.1.6`
### Fixed
- Fixed a layout regression that caused a double scrollbar on pages without tabs
- Resolved an issue with missing cash positions caused by an incorrect data source
## 3.4.0 - 2026-05-21
### Added
- Added the icon column to the benchmark component
- Added support for the `DIRECT_URL` environment variable to enable direct database connections
### Changed
- Improved the pagination in the activities table of the account detail dialog
- Improved the pagination in the activities table of the holding detail dialog
- Randomized the placeholder in the assistant
- Filtered out sectors with zero weight for ETF and mutual fund assets in the _Yahoo Finance_ data enhancer
- Enabled the _Bull Dashboard_ in the admin control panel without requiring an environment variable (experimental)
- Improved the verification of the _Stripe_ checkout session when creating a subscription
- Relaxed the URL validation in the asset profile DTOs to accept both `HTTP` and `HTTPS` protocols
- Relaxed the URL validation in the platform DTOs to accept both `HTTP` and `HTTPS` protocols
- Extracted the page tabs to a reusable component
- Improved the language localization for German (`de`)
- Improved the language localization for Spanish (`es`)
- Upgraded `bull-board` from version `7.0.0` to `7.1.5`
- Upgraded `Nx` from version `22.7.1` to `22.7.2`
### Fixed
- Resolved an issue with the cash balance calculation of an account for `SELL` activities to ensure fees are correctly subtracted
- Resolved an exception in the portfolio details endpoint when an asset profile is unmatched
## 3.3.0 - 2026-05-14
### Added
- Added `nestjs-best-practices` skills
### Changed
- Deactivated asset profiles automatically on delisting in the _Financial Modeling Prep_ service
- Migrated various components from `NgClass` to class bindings
- Refreshed the cryptocurrencies list
- Improved the language localization for Spanish (`es`)
- Cleaned up the _Webpack Bundle Analyzer_ setup
- Upgraded `@internationalized/number` from version `3.6.5` to `3.6.6`
- Upgraded `@ionic/angular` from version `8.8.1` to `8.8.5`
- Upgraded `@openrouter/ai-sdk-provider` from version `0.7.2` to `2.9.0`
- Upgraded `ai` from version `4.3.16` to `6.0.174`
- Upgraded `bull-board` from version `6.20.3` to `7.0.0`
- Upgraded `countries-and-timezones` from version `3.8.0` to `3.9.0`
- Upgraded `fuse.js` from version `7.1.0` to `7.3.0`
- Upgraded `Nx` from version `22.6.5` to `22.7.1`
- Upgraded `papaparse` from version `5.3.1` to `5.5.3`
- Upgraded `prisma` from version `7.7.0` to `7.8.0`
### Fixed
- Synchronized the native browser elements with the theme to improve the dark mode
- Fixed a visual regression in the bottom navigation bar on mobile
## 3.2.0 - 2026-05-03
### Added
- Added `angular-developer` skills
### Changed
- Harmonized the unit styling in the value component
- Upgraded `stripe` from version `20.4.1` to `21.0.1`
### Fixed
- Resolved a validation error with an empty URL in the asset profile details dialog of the admin control panel
- Resolved an issue where charts and components defaulted to _Roboto_ instead of the preconfigured _Inter_ font family
## 3.1.0 - 2026-04-29
### Added
- Added the _EuroAlternative_ logo to the logo carousel on the landing page
- Integrated a theme switcher into _Storybook_ to support toggling between the light and dark mode
### Changed
- Modernized the layout of the overview tab in the admin control panel
- Improved the styling of the paginator across various table components
- Improved the language localization for German (`de`)
### Fixed
- Optimized the spacing of the logo in the header
- Fixed the _Storybook_ setup by resolving missing `@angular/material` styles
## 3.0.1 - 2026-04-26
### Changed
- Moved the copy-to-clipboard button for the ISIN number in the holding detail dialog from experimental to general availability
- Moved the copy-to-clipboard button for the symbol in the holding detail dialog from experimental to general availability
- Improved the styling of buttons and input fields across various components
- Upgraded `prettier` from version `3.8.2` to `3.8.3`
### Fixed
- Fixed the cash label in the holdings table of the portfolio holdings page
- Fixed the cash label in the holdings table of the public page
## 3.0.0 - 2026-04-23
### Added
- Added a blog post: _Announcing Ghostfolio 3.0_
### Changed
- Migrated from _Material Design_ 2 to _Material Design_ 3
- Moved the total amount, change and performance with currency effects on the analysis page from experimental to general availability
- Refreshed the cryptocurrencies list
- Upgraded `countup.js` from version `2.9.0` to `2.10.0`
- Upgraded `jsonpath` from version `1.2.1` to `1.3.0`
- Upgraded `nestjs` from version `11.1.14` to `11.1.19`
- Upgraded `ngx-markdown` from version `21.1.0` to `21.2.0`
- Upgraded `Nx` from version `22.6.4` to `22.6.5`
- Upgraded `prisma` from version `6.19.0` to `7.7.0`
### Todo
- **Breaking Change**: The `sslmode=prefer` parameter in `DATABASE_URL` is no longer supported. Please update your environment variables (see `.env`) to use `sslmode=require` if _SSL_ is enabled or remove the `sslmode` parameter entirely if _SSL_ is not used.
## 2.255.0 - 2026-04-20
### Changed
@ -22,7 +196,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed the missing value column of the accounts table component on mobile
## 2.254.0 - 2026-03-10
## 2.254.0 - 2026-04-10
### Added
@ -40,7 +214,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the style of the activity type component
## 2.253.0 - 2026-03-06
## 2.253.0 - 2026-04-06
### Added
@ -60,7 +234,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed the allocations by ETF provider chart on the allocations page in the _Presenter View_
- Fixed the allocations by platform chart on the allocations page in the _Presenter View_
## 2.252.0 - 2026-03-02
## 2.252.0 - 2026-04-02
### Added

6
DEVELOPMENT.md

@ -84,6 +84,12 @@ https://ghostfol.io/development/storybook
1. Run `npx npm-check-updates --upgrade --target "minor" --filter "/@angular.*/"`
### NestJS
#### Upgrade (minor versions)
1. Run `npx npm-check-updates --upgrade --target "minor" --filter "/@nestjs.*/"`
### Nx
#### Upgrade

41
README.md

@ -85,26 +85,27 @@ We provide official container images hosted on [Docker Hub](https://hub.docker.c
### Supported Environment Variables
| Name | Type | Default Value | Description |
| --------------------------- | --------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens |
| `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key |
| `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key |
| `DATABASE_URL` | `string` | | The database connection URL, e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer` |
| `ENABLE_FEATURE_AUTH_TOKEN` | `boolean` (optional) | `true` | Enables authentication via security token |
| `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on |
| `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) |
| `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` |
| `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on |
| `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database |
| `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database |
| `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database |
| `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ |
| `REDIS_HOST` | `string` | | The host where _Redis_ is running |
| `REDIS_PASSWORD` | `string` | | The password of _Redis_ |
| `REDIS_PORT` | `number` | | The port where _Redis_ is running |
| `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds |
| `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. |
| Name | Type | Default Value | Description |
| --------------------------- | --------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ACCESS_TOKEN_SALT` | `string` | | A random string used as salt for access tokens |
| `API_KEY_COINGECKO_DEMO` | `string` (optional) |   | The _CoinGecko_ Demo API key |
| `API_KEY_COINGECKO_PRO` | `string` (optional) | | The _CoinGecko_ Pro API key |
| `DATABASE_URL` | `string` | | The database connection URL. If using a connection pooler, use the pooled connection URL here. e.g. `postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}` |
| `DIRECT_URL` | `string` (optional) | | The direct database connection URL used by the _Prisma CLI_ (e.g. for schema migrations) and seeding, bypassing any connection poolers (falls back to `DATABASE_URL`) |
| `ENABLE_FEATURE_AUTH_TOKEN` | `boolean` (optional) | `true` | Enables authentication via security token |
| `HOST` | `string` (optional) | `0.0.0.0` | The host where the Ghostfolio application will run on |
| `JWT_SECRET_KEY` | `string` | | A random string used for _JSON Web Tokens_ (JWT) |
| `LOG_LEVELS` | `string[]` (optional) | | The logging levels for the Ghostfolio application, e.g. `["debug","error","log","warn"]` |
| `PORT` | `number` (optional) | `3333` | The port where the Ghostfolio application will run on |
| `POSTGRES_DB` | `string` | | The name of the _PostgreSQL_ database |
| `POSTGRES_PASSWORD` | `string` | | The password of the _PostgreSQL_ database |
| `POSTGRES_USER` | `string` | | The user of the _PostgreSQL_ database |
| `REDIS_DB` | `number` (optional) | `0` | The database index of _Redis_ |
| `REDIS_HOST` | `string` | | The host where _Redis_ is running |
| `REDIS_PASSWORD` | `string` | | The password of _Redis_ |
| `REDIS_PORT` | `number` | | The port where _Redis_ is running |
| `REQUEST_TIMEOUT` | `number` (optional) | `2000` | The timeout of network requests to data providers in milliseconds |
| `ROOT_URL` | `string` (optional) | `http://0.0.0.0:3333` | The root URL of the Ghostfolio application, used for generating callback URLs and external links. |
#### OpenID Connect OIDC (Experimental)

5
apps/api/src/app/access/access.controller.ts

@ -2,6 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { CreateAccessDto, UpdateAccessDto } from '@ghostfolio/common/dtos';
import { SubscriptionType } from '@ghostfolio/common/enums';
import { Access, AccessSettings } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
@ -83,7 +84,7 @@ export class AccessController {
): Promise<AccessModel> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
this.request.user.subscription.type === SubscriptionType.Basic
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
@ -122,7 +123,7 @@ export class AccessController {
): Promise<AccessModel> {
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic'
this.request.user.subscription.type === SubscriptionType.Basic
) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),

11
apps/api/src/app/account/account.controller.ts

@ -1,5 +1,6 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor';
@ -50,7 +51,8 @@ export class AccountController {
private readonly apiService: ApiService,
private readonly impersonationService: ImpersonationService,
private readonly portfolioService: PortfolioService,
@Inject(REQUEST) private readonly request: RequestWithUser
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly userService: UserService
) {}
@Delete(':id')
@ -137,11 +139,14 @@ export class AccountController {
): Promise<AccountBalancesResponse> {
const impersonationUserId =
await this.impersonationService.validateImpersonationId(impersonationId);
const userId = impersonationUserId || this.request.user.id;
const { settings } = await this.userService.user({ id: userId });
return this.accountBalanceService.getAccountBalances({
userId,
filters: [{ id, type: 'ACCOUNT' }],
userCurrency: this.request.user.settings.settings.baseCurrency,
userId: impersonationUserId || this.request.user.id
userCurrency: settings.settings.baseCurrency
});
}

4
apps/api/src/app/account/account.module.ts

@ -1,5 +1,6 @@
import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module';
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
import { UserModule } from '@ghostfolio/api/app/user/user.module';
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
@ -23,7 +24,8 @@ import { AccountService } from './account.service';
ImpersonationModule,
PortfolioModule,
PrismaModule,
RedactValuesInResponseModule
RedactValuesInResponseModule,
UserModule
],
providers: [AccountService]
})

6
apps/api/src/app/activities/activities.controller.ts

@ -43,11 +43,7 @@ import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { ActivitiesService } from './activities.service';
@Controller([
'activities',
/** @deprecated */
'order'
])
@Controller('activities')
export class ActivitiesController {
public constructor(
private readonly activitiesService: ActivitiesService,

11
apps/api/src/app/activities/activities.service.ts

@ -214,19 +214,18 @@ export class ActivitiesService {
});
if (updateAccountBalance === true) {
let amount = new Big(data.unitPrice)
.mul(data.quantity)
.plus(data.fee)
.toNumber();
let amount = new Big(data.unitPrice).mul(data.quantity);
if (['BUY', 'FEE'].includes(data.type)) {
amount = new Big(amount).mul(-1).toNumber();
amount = amount.mul(-1);
}
amount = amount.minus(data.fee);
await this.accountService.updateAccountBalance({
accountId,
amount,
userId,
amount: amount.toNumber(),
currency: data.SymbolProfile.connectOrCreate.create.currency,
date: data.date as Date
});

352
apps/api/src/app/admin/admin.service.ts

@ -36,7 +36,6 @@ import {
BadRequestException,
HttpException,
Injectable,
Logger,
NotFoundException
} from '@nestjs/common';
import {
@ -44,7 +43,6 @@ import {
AssetSubClass,
DataSource,
Prisma,
PrismaClient,
Property,
SymbolProfile
} from '@prisma/client';
@ -280,186 +278,178 @@ export class AdminService {
const extendedPrismaClient = this.getExtendedPrismaClient();
try {
const symbolProfileResult = await Promise.all([
extendedPrismaClient.symbolProfile.findMany({
skip,
take,
where,
orderBy: [...orderBy, { id: sortDirection }],
select: {
_count: {
select: {
activities: true,
watchedBy: true
}
},
activities: {
orderBy: [{ date: 'asc' }],
select: { date: true },
take: 1
},
assetClass: true,
assetSubClass: true,
comment: true,
countries: true,
currency: true,
dataSource: true,
id: true,
isActive: true,
isUsedByUsersWithSubscription: true,
name: true,
scraperConfiguration: true,
sectors: true,
symbol: true,
SymbolProfileOverrides: true
}
}),
this.prismaService.symbolProfile.count({ where })
]);
const assetProfiles = symbolProfileResult[0];
let count = symbolProfileResult[1];
const lastMarketPrices = await this.prismaService.marketData.findMany({
distinct: ['dataSource', 'symbol'],
orderBy: { date: 'desc' },
const symbolProfileResult = await Promise.all([
extendedPrismaClient.symbolProfile.findMany({
skip,
take,
where,
orderBy: [...orderBy, { id: sortDirection }],
select: {
_count: {
select: {
activities: true,
watchedBy: true
}
},
activities: {
orderBy: [{ date: 'asc' }],
select: { date: true },
take: 1
},
assetClass: true,
assetSubClass: true,
comment: true,
countries: true,
currency: true,
dataSource: true,
marketPrice: true,
symbol: true
id: true,
isActive: true,
isUsedByUsersWithSubscription: true,
name: true,
scraperConfiguration: true,
sectors: true,
symbol: true,
SymbolProfileOverrides: true
}
}),
this.prismaService.symbolProfile.count({ where })
]);
const assetProfiles = symbolProfileResult[0];
let count = symbolProfileResult[1];
const lastMarketPrices = await this.prismaService.marketData.findMany({
distinct: ['dataSource', 'symbol'],
orderBy: { date: 'desc' },
select: {
dataSource: true,
marketPrice: true,
symbol: true
},
where: {
dataSource: {
in: assetProfiles.map(({ dataSource }) => {
return dataSource;
})
},
where: {
dataSource: {
in: assetProfiles.map(({ dataSource }) => {
return dataSource;
})
},
symbol: {
in: assetProfiles.map(({ symbol }) => {
return symbol;
})
}
symbol: {
in: assetProfiles.map(({ symbol }) => {
return symbol;
})
}
});
}
});
const lastMarketPriceMap = new Map<string, number>();
const lastMarketPriceMap = new Map<string, number>();
for (const { dataSource, marketPrice, symbol } of lastMarketPrices) {
lastMarketPriceMap.set(
getAssetProfileIdentifier({ dataSource, symbol }),
marketPrice
);
}
for (const { dataSource, marketPrice, symbol } of lastMarketPrices) {
lastMarketPriceMap.set(
getAssetProfileIdentifier({ dataSource, symbol }),
marketPrice
);
}
let marketData: AdminMarketDataItem[] = await Promise.all(
assetProfiles.map(
async ({
_count,
activities,
assetClass,
assetSubClass,
comment,
countries,
currency,
dataSource,
id,
isActive,
isUsedByUsersWithSubscription,
name,
sectors,
symbol,
SymbolProfileOverrides
}) => {
let countriesCount = countries ? Object.keys(countries).length : 0;
const lastMarketPrice = lastMarketPriceMap.get(
getAssetProfileIdentifier({ dataSource, symbol })
);
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
let sectorsCount = sectors ? Object.keys(sectors).length : 0;
if (SymbolProfileOverrides) {
assetClass = SymbolProfileOverrides.assetClass ?? assetClass;
assetSubClass =
SymbolProfileOverrides.assetSubClass ?? assetSubClass;
if (
(SymbolProfileOverrides.countries as unknown as Prisma.JsonArray)
?.length > 0
) {
countriesCount = (
SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
).length;
}
name = SymbolProfileOverrides.name ?? name;
if (
(SymbolProfileOverrides.sectors as unknown as Sector[])?.length >
0
) {
sectorsCount = (
SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray
).length;
}
}
let marketData: AdminMarketDataItem[] = await Promise.all(
assetProfiles.map(
async ({
_count,
activities,
return {
assetClass,
assetSubClass,
comment,
countries,
countriesCount,
currency,
dataSource,
id,
isActive,
isUsedByUsersWithSubscription,
lastMarketPrice,
marketDataItemCount,
name,
sectors,
sectorsCount,
symbol,
SymbolProfileOverrides
}) => {
let countriesCount = countries ? Object.keys(countries).length : 0;
const lastMarketPrice = lastMarketPriceMap.get(
getAssetProfileIdentifier({ dataSource, symbol })
);
const marketDataItemCount =
marketDataItems.find((marketDataItem) => {
return (
marketDataItem.dataSource === dataSource &&
marketDataItem.symbol === symbol
);
})?._count ?? 0;
let sectorsCount = sectors ? Object.keys(sectors).length : 0;
if (SymbolProfileOverrides) {
assetClass = SymbolProfileOverrides.assetClass ?? assetClass;
assetSubClass =
SymbolProfileOverrides.assetSubClass ?? assetSubClass;
if (
(
SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
)?.length > 0
) {
countriesCount = (
SymbolProfileOverrides.countries as unknown as Prisma.JsonArray
).length;
}
name = SymbolProfileOverrides.name ?? name;
if (
(SymbolProfileOverrides.sectors as unknown as Sector[])
?.length > 0
) {
sectorsCount = (
SymbolProfileOverrides.sectors as unknown as Prisma.JsonArray
).length;
}
}
return {
assetClass,
assetSubClass,
comment,
currency,
countriesCount,
dataSource,
id,
isActive,
lastMarketPrice,
name,
symbol,
marketDataItemCount,
sectorsCount,
activitiesCount: _count.activities,
date: activities?.[0]?.date,
isUsedByUsersWithSubscription:
await isUsedByUsersWithSubscription,
watchedByCount: _count.watchedBy
};
}
)
);
if (presetId) {
if (presetId === 'ETF_WITHOUT_COUNTRIES') {
marketData = marketData.filter(({ countriesCount }) => {
return countriesCount === 0;
});
} else if (presetId === 'ETF_WITHOUT_SECTORS') {
marketData = marketData.filter(({ sectorsCount }) => {
return sectorsCount === 0;
});
activitiesCount: _count.activities,
date: activities?.[0]?.date,
isUsedByUsersWithSubscription: await isUsedByUsersWithSubscription,
watchedByCount: _count.watchedBy
};
}
)
);
count = marketData.length;
if (presetId) {
if (presetId === 'ETF_WITHOUT_COUNTRIES') {
marketData = marketData.filter(({ countriesCount }) => {
return countriesCount === 0;
});
} else if (presetId === 'ETF_WITHOUT_SECTORS') {
marketData = marketData.filter(({ sectorsCount }) => {
return sectorsCount === 0;
});
}
return {
count,
marketData
};
} finally {
await extendedPrismaClient.$disconnect();
Logger.debug('Disconnect extended prisma client', 'AdminService');
count = marketData.length;
}
return {
count,
marketData
};
}
public async getMarketDataBySymbol({
@ -586,8 +576,8 @@ export class AdminService {
}
try {
Promise.all([
await this.symbolProfileService.updateAssetProfileIdentifier(
await Promise.all([
this.symbolProfileService.updateAssetProfileIdentifier(
{
dataSource,
symbol
@ -597,7 +587,7 @@ export class AdminService {
symbol: newSymbol as string
}
),
await this.marketDataService.updateAssetProfileIdentifier(
this.marketDataService.updateAssetProfileIdentifier(
{
dataSource,
symbol
@ -609,12 +599,15 @@ export class AdminService {
)
]);
return this.symbolProfileService.getSymbolProfiles([
{
dataSource: DataSource[newDataSource.toString()],
symbol: newSymbol as string
}
])?.[0];
const [updatedAssetProfile] =
await this.symbolProfileService.getSymbolProfiles([
{
dataSource: DataSource[newDataSource.toString()],
symbol: newSymbol as string
}
]);
return updatedAssetProfile;
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
@ -660,12 +653,15 @@ export class AdminService {
updatedSymbolProfile
);
return this.symbolProfileService.getSymbolProfiles([
{
dataSource: dataSource as DataSource,
symbol: symbol as string
}
])?.[0];
const [updatedAssetProfile] =
await this.symbolProfileService.getSymbolProfiles([
{
dataSource: dataSource as DataSource,
symbol: symbol as string
}
]);
return updatedAssetProfile;
}
}
@ -704,8 +700,6 @@ export class AdminService {
}
private getExtendedPrismaClient() {
Logger.debug('Connect extended prisma client', 'AdminService');
const symbolProfileExtension = Prisma.defineExtension((client) => {
return client.$extends({
result: {
@ -746,7 +740,7 @@ export class AdminService {
});
});
return new PrismaClient().$extends(symbolProfileExtension);
return this.prismaService.$extends(symbolProfileExtension);
}
private async getMarketDataForCurrencies(): Promise<AdminMarketData> {

42
apps/api/src/app/app.module.ts

@ -74,29 +74,25 @@ import { UserModule } from './user/user.module';
AuthDeviceModule,
AuthModule,
BenchmarksModule,
...(process.env.ENABLE_FEATURE_BULL_BOARD === 'true'
? [
BullBoardModule.forRoot({
adapter: ExpressAdapter,
boardOptions: {
uiConfig: {
boardLogo: {
height: 0,
path: '',
width: 0
},
boardTitle: 'Job Queues',
favIcon: {
alternative: '/assets/favicon-32x32.png',
default: '/assets/favicon-32x32.png'
}
}
},
middleware: BullBoardAuthMiddleware,
route: BULL_BOARD_ROUTE
})
]
: []),
BullBoardModule.forRoot({
adapter: ExpressAdapter,
boardOptions: {
uiConfig: {
boardLogo: {
height: 0,
path: '',
width: 0
},
boardTitle: 'Job Queues',
favIcon: {
alternative: '/assets/favicon-32x32.png',
default: '/assets/favicon-32x32.png'
}
}
},
middleware: BullBoardAuthMiddleware,
route: BULL_BOARD_ROUTE
}),
BullModule.forRoot({
redis: {
db: parseInt(process.env.REDIS_DB ?? '0', 10),

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save