nestjs-boost 0.0.0-stage → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/NOTICE.md +6 -0
- package/README.md +168 -2
- package/bin/nestjs-boost.mjs +148 -0
- package/guidelines/core.md +64 -0
- package/lib/agents.mjs +137 -0
- package/lib/installer.mjs +450 -0
- package/lib/prompt.mjs +131 -0
- package/lib/setup.mjs +60 -0
- package/manifest.json +676 -0
- package/package.json +67 -6
- package/scripts/postinstall.mjs +49 -0
- package/skills/nestjs-ai/SKILL.md +145 -0
- package/skills/nestjs-ai/references/ai-reference.md +143 -0
- package/skills/nestjs-architecture/SKILL.md +83 -0
- package/skills/nestjs-architecture/references/dynamic-modules.md +92 -0
- package/skills/nestjs-architecture/references/scopes-lifecycle.md +98 -0
- package/skills/nestjs-architecture/references/troubleshooting.md +77 -0
- package/skills/nestjs-authentication/SKILL.md +115 -0
- package/skills/nestjs-authentication/references/flows.md +180 -0
- package/skills/nestjs-authorization/SKILL.md +104 -0
- package/skills/nestjs-caching/SKILL.md +109 -0
- package/skills/nestjs-cli-tooling/SKILL.md +94 -0
- package/skills/nestjs-cli-tooling/references/nest-cli-json.md +91 -0
- package/skills/nestjs-configuration/SKILL.md +109 -0
- package/skills/nestjs-cqrs/SKILL.md +124 -0
- package/skills/nestjs-database-drizzle/SKILL.md +117 -0
- package/skills/nestjs-database-mikro-orm/SKILL.md +109 -0
- package/skills/nestjs-database-mongoose/SKILL.md +124 -0
- package/skills/nestjs-database-prisma/SKILL.md +126 -0
- package/skills/nestjs-database-sequelize/SKILL.md +88 -0
- package/skills/nestjs-database-typeorm/SKILL.md +110 -0
- package/skills/nestjs-deployment/SKILL.md +133 -0
- package/skills/nestjs-deployment/references/serverless-and-systemd.md +133 -0
- package/skills/nestjs-events/SKILL.md +78 -0
- package/skills/nestjs-file-storage/SKILL.md +111 -0
- package/skills/nestjs-file-storage/references/storage-reference.md +82 -0
- package/skills/nestjs-graphql/SKILL.md +119 -0
- package/skills/nestjs-graphql/references/federation.md +110 -0
- package/skills/nestjs-graphql/references/schema-features.md +133 -0
- package/skills/nestjs-health-checks/SKILL.md +114 -0
- package/skills/nestjs-http-client/SKILL.md +118 -0
- package/skills/nestjs-http-client/references/http-client-reference.md +116 -0
- package/skills/nestjs-http-platform/SKILL.md +104 -0
- package/skills/nestjs-http-platform/references/adapters.md +88 -0
- package/skills/nestjs-http-web-features/SKILL.md +113 -0
- package/skills/nestjs-http-web-features/references/uploads-streaming.md +108 -0
- package/skills/nestjs-i18n/SKILL.md +113 -0
- package/skills/nestjs-idempotency/SKILL.md +112 -0
- package/skills/nestjs-locks/SKILL.md +108 -0
- package/skills/nestjs-logging/SKILL.md +123 -0
- package/skills/nestjs-mail/SKILL.md +125 -0
- package/skills/nestjs-mail/references/mail-reference.md +90 -0
- package/skills/nestjs-mcp-server/SKILL.md +152 -0
- package/skills/nestjs-mcp-server/references/mcp-reference.md +146 -0
- package/skills/nestjs-microservices/SKILL.md +108 -0
- package/skills/nestjs-microservices/references/transports.md +117 -0
- package/skills/nestjs-observability/SKILL.md +108 -0
- package/skills/nestjs-observability/references/apm-and-otel.md +106 -0
- package/skills/nestjs-openapi/SKILL.md +90 -0
- package/skills/nestjs-openapi/references/swagger-reference.md +180 -0
- package/skills/nestjs-outbox/SKILL.md +119 -0
- package/skills/nestjs-outbox/references/outbox-reference.md +74 -0
- package/skills/nestjs-passport-auth/SKILL.md +125 -0
- package/skills/nestjs-queues/SKILL.md +118 -0
- package/skills/nestjs-queues/references/bull-legacy.md +75 -0
- package/skills/nestjs-request-pipeline/SKILL.md +146 -0
- package/skills/nestjs-request-pipeline/references/filters-interceptors.md +114 -0
- package/skills/nestjs-request-pipeline/references/pipes-decorators.md +80 -0
- package/skills/nestjs-resilience/SKILL.md +116 -0
- package/skills/nestjs-resilience/references/resilience-reference.md +74 -0
- package/skills/nestjs-scheduling/SKILL.md +100 -0
- package/skills/nestjs-security-hardening/SKILL.md +99 -0
- package/skills/nestjs-security-hardening/references/throttler-advanced.md +78 -0
- package/skills/nestjs-standalone-apps/SKILL.md +78 -0
- package/skills/nestjs-testing/SKILL.md +118 -0
- package/skills/nestjs-testing/references/advanced-testing.md +62 -0
- package/skills/nestjs-upgrading/SKILL.md +82 -0
- package/skills/nestjs-upgrading/references/esm-switch.md +114 -0
- package/skills/nestjs-validation-serialization/SKILL.md +124 -0
- package/skills/nestjs-validation-serialization/references/validation-options.md +84 -0
- package/skills/nestjs-webhooks/SKILL.md +116 -0
- package/skills/nestjs-webhooks/references/webhooks-reference.md +132 -0
- package/skills/nestjs-websockets/SKILL.md +108 -0
- package/skills/nestjs-workflows/SKILL.md +123 -0
- package/skills/nestjs-workflows/references/workflows-reference.md +85 -0
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "nestjs-boost",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "nestjs-boost",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Agent Skills and project guidelines for AI-assisted NestJS 12 development. Interactive installer for Claude Code, GitHub Copilot, Cursor, Codex, Gemini CLI, Windsurf, Cline, Roo Code, Kiro, Junie and OpenCode.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nestjs",
|
|
7
|
+
"agent-skills",
|
|
8
|
+
"skills",
|
|
9
|
+
"ai",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"codex",
|
|
12
|
+
"cursor",
|
|
13
|
+
"copilot",
|
|
14
|
+
"agents-md",
|
|
15
|
+
"mcp",
|
|
16
|
+
"gemini-cli",
|
|
17
|
+
"windsurf",
|
|
18
|
+
"cline",
|
|
19
|
+
"roo-code",
|
|
20
|
+
"kiro",
|
|
21
|
+
"junie",
|
|
22
|
+
"opencode",
|
|
23
|
+
"skill-md"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "Ruwan Bandara",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/ruwanb/nestjs-boost.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/ruwanb/nestjs-boost#readme",
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/ruwanb/nestjs-boost/issues"
|
|
34
|
+
},
|
|
35
|
+
"type": "module",
|
|
36
|
+
"bin": {
|
|
37
|
+
"nestjs-boost": "bin/nestjs-boost.mjs"
|
|
38
|
+
},
|
|
39
|
+
"exports": {
|
|
40
|
+
".": "./lib/installer.mjs",
|
|
41
|
+
"./manifest.json": "./manifest.json",
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"bin/",
|
|
46
|
+
"lib/",
|
|
47
|
+
"scripts/postinstall.mjs",
|
|
48
|
+
"guidelines/",
|
|
49
|
+
"skills/",
|
|
50
|
+
"manifest.json",
|
|
51
|
+
"CHANGELOG.md",
|
|
52
|
+
"NOTICE.md"
|
|
53
|
+
],
|
|
54
|
+
"scripts": {
|
|
55
|
+
"postinstall": "node scripts/postinstall.mjs",
|
|
56
|
+
"validate": "node scripts/validate.mjs .",
|
|
57
|
+
"test": "node scripts/validate.mjs . && node --test test/installer.test.mjs test/agents.test.mjs",
|
|
58
|
+
"prepublishOnly": "npm test",
|
|
59
|
+
"version": "node -e \"const fs=require('fs');const v=JSON.parse(fs.readFileSync('package.json','utf8')).version;const m=JSON.parse(fs.readFileSync('manifest.json','utf8'));m.version=v;fs.writeFileSync('manifest.json',JSON.stringify(m,null,2)+'\\n')\" && git add manifest.json"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=20.19"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Runs on `npm install` / `npm ci` (and yarn, or pnpm/bun when allowed). It keeps skills in sync in
|
|
2
|
+
// a project that has opted in. It must never fail or slow down an install, so every problem is
|
|
3
|
+
// reported as a one-line note and the script always exits 0.
|
|
4
|
+
//
|
|
5
|
+
// Opt-in: the hook never adds agent instructions to a project on its own. It syncs only when the
|
|
6
|
+
// project already has `.ai/nestjs-boost.lock.json` (written by running `npx nestjs-boost install`
|
|
7
|
+
// once) or a "nestjs-boost" key in package.json. Otherwise it prints how to opt in.
|
|
8
|
+
//
|
|
9
|
+
// Skipped when:
|
|
10
|
+
// - NESTJS_BOOST_SKIP=1, or CI is set (unless NESTJS_BOOST_CI=1)
|
|
11
|
+
// - installing globally, through npx, or inside this package's own repository
|
|
12
|
+
// - nestjs-boost isn't a direct dependency of the project (it was pulled in by another package)
|
|
13
|
+
// - the project root can't be found or isn't a NestJS project
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join, resolve } from 'node:path';
|
|
16
|
+
|
|
17
|
+
const env = process.env;
|
|
18
|
+
const note = (msg) => console.log(`nestjs-boost: ${msg}`);
|
|
19
|
+
|
|
20
|
+
function projectRoot() {
|
|
21
|
+
// npm sets npm_config_local_prefix to the project root; INIT_CWD is where the command ran (npm, yarn, pnpm).
|
|
22
|
+
for (const dir of [env.npm_config_local_prefix, env.INIT_CWD]) {
|
|
23
|
+
if (dir && existsSync(join(dir, 'package.json'))) return resolve(dir);
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function run() {
|
|
29
|
+
if (env.NESTJS_BOOST_SKIP === '1' || env.NESTJS_BOOST_SKIP === 'true') return;
|
|
30
|
+
if (env.CI && env.NESTJS_BOOST_CI !== '1') return;
|
|
31
|
+
if (env.npm_config_global === 'true') return note('global install: run `nestjs-boost install` inside a NestJS project.');
|
|
32
|
+
if (env.npm_command === 'exec') return; // `npx nestjs-boost ...`: the command itself decides what to write
|
|
33
|
+
const root = projectRoot();
|
|
34
|
+
if (!root) return;
|
|
35
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
36
|
+
if (pkg.name === 'nestjs-boost') return; // developing this package
|
|
37
|
+
if (!{ ...pkg.devDependencies, ...pkg.dependencies }['nestjs-boost']) return; // transitive dependency
|
|
38
|
+
|
|
39
|
+
const { LOCK, isNestProject, sync } = await import('../lib/installer.mjs');
|
|
40
|
+
if (!isNestProject(root)) return note('no @nestjs/core in package.json; skills not installed.');
|
|
41
|
+
if (!existsSync(join(root, LOCK)) && pkg['nestjs-boost'] === undefined) {
|
|
42
|
+
return note('run `npx nestjs-boost install` once to add the skills; later installs keep them in sync.');
|
|
43
|
+
}
|
|
44
|
+
const { skills, changes, warnings } = sync(root, {}, { force: false });
|
|
45
|
+
if (changes.length) note(`${skills.length} skills synced (${changes.length} change${changes.length === 1 ? '' : 's'}). Run \`npx nestjs-boost list\` for details.`);
|
|
46
|
+
for (const w of warnings) note(w);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
run().catch((err) => note(`skipped (${err.message}). Run \`npx nestjs-boost install\` to retry.`));
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs-ai
|
|
3
|
+
description: Brings language models into NestJS 12 apps with @nestjs/ai on the AI SDK — AiModule.forRoot/forRootAsync with named models (Anthropic, OpenAI, Google and other @ai-sdk providers), AiService generateText, generateObject with Zod schemas and streamText to the browser (pipeUIMessageStreamToResponse, useChat), per-call/model/module timeouts, retries and fallback models on @nestjs/resilience, pricing, onUsage, budgets, AiModelError/AiOutputError/AiBudgetExceededError, @AgentFunction tools with caller, scopes, guards and AiExecutionContext, the agent loop (maxSteps, conversations), durable agent runs with human approval on @nestjs/workflows, @InjectModel and AiTools for raw AI SDK calls, and FakeModel testing. Use when adding LLM features (classification, extraction, drafting, chat assistants, agents with tools), making model calls reliable and cost-bounded, or testing code that calls models without a provider. Not for exposing the app to external assistants (nestjs-mcp-server).
|
|
4
|
+
compatibility: NestJS 12.x; @nestjs/ai, @nestjs/resilience, ai (AI SDK), an @ai-sdk/* provider package, zod 4.2+; @nestjs/workflows for durable runs
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.1.0"
|
|
7
|
+
nestjs-major: "12"
|
|
8
|
+
docs-crawled: "2026-11-05"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# NestJS AI (@nestjs/ai)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @nestjs/ai @nestjs/resilience ai @ai-sdk/anthropic zod # any @ai-sdk/* provider works the same way
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The schema rules are the same as for `@nestjs/mcp`: Zod 4.2+, ArkType, or Valibot via `toStandardJsonSchema()`.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
AiModule.forRootAsync({
|
|
21
|
+
isGlobal: true, // otherwise AiService is injectable only where AiModule is imported
|
|
22
|
+
imports: [ConfigModule], inject: [ConfigService],
|
|
23
|
+
useFactory: (config: ConfigService) => {
|
|
24
|
+
const anthropic = createAnthropic({ apiKey: config.getOrThrow('ANTHROPIC_API_KEY') });
|
|
25
|
+
return {
|
|
26
|
+
models: {
|
|
27
|
+
default: { model: anthropic('claude-opus-5-5'), pricing: { input: 4, output: 20, cachedInput: 0.2 }, fallback: 'backup' },
|
|
28
|
+
fast: { model: anthropic('claude-haiku-5-5'), timeout: '15s', fallback: 'default' },
|
|
29
|
+
backup: openai('gpt-5'),
|
|
30
|
+
},
|
|
31
|
+
defaults: { timeout: '30s', retry: { attempts: 3 }, maxOutputTokens: 1_000 },
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Calls that name no model use `default`. In the module, a string such as `'openai/gpt-5'` is an AI Gateway model id; in a call, a string always names one of the module's models. Without a factory, the provider reads its key from the environment (`ANTHROPIC_API_KEY`). A setting resolves from the call first, then the model definition, then `defaults`.
|
|
38
|
+
|
|
39
|
+
## Calls
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
@Injectable()
|
|
43
|
+
export class TriageService {
|
|
44
|
+
constructor(private readonly ai: AiService) {}
|
|
45
|
+
|
|
46
|
+
async draft(email: string) {
|
|
47
|
+
const { text } = await this.ai.generateText({
|
|
48
|
+
model: 'fast', label: 'draft-reply', // label names the call in usage and policy events
|
|
49
|
+
instructions: 'Draft a short, friendly reply. Sign it "The Cat Store".',
|
|
50
|
+
prompt: email, // or messages: ModelMessage[] (not both)
|
|
51
|
+
maxOutputTokens: 400,
|
|
52
|
+
});
|
|
53
|
+
return text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async triage(email: string) {
|
|
57
|
+
const { object } = await this.ai.generateObject({
|
|
58
|
+
model: 'fast', label: 'triage',
|
|
59
|
+
schema: z.object({
|
|
60
|
+
category: z.enum(['shipping', 'billing', 'product', 'other']).describe('What the email is about'),
|
|
61
|
+
orderId: z.string().nullable().describe('The order id it mentions, such as ord_123, or null'),
|
|
62
|
+
urgent: z.boolean().describe('Whether the customer needs an answer today'),
|
|
63
|
+
}),
|
|
64
|
+
prompt: email,
|
|
65
|
+
});
|
|
66
|
+
return object; // typed and validated; schema transforms run on the answer
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- Results include `text`, `finishReason` (`stop`, `length`, or `tool-calls` when `maxSteps` ran out), `usage`, `cost`, `steps`, `toolCalls`, `toolResults`, `responseMessages` and `model`.
|
|
72
|
+
- `streamText()` returns a stream immediately. In a route, call `await answer.pipeUIMessageStreamToResponse(res)` with `@Res()`, then use `useChat()` from `@ai-sdk/react` in the browser. Use `pipeTextStreamToResponse()` for plain text. Details: reference.
|
|
73
|
+
- `.describe()` texts reach the model: say what each field means and give an example value.
|
|
74
|
+
|
|
75
|
+
## Reliability and cost
|
|
76
|
+
|
|
77
|
+
| Setting | Default | Notes |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| `timeout` | `'60s'` per attempt | Until the full answer (until the first chunk for streams). Tool time doesn't count. `false` = no limit |
|
|
80
|
+
| `retry` | 3 attempts, backoff from `'1s'` doubling to `'30s'` with jitter | Retries 408/409/429/5xx, connection failures and timeouts; honours `retry-after` |
|
|
81
|
+
| `fallback` | the model's | Models tried in order after retries run out. Only the call's own model's `fallback` is followed, so list every model. Narrow with `fallbackIf` (skip on 400). Each fallback is logged as a warning |
|
|
82
|
+
| `resilience` | | A policy or a `ResilienceService` preset name (circuit breaker, bulkhead) in place of `retry`+`timeout` |
|
|
83
|
+
| `budget` | | `{ maxTokens, maxCost }` per call, checked before each model call → `AiBudgetExceededError` |
|
|
84
|
+
| `maxSteps` | 10 | Model calls per call while tools are requested |
|
|
85
|
+
| `onUsage` | | On the module and/or the call (both run). `event.cost` needs `pricing`. A listener that throws is logged and never fails the call |
|
|
86
|
+
|
|
87
|
+
Errors (all extend `AiError`):
|
|
88
|
+
- `AiModelError`: every attempt on every model failed. It has `attempts` and `cause`; map it to 503 in a filter.
|
|
89
|
+
- `AiOutputError`: the answer isn't valid for the schema, or tools were still being called at `maxSteps`. It carries `text` and is **never retried or sent to a fallback**.
|
|
90
|
+
- `AiBudgetExceededError`.
|
|
91
|
+
- `AiConfigurationError`: an unknown model, tool or option, raised before anything is sent.
|
|
92
|
+
- `AiToolError`.
|
|
93
|
+
|
|
94
|
+
Once a chunk has streamed or tools have started, the outcome is committed: no retry or fallback can replay it. An `abortSignal` (for example `AbortSignal.timeout(120_000)`) cancels the call immediately, without retries.
|
|
95
|
+
|
|
96
|
+
`@InjectModel('fast') model: LanguageModel` gives the raw provider model for direct AI SDK calls. These calls get **none** of the timeouts, fallbacks, budgets or usage events. Pass agent functions to them with `this.tools.select(['find-order'], { caller: { request } })` from `AiTools`.
|
|
97
|
+
|
|
98
|
+
## Agent functions (tools)
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
@Injectable() // singleton: agent functions on request-scoped providers fail at startup
|
|
102
|
+
export class AssistantTools {
|
|
103
|
+
constructor(private readonly orders: OrdersService) {}
|
|
104
|
+
|
|
105
|
+
@AgentFunction({
|
|
106
|
+
name: 'change-address', // 1–64 chars [A-Za-z0-9_-]; defaults to the method name
|
|
107
|
+
description: 'Change the delivery address of an order that has not shipped yet',
|
|
108
|
+
inputSchema: z.object({ orderId: z.string().describe('The order id, such as ord_123'), address: z.string().min(10) }),
|
|
109
|
+
scopes: ['orders:write'], // checked against caller.scopes before guards
|
|
110
|
+
})
|
|
111
|
+
@UseGuards(OrderOwnerGuard)
|
|
112
|
+
changeAddress({ orderId, address }: { orderId: string; address: string }, ctx: AgentFunctionContext) {
|
|
113
|
+
return this.orders.changeAddress(orderId, address, { signal: ctx.signal });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// in a route: tools by name or provider class; caller = who the agent acts for
|
|
118
|
+
await this.ai.generateText({ prompt: body.question, tools: ['find-order', AssistantTools], caller: { request }, maxSteps: 5 });
|
|
119
|
+
// in a job: caller: { user: { id: 'refund-bot' }, scopes: ['orders:refund'] }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- **Order of a call:** schema validation → `scopes` → guards (global first) → interceptors → pipes → method.
|
|
123
|
+
- **Context:** the type is `'ai'`. Read it with `AiExecutionContext.create(context)`, which provides `getInput()`, `getContext()`, `getName()` and `getRequest()`. HTTP-only global enhancers must check `context.getType()`.
|
|
124
|
+
- **`AgentFunctionContext`:** `name`, `request`, `user`, `scopes`, `toolCallId`, `idempotencyKey` (durable runs), `messages`, `signal` and `reportProgress()`.
|
|
125
|
+
- **Errors:** thrown errors are fed back to the model. `HttpException` messages pass through; other errors become `Internal server error`.
|
|
126
|
+
- **Results:** return values are serialized as JSON for the model.
|
|
127
|
+
- **Calls with no `caller`** run with no request, no user and no scopes.
|
|
128
|
+
- **MCP:** one method can carry both `@Tool()` (`@nestjs/mcp`) and `@AgentFunction()`. Branch on `'mcp'` vs `'ai'` in guards.
|
|
129
|
+
- **With `@nestjs/authentication` ≥ 0.1.4 and `@nestjs/authorization` ≥ 0.1.2:**
|
|
130
|
+
- `@CurrentUser()` and `@Can()` work in agent functions.
|
|
131
|
+
- Providers that jobs call need `@Public()`.
|
|
132
|
+
|
|
133
|
+
## Workflow
|
|
134
|
+
|
|
135
|
+
- [ ] 1. Install the provider package; register named models (`default` plus cheaper `fast`, with a fallback) from config; set `defaults`.
|
|
136
|
+
- [ ] 2. Use `generateObject` for classification and extraction (schemas with `.describe()`), `generateText`/`streamText` for prose and chat.
|
|
137
|
+
- [ ] 3. Set timeouts, retries and fallbacks; add `pricing` + `onUsage` metering and `budget` on agentic calls; map `AiModelError` to 503.
|
|
138
|
+
- [ ] 4. Agent functions: model-oriented descriptions, `caller`, scopes and guards, `signal` passthrough, idempotent side effects.
|
|
139
|
+
- [ ] 5. Tests with `FakeModel` / `overrideModels()` / `callTool()`, never a real provider. Build, lint, run tests, start the app.
|
|
140
|
+
|
|
141
|
+
## Reference files
|
|
142
|
+
|
|
143
|
+
- `references/ai-reference.md`: module options, the agent loop and conversations, streaming (errors, keep-alive, progress chunks, other stream readers), durable agent runs with approvals (`AiWorkflowsModule`, `AiAgents.run` options, `idempotencyKey`), resilience presets and diagnostics channels, Observe integration, and testing (FakeModel answers, `providerError`, `overrideModels`, `callTool`, stream and durable-run tests). Read when building chat or agents, durable flows, or tests.
|
|
144
|
+
|
|
145
|
+
Sources: [Overview](https://docs.nestjs.com/ai/overview) · [Models](https://docs.nestjs.com/ai/models) · [Tools](https://docs.nestjs.com/ai/tools) · [Agents](https://docs.nestjs.com/ai/agents) · [Testing](https://docs.nestjs.com/ai/testing)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# @nestjs/ai reference
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
- Module options and setting precedence
|
|
5
|
+
- Resilience presets and events
|
|
6
|
+
- The agent loop and conversations
|
|
7
|
+
- Streaming to the browser
|
|
8
|
+
- Durable agent runs
|
|
9
|
+
- Testing
|
|
10
|
+
|
|
11
|
+
## Module options and setting precedence
|
|
12
|
+
|
|
13
|
+
| Option | Default | Notes |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| `models` | | Required. Each value is a `LanguageModel` or `{ model, fallback, pricing, timeout, retry, resilience, maxOutputTokens, … }` |
|
|
16
|
+
| `defaults.timeout` / `retry` / `resilience` | `'60s'` / 3 attempts from `'1s'` / | `retry` = `RetryPolicy` options (`attempts`, `backoff` {`delay`, `factor`, `maxDelay`, `jitter`}, `retryIf`); `false` disables |
|
|
17
|
+
| `defaults.maxSteps` | `10` | Also caps durable runs (whose own default is 25) |
|
|
18
|
+
| `defaults.budget` | | `maxTokens`, `maxCost` |
|
|
19
|
+
| `defaults.maxOutputTokens`, `temperature`, `providerOptions`, `headers` | | Sent to the model |
|
|
20
|
+
| `onUsage` | | Usage event: `label`, `model`, `step`, `attempt`, `fallback`, `stream`, `usage`, `cost`, `durationMs`, `finishReason` |
|
|
21
|
+
| `include` | all modules | Where agent functions are searched |
|
|
22
|
+
| `isGlobal` | `false` | Top level of `forRootAsync()` |
|
|
23
|
+
| `modelNames` | | Extra names `@InjectModel()` may inject (lazy-loaded classes). Top level of `forRootAsync()` |
|
|
24
|
+
|
|
25
|
+
| Setting | Where it can be set |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `timeout`, `retry`, `resilience`, generation settings | call, model, `defaults` |
|
|
28
|
+
| `maxSteps`, `budget` | call, `defaults` |
|
|
29
|
+
| `fallback` | call, model |
|
|
30
|
+
| `pricing` | model (per million tokens, in your unit) |
|
|
31
|
+
| `topP`, `topK`, `seed`, `stopSequences`, penalties, `toolChoice`, `fallbackIf`, `abortSignal` | call only |
|
|
32
|
+
|
|
33
|
+
`forRootAsync()` accepts `useFactory`, `useClass` (`AiOptionsFactory.createAiOptions()`) or `useExisting`. `@InjectModel('typo')` fails at startup and names the class that asked.
|
|
34
|
+
|
|
35
|
+
## Resilience presets and events
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
ResilienceModule.forRoot({ presets: { anthropic: { retry: { attempts: 3 }, timeout: '30s', circuitBreaker: { name: 'anthropic', failureRateThreshold: 50 } } } }),
|
|
39
|
+
AiModule.forRoot({ models: { default: { model: anthropic('claude-opus-5-5'), resilience: 'anthropic', fallback: 'backup' }, backup: openai('gpt-5') } }),
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- A preset replaces the model's `retry` and `timeout`. A preset without a timeout means **no timeout**.
|
|
43
|
+
- `CircuitOpenError` or `BulkheadFullError` reaches the caller as-is when no fallback takes over.
|
|
44
|
+
- Events are published on the `nestjs:resilience:retry|timeout|fallback` diagnostics channels, named `ai:<model> retry`, with `source` = the call's label: `subscribe('nestjs:resilience:retry', …)`.
|
|
45
|
+
- `@nestjs/observe` ≥ 0.3.20 records the model and tokens on the outbound span. For AI Gateway ids, add `ai-gateway.vercel.sh` to `outgoing.llm.hosts`.
|
|
46
|
+
|
|
47
|
+
## The agent loop and conversations
|
|
48
|
+
|
|
49
|
+
- Each model call is a **step** (`result.steps[i]`: `index`, `text`, `toolCalls`, `toolResults` (`output` or `error`), `finishReason`, `usage`, `cost`, `model`, `attempts`).
|
|
50
|
+
- If `maxSteps` runs out: `generateText` resolves with `finishReason: 'tool-calls'`, and `generateObject` rejects with `AiOutputError`.
|
|
51
|
+
- `toolChoice`: `'auto'` (default), `'none'`, `'required'`, or `{ type: 'tool', toolName }`. Not every model supports the last two.
|
|
52
|
+
- Conversations: send `messages: ModelMessage[]` and append `responseMessages` after each call. Store conversations in a database, not in memory. `instructions` aren't part of the conversation and are sent on every call.
|
|
53
|
+
- A failed agent function call is a tool result the model reads, not a failed call.
|
|
54
|
+
|
|
55
|
+
## Streaming to the browser
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
@Post('chat')
|
|
59
|
+
async chat(@Body() { messages }: { messages: UIMessage[] }, @Req() request: Request, @Res() response: Response) {
|
|
60
|
+
const answer = this.ai.streamText({
|
|
61
|
+
label: 'assistant', instructions: '…',
|
|
62
|
+
messages: await convertToModelMessages(messages), // from 'ai'
|
|
63
|
+
tools: ['find-order', 'change-address'], caller: { request },
|
|
64
|
+
});
|
|
65
|
+
await answer.pipeUIMessageStreamToResponse(response, {
|
|
66
|
+
keepAlive: '10s', // default 15 s comment lines; false disables
|
|
67
|
+
errorText: (e) => (e instanceof AiModelError ? 'The assistant is unavailable. Try again in a minute.' : 'Something went wrong.'),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// browser: useChat({ transport: new DefaultChatTransport({ api: '/assistant/chat' }), onData: (p) => p.type === 'data-tool-progress' && … })
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- Works on Express, Fastify (`FastifyReply`, keeps pre-set headers) and the Node adapter.
|
|
74
|
+
- **Errors:** a failure before the first chunk rejects normally, so route filters answer it, and retries/fallbacks still apply. A failure after it ends the stream with `An error occurred.`; customise this with `errorText`.
|
|
75
|
+
- **Disconnects:** a browser disconnect aborts the model call and the tools' `signal`.
|
|
76
|
+
- **Headers:** `text/event-stream` with `x-accel-buffering: no`. Add your own with `status` and `headers`.
|
|
77
|
+
- **Progress:** `ctx.reportProgress()` in an agent function sends a transient `data-tool-progress` chunk `{ toolCallId, toolName, progress, total, message }`.
|
|
78
|
+
- **Other readers:**
|
|
79
|
+
- `pipeTextStreamToResponse(res)` (text/plain)
|
|
80
|
+
- `textStream`, `toUIMessageStream()`, `toUIMessageStreamResponse()`
|
|
81
|
+
- promises: `text`, `usage`, `steps`, `finishReason`, `responseMessages`, `result`
|
|
82
|
+
|
|
83
|
+
## Durable agent runs
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm i @nestjs/workflows
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
@Module({ imports: [AiWorkflowsModule], controllers: [ApprovalsController], providers: [RefundRequest, RefundTools] }) // from '@nestjs/ai/workflows'
|
|
91
|
+
export class RefundsModule {}
|
|
92
|
+
|
|
93
|
+
@Workflow('refund-request')
|
|
94
|
+
export class RefundRequest {
|
|
95
|
+
constructor(private readonly agents: AiAgents) {}
|
|
96
|
+
run(ctx: WorkflowContext, input: { email: string; customerId: string }) {
|
|
97
|
+
return this.agents.run(ctx, {
|
|
98
|
+
label: 'refund-request', instructions: '…', prompt: input.email,
|
|
99
|
+
tools: ['find-order', 'refund'], approve: ['refund'],
|
|
100
|
+
caller: { user: { id: input.customerId }, scopes: ['orders:refund'] },
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await this.workflows.start(RefundRequest, input, { id: `refund-${ticket.id}` }); // WorkflowClient; same id → same run
|
|
106
|
+
this.agents.pendingApprovals(runId); // [{ key, toolName, input }]
|
|
107
|
+
this.agents.approve(key); this.agents.reject(key, { reason }); // both also accept `by` (who answered), kept with the answer
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- The app also imports `WorkflowsModule` (with a DB store) and `AiModule`.
|
|
111
|
+
- Options are stored as data: models and tools by name, and `caller` as `user` + `scopes` only (there is no request after a restart). With authentication, mark the tool providers `@Public()`.
|
|
112
|
+
- Calls listed in `approve` suspend the instance until a person answers, or until `approvalTimeout` passes, in which case the call doesn't run. Rejected calls tell the model the reason.
|
|
113
|
+
- Agent function calls run **at least once**. Pass `ctx.idempotencyKey` to payment providers and other side effects.
|
|
114
|
+
- Failed model calls retry as workflow steps (3 attempts by default). After `stepsPerInstance` (10) model calls, the run continues in a child workflow to keep journals short.
|
|
115
|
+
- The output is the `generateText` result as JSON, also available via `WorkflowClient.getStatus(runId)`.
|
|
116
|
+
- `run()` options: `model`, `fallback`, `instructions`, `prompt`, `messages`, `tools`, `approve`, `approvalTimeout`, `caller`, `maxSteps` (25), `stepsPerInstance` (10), `budget`, `label`, `toolChoice`, `maxOutputTokens`, `temperature`, `providerOptions`.
|
|
117
|
+
|
|
118
|
+
## Testing
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { FakeModel, providerError, overrideModels, callTool } from '@nestjs/ai/testing';
|
|
122
|
+
|
|
123
|
+
const model = new FakeModel([
|
|
124
|
+
'Your order has shipped.', // text
|
|
125
|
+
{ toolCalls: [{ name: 'find-order', input: { orderId: 'ord_123' } }] }, // tool call
|
|
126
|
+
(call) => `The order is ${JSON.stringify(call.toolResults[0].output)}`, // function of the call
|
|
127
|
+
{ object: { category: 'shipping', orderId: 'ord_123', urgent: false } }, // generateObject
|
|
128
|
+
providerError(529), // APICallError; retryable for 408/409/429/5xx
|
|
129
|
+
]);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- **Answer options:**
|
|
133
|
+
- `usage` (10/10 tokens by default), `finishReason`
|
|
134
|
+
- `delay`, which abort and timeout cut short
|
|
135
|
+
- `chunkDelay`, for streams sent in word-sized chunks
|
|
136
|
+
- `streamError`
|
|
137
|
+
- **Helpers:** `model.reply(...)` appends answers, and `fakeModel(...)` is a shorthand. When answers run out, the call fails with `no answer left for call N`.
|
|
138
|
+
- **Inspecting calls:** `model.calls` and `model.lastCall` expose `instructions`, `lastUserMessage`, `toolResults`, `tools`, `stream` and `options`.
|
|
139
|
+
- **Unit and service tests:** `AiModule.forRoot({ models: { default: new FakeModel(), fast } })` in a testing module. For retry tests, use `timeout: 50` and `retry: { attempts: 2, backoff: { delay: 0 } }`.
|
|
140
|
+
- **E2E:** `overrideModels(Test.createTestingModule({ imports: [AppModule] }), { default: model })` keeps pricing, fallbacks and policies, and replaces `@InjectModel()` models too. Agent functions run for real, and their failures appear in `model.lastCall.toolResults`.
|
|
141
|
+
- **Agent functions:** `callTool(moduleRef, 'change-address', input, { caller: { request: { user } } })` runs schema → scopes → enhancers → method, and rejects with `AiToolError`. A direct method call skips all of that.
|
|
142
|
+
- **Streams:** POST to the chat route with `UIMessage`s and expect a 200 `text/event-stream` response, then split the body on `\n\n`.
|
|
143
|
+
- **Durable runs:** use `WorkflowsModule` with the in-memory store, a `ManualWorkflowClock` and the worker off. `WorkflowWorker.drain()` advances the run. Journaled calls aren't repeated on resume.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs-architecture
|
|
3
|
+
description: Structures NestJS 12 applications with feature modules, providers and dependency injection — custom and async providers, injection tokens, dynamic modules (ConfigurableModuleBuilder), injection scopes, circular dependencies, ModuleRef, lazy-loaded modules and lifecycle hooks. Use when adding a feature module, sharing a provider between modules, injecting a value/config/third-party client, building a reusable forRoot/forRootAsync module, or when the app fails at startup with "Nest can't resolve dependencies", a circular-dependency or "undefined at index" error, even if the user doesn't mention DI. Not for guards/pipes/interceptors (nestjs-request-pipeline) or ConfigModule specifics (nestjs-configuration).
|
|
4
|
+
compatibility: NestJS 12.x; Node 20.19+ to run, 22.22.3+/24.15+ for CLI generators
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.1.0"
|
|
7
|
+
nestjs-major: "12"
|
|
8
|
+
docs-crawled: "2026-11-04"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# NestJS architecture and dependency injection
|
|
12
|
+
|
|
13
|
+
## Default approach
|
|
14
|
+
|
|
15
|
+
One **feature module** per feature: its controller, providers and DTOs live together, and only what other modules need is exported. Scaffold, then wire:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx nest g resource billing --no-spec # or: nest g module|service|controller billing
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
A provider used by another module must be **exported by its own module**, and the consumer **imports that module**:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
@Module({ providers: [UsersService, UsersRepository], exports: [UsersService] })
|
|
25
|
+
export class UsersModule {}
|
|
26
|
+
|
|
27
|
+
@Module({ imports: [UsersModule], providers: [OrdersService] })
|
|
28
|
+
export class OrdersModule {}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Never add another module's provider to your own `providers` to "fix" a resolution error. That creates a second, independent instance, and the provider's own dependencies usually fail to resolve next.
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
- [ ] 1. Scaffold with `nest g` in the feature folder (`--dry-run` first if unsure where files will go).
|
|
36
|
+
- [ ] 2. Implement providers. Inject classes by type; inject anything that is not a class with `@Inject(TOKEN)` (see "Tokens").
|
|
37
|
+
- [ ] 3. Wire the modules: `providers` for what the module owns, `exports` for what others may inject, `imports` for modules whose exports it uses.
|
|
38
|
+
- [ ] 4. Build, then **start the app once**. Resolution errors appear only at bootstrap, never at compile time.
|
|
39
|
+
- [ ] 5. Run the unit and e2e tests; e2e tests that import `AppModule` catch wiring errors too.
|
|
40
|
+
|
|
41
|
+
## Tokens: injecting things that are not classes
|
|
42
|
+
|
|
43
|
+
Interfaces and type aliases are erased at runtime, so they cannot be tokens. Choose by what you have:
|
|
44
|
+
|
|
45
|
+
| You have | Register | Inject |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| A class | `providers: [MyService]` | `constructor(private s: MyService)` |
|
|
48
|
+
| An interface-typed value (config object, client instance) | `{ provide: PAYMENT_OPTIONS, useValue: {...} }` with `export const PAYMENT_OPTIONS = Symbol('PAYMENT_OPTIONS')` | `@Inject(PAYMENT_OPTIONS) private opts: PaymentOptions` |
|
|
49
|
+
| A contract with swappable implementations | `{ provide: LoggerService, useClass: PinoLogger }` where `LoggerService` is an **abstract class** | `constructor(private logger: LoggerService)`, no `@Inject()` needed |
|
|
50
|
+
| A value built from other providers / async setup | `{ provide: DB, useFactory: async (cfg: ConfigService) => connect(cfg.get('DB_URL')), inject: [ConfigService] }` | `@Inject(DB)` |
|
|
51
|
+
| An alias of an existing provider | `{ provide: 'AliasedLogger', useExisting: LoggerService }` | `@Inject('AliasedLogger')` |
|
|
52
|
+
|
|
53
|
+
Put tokens in their own file (e.g. `payments.constants.ts`), not in the module file: a module file that both declares a token and imports the provider using it causes a circular file import.
|
|
54
|
+
|
|
55
|
+
## Decision points
|
|
56
|
+
|
|
57
|
+
- **Reusable module configured by its consumer** (`forRoot`/`forRootAsync`/`register`) → use `ConfigurableModuleBuilder`; read `references/dynamic-modules.md`.
|
|
58
|
+
- **Per-request state** (tenant, user) → `Scope.REQUEST` or `@Inject(REQUEST)`. Scope bubbles up: every consumer of a request-scoped provider becomes request-scoped. Read `references/scopes-lifecycle.md` first and prefer `AsyncLocalStorage` (see the nestjs-logging skill) when only context needs to travel.
|
|
59
|
+
- **Two providers need each other** → refactor first (extract a third provider). If unavoidable, use `forwardRef()` on **both** sides; see `references/troubleshooting.md`.
|
|
60
|
+
- **Startup or shutdown work** (connect, warm caches, close connections) → `onModuleInit` / `onApplicationBootstrap` / `onModuleDestroy` / `onApplicationShutdown`. Shutdown hooks run only after `app.close()`, or on a signal if `app.enableShutdownHooks()` was called in `main.ts`.
|
|
61
|
+
- **Load a heavy module only when needed** → `LazyModuleLoader`; lifecycle hooks do not run in lazy-loaded modules.
|
|
62
|
+
- **Resolve providers dynamically** → `ModuleRef.get()` for singletons; `ModuleRef.resolve()` for request-scoped/transient ones (`get()` can't retrieve them).
|
|
63
|
+
|
|
64
|
+
## Gotchas
|
|
65
|
+
|
|
66
|
+
- **`Nest can't resolve dependencies ... argument X at index [n] is available in the Y module`:** X is neither in Y's `providers` nor exported by a module Y imports. Fix the export/import; don't duplicate the provider. Full procedure in `references/troubleshooting.md` ([Common errors](https://docs.nestjs.com/faq/common-errors)).
|
|
67
|
+
- **`... the argument at index [n] is available in the current module` (no token name):** the dependency is `undefined` at runtime. The usual causes are an interface/type used as the parameter type, an `import type` of an injectable class, or a circular file import.
|
|
68
|
+
- **Barrel files** (`index.ts`) re-exporting modules and providers cause circular-import errors. Import from the concrete file ([Circular dependency](https://docs.nestjs.com/fundamentals/circular-dependency)).
|
|
69
|
+
- **ESM projects** (`"type": "module"`) import local files with a `.js` extension: `import { UsersService } from './users.service.js'`.
|
|
70
|
+
- **`@Global()`** is for app-wide infrastructure only (config, logging, database). Making feature modules global hides dependencies and is "not a good design decision" ([Modules](https://docs.nestjs.com/modules)).
|
|
71
|
+
- **Lifecycle hooks** do not run for request-scoped providers, and in v12 they are called by component hierarchy level. Don't rely on the order between sibling providers ([Migration guide](https://docs.nestjs.com/migration-guide)).
|
|
72
|
+
- **`@Optional()` is not inherited** by a subclass that declares its own constructor (v12); repeat it on the subclass parameter.
|
|
73
|
+
- **Passport strategies and cron jobs must stay singletons**; never make their dependencies request-scoped ([Injection scopes](https://docs.nestjs.com/fundamentals/injection-scopes)).
|
|
74
|
+
- **SWC/ESM and circular DI:** wrap the type of a `forwardRef()` dependency in a `WrapperType<T> = T` alias (or the ORM's `Relation<T>`), so its type isn't emitted into decorator metadata ([SWC](https://docs.nestjs.com/recipes/swc)).
|
|
75
|
+
- **Debugging:** set `NEST_DEBUG=1` to log how each dependency is resolved and in which module Nest looks for it.
|
|
76
|
+
|
|
77
|
+
## Reference files
|
|
78
|
+
|
|
79
|
+
- `references/troubleshooting.md`: step-by-step diagnosis of resolution, undefined-dependency and circular-dependency errors, `forwardRef` patterns, monorepo duplicate `@nestjs/core`. Read when the app fails at startup or a dependency is `undefined`.
|
|
80
|
+
- `references/dynamic-modules.md`: `ConfigurableModuleBuilder`, `forRoot`/`forRootAsync`, custom method keys, extra options. Read when building a configurable module.
|
|
81
|
+
- `references/scopes-lifecycle.md`: injection scopes, `REQUEST`/`INQUIRER`, durable providers, lifecycle hook order, shutdown hooks, lazy loading, `ModuleRef`, `DiscoveryService`. Read before making anything request-scoped or relying on hook order.
|
|
82
|
+
|
|
83
|
+
Sources: [Providers](https://docs.nestjs.com/providers) · [Modules](https://docs.nestjs.com/modules) · [Custom providers](https://docs.nestjs.com/fundamentals/custom-providers) · [Common errors](https://docs.nestjs.com/faq/common-errors)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Dynamic modules with ConfigurableModuleBuilder
|
|
2
|
+
|
|
3
|
+
Source: [Dynamic modules](https://docs.nestjs.com/fundamentals/dynamic-modules) (NestJS 12).
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
- Naming: register vs forRoot vs forFeature
|
|
7
|
+
- Building a configurable module (default path)
|
|
8
|
+
- Consumer side: sync and async
|
|
9
|
+
- Options factory classes
|
|
10
|
+
- Extra options (e.g. isGlobal)
|
|
11
|
+
- Extending generated methods
|
|
12
|
+
|
|
13
|
+
## Naming: register vs forRoot vs forFeature
|
|
14
|
+
|
|
15
|
+
| Method | Meaning |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `register()` | Configures the module for the calling module only. Each importer can pass different options. |
|
|
18
|
+
| `forRoot()` | Configured once (usually in `AppModule`), shared everywhere. |
|
|
19
|
+
| `forFeature()` | Uses the `forRoot()` configuration, adjusted for the calling module (e.g. which repositories it can inject). |
|
|
20
|
+
|
|
21
|
+
Each has an async twin (`registerAsync`, `forRootAsync`, `forFeatureAsync`) that resolves options through DI.
|
|
22
|
+
|
|
23
|
+
## Building a configurable module (default path)
|
|
24
|
+
|
|
25
|
+
Don't hand-write `static register()`/`registerAsync()`. Use the builder in a separate `*.module-definition.ts` file:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// id-generator.module-definition.ts
|
|
29
|
+
import { ConfigurableModuleBuilder } from '@nestjs/common';
|
|
30
|
+
import type { IdGeneratorModuleOptions } from './id-generator.interfaces.js';
|
|
31
|
+
|
|
32
|
+
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE } =
|
|
33
|
+
new ConfigurableModuleBuilder<IdGeneratorModuleOptions>()
|
|
34
|
+
.setClassMethodName('forRoot') // omit to get register()/registerAsync()
|
|
35
|
+
.build();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// id-generator.module.ts
|
|
40
|
+
@Module({ providers: [IdGenerator], exports: [IdGenerator] })
|
|
41
|
+
export class IdGeneratorModule extends ConfigurableModuleClass {}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// id-generator.ts — inject the generated options token
|
|
46
|
+
@Injectable()
|
|
47
|
+
export class IdGenerator {
|
|
48
|
+
constructor(@Inject(MODULE_OPTIONS_TOKEN) private readonly options: IdGeneratorModuleOptions) {}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The metadata returned by the generated methods is merged with the `@Module()` metadata, so `providers`/`exports` in the decorator still apply.
|
|
53
|
+
|
|
54
|
+
## Consumer side
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
IdGeneratorModule.forRoot({ prefix: 'id' })
|
|
58
|
+
|
|
59
|
+
IdGeneratorModule.forRootAsync({
|
|
60
|
+
imports: [ConfigModule], // modules whose exports the factory needs
|
|
61
|
+
useFactory: (config: ConfigService) => ({ prefix: config.getOrThrow<string>('ID_PREFIX') }),
|
|
62
|
+
inject: [ConfigService], // order matches the factory's parameters
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Async options accept exactly one of `useFactory`, `useClass` or `useExisting`. `provideInjectionTokensFrom` lets a module forward providers it already has into the factory's `inject` list.
|
|
67
|
+
|
|
68
|
+
## Options factory classes
|
|
69
|
+
|
|
70
|
+
`useClass`/`useExisting` take a class implementing `ConfigurableModuleOptionsFactory<Options, 'create'>`, i.e. a `create()` method returning the options. Rename the expected method with `.setFactoryMethodName('createIdGeneratorOptions')`.
|
|
71
|
+
|
|
72
|
+
## Extra options (e.g. isGlobal)
|
|
73
|
+
|
|
74
|
+
Options that change the module definition but must not reach `MODULE_OPTIONS_TOKEN`:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
new ConfigurableModuleBuilder<IdGeneratorModuleOptions>()
|
|
78
|
+
.setExtras({ isGlobal: false }, (definition, extras) => ({ ...definition, global: extras.isGlobal }))
|
|
79
|
+
.build();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The injected options object does not contain `isGlobal`.
|
|
83
|
+
|
|
84
|
+
## Extending generated methods
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
export class IdGeneratorModule extends ConfigurableModuleClass {
|
|
88
|
+
static forRoot(options: typeof OPTIONS_TYPE): DynamicModule {
|
|
89
|
+
return { ...super.forRoot(options) /* plus your additions */ };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|