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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs-caching
|
|
3
|
+
description: Adds caching to NestJS 12 with @nestjs/cache-manager and cache-manager (Keyv) — CacheModule.register/registerAsync, in-memory and Redis (@keyv/redis) or multi-store setups, the CACHE_MANAGER Cache API (get/set/del/clear, TTL in milliseconds), CacheInterceptor auto-caching of GET responses, @CacheKey/@CacheTTL overrides, custom trackBy keys (per user or header), and caching WebSocket and microservice handlers. Use when the user wants to cache responses or query results, add Redis caching, set TTLs, invalidate entries, or debug stale or shared cached responses. Not for HTTP response compression or rate limiting (nestjs-http-platform, nestjs-security-hardening), queues (nestjs-queues) or distributed locks (nestjs-locks).
|
|
4
|
+
compatibility: NestJS 12.x; @nestjs/cache-manager, cache-manager (v7, Keyv-based); optional @keyv/redis, cacheable
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.1.0"
|
|
7
|
+
nestjs-major: "12"
|
|
8
|
+
docs-crawled: "2026-11-05"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# NestJS caching
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @nestjs/cache-manager cache-manager
|
|
15
|
+
npm i @keyv/redis cacheable # for Redis + a fast in-memory layer
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Choose a strategy
|
|
19
|
+
|
|
20
|
+
| Need | Use |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| Cache an expensive query or external call inside a service | Inject `CACHE_MANAGER`, read-through `get` → compute → `set` (default choice: explicit keys, explicit invalidation) |
|
|
23
|
+
| Cache whole public GET responses | `CacheInterceptor` on the controller or globally |
|
|
24
|
+
| Responses differ per user/tenant | `CacheInterceptor` subclass overriding `trackBy()`, or don't cache at the HTTP layer |
|
|
25
|
+
| Several instances | Redis store (shared); memory-only caches diverge per instance |
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
CacheModule.registerAsync({
|
|
31
|
+
isGlobal: true,
|
|
32
|
+
inject: [ConfigService],
|
|
33
|
+
useFactory: (config: ConfigService) => {
|
|
34
|
+
const redis = createKeyv(config.getOrThrow<string>('REDIS_URL')); // from '@keyv/redis'
|
|
35
|
+
redis.on('error', (err) => logger.error('Redis cache error', err)); // required: unhandled errors crash the app
|
|
36
|
+
return {
|
|
37
|
+
ttl: 60_000, // ms; unset/0 = never expires
|
|
38
|
+
stores: [new Keyv({ store: new KeyvCacheableMemory({ ttl: 60_000, lruSize: 5000 }) }), redis],
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`CacheModule.register()` alone gives an in-memory cache. `isGlobal: true` avoids importing it in each module.
|
|
45
|
+
|
|
46
|
+
## Service-level caching
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
constructor(@Inject(CACHE_MANAGER) private readonly cache: Cache, private readonly repo: ProductsRepository) {}
|
|
50
|
+
|
|
51
|
+
async findOne(id: string) {
|
|
52
|
+
const key = `product:${id}`;
|
|
53
|
+
const cached = await this.cache.get<Product>(key);
|
|
54
|
+
if (cached !== undefined && cached !== null) return cached; // v7 returns undefined on miss (v6: null)
|
|
55
|
+
const product = await this.repo.findOne(id);
|
|
56
|
+
await this.cache.set(key, product, 30_000); // TTL in ms; 0 = never expires
|
|
57
|
+
return product;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async update(id: string, dto: UpdateProductDto) {
|
|
61
|
+
const product = await this.repo.update(id, dto);
|
|
62
|
+
await this.cache.del(`product:${id}`); // invalidate on write
|
|
63
|
+
return product;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The memory store returns the **same object** you stored: treat cached values as read-only or clone before mutating.
|
|
68
|
+
|
|
69
|
+
## Response caching with CacheInterceptor
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
@Controller('catalog')
|
|
73
|
+
@UseInterceptors(CacheInterceptor)
|
|
74
|
+
@CacheTTL(5000)
|
|
75
|
+
export class CatalogController {
|
|
76
|
+
@Get() findAll() { … } // key = request URL
|
|
77
|
+
@CacheKey('catalog_featured') @CacheTTL(2000) @Get('featured') featured() { … }
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- Only GET routes are cached (unless a route has an explicit `@CacheKey()`); routes using `@Res()` can't be cached.
|
|
82
|
+
- Global: `{ provide: APP_INTERCEPTOR, useClass: CacheInterceptor }`.
|
|
83
|
+
- The URL key ignores the user: **never** put it on routes whose response depends on who asks (orders, profiles, anything behind record-level authorization). Override `trackBy(context)` to include the user/tenant, or return `undefined` to skip caching.
|
|
84
|
+
- Doesn't work in GraphQL (interceptors run per field resolver).
|
|
85
|
+
- WebSockets/microservices: `@CacheKey('events')` is **required** (and `@CacheTTL` needs it). Don't cache handlers that perform business operations.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
@Injectable()
|
|
89
|
+
export class UserAwareCacheInterceptor extends CacheInterceptor {
|
|
90
|
+
trackBy(context: ExecutionContext): string | undefined {
|
|
91
|
+
const req = context.switchToHttp().getRequest();
|
|
92
|
+
if (req.method !== 'GET' || !req.user) return undefined;
|
|
93
|
+
return `${req.user.id}:${req.originalUrl ?? req.url}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Testing
|
|
99
|
+
|
|
100
|
+
Unit: `{ provide: CACHE_MANAGER, useValue: { get: vi.fn(), set: vi.fn(), del: vi.fn() } }`. Integration: real `CacheModule.register()` (memory) and assert the second call skips the repository.
|
|
101
|
+
|
|
102
|
+
## Workflow
|
|
103
|
+
|
|
104
|
+
- [ ] 1. Decide service-level vs interceptor caching and the store (Redis for multi-instance).
|
|
105
|
+
- [ ] 2. Register `CacheModule` (global, TTL, Redis `error` listener).
|
|
106
|
+
- [ ] 3. Implement read-through + invalidation, or add the interceptor with per-user `trackBy` where needed.
|
|
107
|
+
- [ ] 4. Test hits, misses and invalidation. Build, lint, run tests, start the app.
|
|
108
|
+
|
|
109
|
+
Sources: [Caching](https://docs.nestjs.com/data/caching)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs-cli-tooling
|
|
3
|
+
description: Uses the NestJS 12 CLI and build tooling — nest generate schematics (resource, module, controller, service, guard, …), the CRUD resource generator, converting to a monorepo with apps and libraries (@app/* path aliases), builders (tsc, swc, rspack), SWC with type checking, nest-cli.json options, nest upgrade/nest info/nest deploy, REPL, hot reload and Devtools. Use when scaffolding anything Nest-shaped, adding a second app or shared library, speeding up builds, changing the compiler/bundler, fixing nest-cli.json or path alias issues, or upgrading Nest packages together, even if the user doesn't mention the CLI. Not for runtime module design (nestjs-architecture).
|
|
4
|
+
compatibility: NestJS 12.x; @nestjs/cli 12; generators need Node 22.22.3+, 24.15+ or 26+
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.1.0"
|
|
7
|
+
nestjs-major: "12"
|
|
8
|
+
docs-crawled: "2026-11-04"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# NestJS CLI and tooling
|
|
12
|
+
|
|
13
|
+
Use the project-local CLI: `npx nest …` (or the package manager's equivalent). Generators place files in Nest's conventional layout, register them in the closest module, and add spec files.
|
|
14
|
+
|
|
15
|
+
## Generate
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx nest g resource orders # prompts for transport (REST/GraphQL/microservice/WS) and CRUD entry points
|
|
19
|
+
npx nest g resource orders --type rest --crud # non-interactive (--crud generates CRUD entry points)
|
|
20
|
+
npx nest g module|controller|service|guard|interceptor|pipe|filter|middleware|decorator|gateway|resolver <name>
|
|
21
|
+
npx nest g service users/admin --flat # no extra folder
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
| Option | Effect |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `--dry-run` / `-d` | Preview the changes |
|
|
27
|
+
| `--no-spec` | Skip spec files (keep them by default) |
|
|
28
|
+
| `--skip-import` | Don't register in the closest module |
|
|
29
|
+
| `--flat` | Don't create a folder |
|
|
30
|
+
| `--project <name>` / `-p` | Target an app or library in a monorepo |
|
|
31
|
+
| `--type rest\|graphql-code-first\|graphql-schema-first\|microservice\|ws` | `resource` only |
|
|
32
|
+
|
|
33
|
+
`nest g resource` creates a module, controller, service, `dto/create-*.dto.ts`, `dto/update-*.dto.ts` (using `PartialType`, adding `@nestjs/mapped-types` if needed), `entities/*.entity.ts` and specs, and imports the module into the app module. It is TypeScript-only. Fill in the generated stubs (DTO fields, service logic) and replace the placeholder "should be defined" specs with real tests.
|
|
34
|
+
|
|
35
|
+
## Monorepo: apps and libraries
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx nest g app worker # first run converts standard mode → monorepo (moves src/ to apps/<name>/)
|
|
39
|
+
npx nest g library common # libs/common, aliased as @app/common in tsconfig paths
|
|
40
|
+
npx nest build worker # build one project; `nest build --all --parallel` builds all
|
|
41
|
+
npx nest start worker --watch
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- The conversion moves the existing app to `apps/<original-name>/` and sets `"monorepo": true` and the `projects` map in `nest-cli.json`. Update the `package.json` scripts (`start:prod` → `dist/apps/<name>/main`) and the lint/test globs.
|
|
45
|
+
- **v12 monorepos default to the Rspack builder** (`compilerOptions.builder: "rspack"`), which bundles each app into one file and already compiles with SWC.
|
|
46
|
+
- Import libraries through the alias: `import { CommonModule } from '@app/common'`. The alias lives only in `tsconfig.json` `paths`; **test runners resolve modules themselves**. Mirror the alias in Vitest (`resolve.tsconfigPaths: true` with Vite 8, as generated projects do) or in Jest `moduleNameMapper` for e2e configs.
|
|
47
|
+
- Project-level `compilerOptions` in `nest-cli.json` **replace** the top-level ones; they are not merged.
|
|
48
|
+
- Use `includeLibraryAssets` to copy a library's assets into an app build.
|
|
49
|
+
|
|
50
|
+
## Faster builds: SWC
|
|
51
|
+
|
|
52
|
+
Standard (non-monorepo) projects:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm i -D @swc/cli @swc/core
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
// nest-cli.json
|
|
60
|
+
{ "compilerOptions": { "builder": "swc", "typeCheck": true } }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- `typeCheck: true` runs `tsc --noEmit` alongside SWC, because SWC alone does not type-check.
|
|
64
|
+
- SWC emits ESM when `package.json` has `"type": "module"`, and CommonJS otherwise. A `.swcrc` overrides this; only add one for custom needs.
|
|
65
|
+
- Tests: Vitest needs `unplugin-swc` (the `swc.vite()` plugin) to emit decorator metadata; Jest needs `@swc/jest`.
|
|
66
|
+
- Circular imports under SWC: wrap property and constructor types in `Relation<T>` (TypeORM) or a `WrapperType<T> = T` alias.
|
|
67
|
+
|
|
68
|
+
## Upgrading and diagnostics
|
|
69
|
+
|
|
70
|
+
- `nest upgrade` (from the project root, after updating the global CLI): moves all `@nestjs/*` packages to the matching major at once, bumps TypeScript to v6, migrates `nest-cli.json` webpack options to Rspack, and prints a report. Run it with `--dry-run` first. It deliberately doesn't switch you to ESM, Vitest or oxlint.
|
|
71
|
+
- `nest info`: prints Node, CLI and `@nestjs/*` versions. Include it in bug reports.
|
|
72
|
+
- `nest add <lib>`: installs a library and runs its schematic.
|
|
73
|
+
- `nest deploy`: forwards to NestJS Mau (installs `@nestjs/mau` on first use).
|
|
74
|
+
|
|
75
|
+
## Gotchas
|
|
76
|
+
|
|
77
|
+
- **webpack is deprecated** in v12: `--webpack`, `--webpackPath` and `webpack`/`webpackConfigPath` in `nest-cli.json` → use `--builder rspack` (or `swc`/`tsc`). The hot-reload recipe still targets webpack-based CommonJS projects.
|
|
78
|
+
- **Generators need a newer Node** (22.22.3+, 24.15+ or 26+) than running the app (20.19+).
|
|
79
|
+
- `nest g decorator` generates the `Reflector.createDecorator()` form in v12.
|
|
80
|
+
- Run the generator from the project root; inside a monorepo, pass `--project`.
|
|
81
|
+
- Converting to a monorepo only works for the canonical Nest structure (`src/` + `test/` at the root); the schematic moves both folders into `apps/<name>/`.
|
|
82
|
+
|
|
83
|
+
## Workflow
|
|
84
|
+
|
|
85
|
+
- [ ] 1. Preview with `--dry-run`, then generate.
|
|
86
|
+
- [ ] 2. Check the module registration and imports the generator added; fill in the generated stubs.
|
|
87
|
+
- [ ] 3. After structural changes (monorepo, builder), update `package.json` scripts and test-runner aliases.
|
|
88
|
+
- [ ] 4. Build every affected project, run lint and both test suites, and start the built app.
|
|
89
|
+
|
|
90
|
+
## Reference files
|
|
91
|
+
|
|
92
|
+
- `references/nest-cli-json.md`: every `nest-cli.json` property (compilerOptions, generateOptions, assets, projects), REPL, hot reload, Devtools and CI integration. Read when editing `nest-cli.json` or setting up these tools.
|
|
93
|
+
|
|
94
|
+
Sources: [CLI usage](https://docs.nestjs.com/cli/usages) · [Workspaces](https://docs.nestjs.com/cli/monorepo) · [Libraries](https://docs.nestjs.com/cli/libraries) · [CRUD generator](https://docs.nestjs.com/recipes/crud-generator) · [SWC](https://docs.nestjs.com/recipes/swc) · [Migration guide](https://docs.nestjs.com/migration-guide)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# nest-cli.json, REPL, hot reload and Devtools
|
|
2
|
+
|
|
3
|
+
Sources: [Workspaces](https://docs.nestjs.com/cli/monorepo) · [CLI usage](https://docs.nestjs.com/cli/usages) · [REPL](https://docs.nestjs.com/recipes/repl) · [Hot reload](https://docs.nestjs.com/recipes/hot-reload) · [Devtools](https://docs.nestjs.com/devtools/overview) · [CI/CD integration](https://docs.nestjs.com/devtools/ci-cd-integration)
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
- compilerOptions
|
|
7
|
+
- generateOptions
|
|
8
|
+
- Assets
|
|
9
|
+
- Monorepo project entries
|
|
10
|
+
- REPL
|
|
11
|
+
- Hot reload
|
|
12
|
+
- Devtools
|
|
13
|
+
|
|
14
|
+
## compilerOptions
|
|
15
|
+
|
|
16
|
+
| Property | Type | Notes |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `builder` | `"tsc" \| "swc" \| "rspack"` or `{ type, options }` | Monorepos default to `rspack` in v12 |
|
|
19
|
+
| `typeCheck` | boolean | With `swc`: run type checking alongside (default `false`) |
|
|
20
|
+
| `emitDeclarations` | boolean | Emit `.d.ts` with SWC |
|
|
21
|
+
| `deleteOutDir` | boolean | Clean `dist` before each build |
|
|
22
|
+
| `assets` | array | Copy non-TS files (not on incremental watch compiles) |
|
|
23
|
+
| `watchAssets` | boolean | Watch all non-TS assets |
|
|
24
|
+
| `manualRestart` | boolean | Enable the `rs` shortcut to restart |
|
|
25
|
+
| `tsConfigPath` | string | Monorepo: tsconfig used when no project is given |
|
|
26
|
+
| `includeLibraryAssets` | array | Monorepo: library projects whose assets are copied into this app |
|
|
27
|
+
| `webpack`, `webpackConfigPath` | — | **Deprecated** in v12; use `builder` (`builder.options.configPath`) |
|
|
28
|
+
|
|
29
|
+
Builder object form: `{ "builder": { "type": "swc", "options": { "swcrcPath": "infrastructure/.swcrc" } } }`.
|
|
30
|
+
|
|
31
|
+
## generateOptions
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{ "generateOptions": { "spec": false, "flat": true } }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`spec` can also be an object per schematic, e.g. `{ "service": true, "controller": false }`. Command-line flags override it, and project-level `generateOptions` override the top-level ones.
|
|
38
|
+
|
|
39
|
+
## Assets
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{ "compilerOptions": { "assets": ["**/*.graphql", { "include": "i18n/**/*", "outDir": "dist/i18n", "watchAssets": true }] } }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Monorepo project entries
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"monorepo": true,
|
|
50
|
+
"root": "apps/api",
|
|
51
|
+
"sourceRoot": "apps/api/src",
|
|
52
|
+
"compilerOptions": { "builder": "rspack", "tsConfigPath": "apps/api/tsconfig.app.json" },
|
|
53
|
+
"projects": {
|
|
54
|
+
"api": { "type": "application", "root": "apps/api", "entryFile": "main", "sourceRoot": "apps/api/src", "compilerOptions": { "tsConfigPath": "apps/api/tsconfig.app.json" } },
|
|
55
|
+
"common": { "type": "library", "root": "libs/common", "entryFile": "index", "sourceRoot": "libs/common/src", "compilerOptions": { "tsConfigPath": "libs/common/tsconfig.lib.json" } }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
A project's `compilerOptions` replace the top-level ones; they are not merged.
|
|
61
|
+
|
|
62
|
+
## REPL
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// src/repl.ts
|
|
66
|
+
import { repl } from '@nestjs/core';
|
|
67
|
+
import { AppModule } from './app.module.js';
|
|
68
|
+
await repl(AppModule);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Run it with `npm run start -- --entryFile repl`. Inside: `get(UsersService)` (alias `$`), `await $(UsersService).findAll()`, `debug()`, `methods(UsersService)`, `resolve()`, `select()`.
|
|
72
|
+
|
|
73
|
+
## Hot reload
|
|
74
|
+
|
|
75
|
+
The documented recipe targets **webpack-based CommonJS** projects (webpack is deprecated in v12, and Rspack is the monorepo default). For ESM or SWC projects, `nest start --watch` with the SWC builder is usually fast enough. Use the recipe only if the project still uses webpack.
|
|
76
|
+
|
|
77
|
+
## Devtools
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const app = await NestFactory.create(AppModule, { snapshot: true }); // main.ts
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm i @nestjs/devtools-integration
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
@Module({ imports: [DevtoolsModule.register({ http: process.env.NODE_ENV !== 'production' })] })
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Devtools offers a graph explorer, routes explorer, playground, bootstrap performance analyzer, audit, and help with "Cannot resolve dependency" errors. CI/CD integration publishes graph snapshots for pull requests (see the CI/CD integration page).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs-configuration
|
|
3
|
+
description: Configures NestJS 12 applications with @nestjs/config — loading .env files and environment variables, fail-fast validation with a Standard Schema (Zod/Valibot/ArkType) validationSchema or validate() function, typed namespaced config with registerAs/ConfigType, global ConfigModule, reading config in main.ts and in async module factories (forRootAsync), env-specific files and variable expansion. Use when the user wants settings from env/.env, required variables, typed config, per-environment config, configuring another module from config, or when ConfigService is undefined, a variable is missing at startup, or Joi validation options stopped working after upgrading. Not for request validation (nestjs-validation-serialization).
|
|
4
|
+
compatibility: NestJS 12.x; @nestjs/config; zod (or valibot/arktype) for validation
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.1.0"
|
|
7
|
+
nestjs-major: "12"
|
|
8
|
+
docs-crawled: "2026-11-04"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# NestJS configuration
|
|
12
|
+
|
|
13
|
+
## Default recipe
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i @nestjs/config zod
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// config/env.schema.ts
|
|
21
|
+
import { z } from 'zod';
|
|
22
|
+
export const envSchema = z.object({
|
|
23
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
24
|
+
PORT: z.coerce.number().int().default(3000), // env values are strings: coerce
|
|
25
|
+
DATABASE_URL: z.url(), // no default → required: startup fails if missing
|
|
26
|
+
});
|
|
27
|
+
export type Env = z.infer<typeof envSchema>;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// app.module.ts
|
|
32
|
+
@Module({
|
|
33
|
+
imports: [
|
|
34
|
+
ConfigModule.forRoot({
|
|
35
|
+
isGlobal: true, // inject ConfigService anywhere without importing ConfigModule
|
|
36
|
+
validationSchema: envSchema, // any Standard Schema; ConfigService serves the schema's OUTPUT (coerced, defaulted)
|
|
37
|
+
load: [databaseConfig], // namespaced, typed config (below)
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
})
|
|
41
|
+
export class AppModule {}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// main.ts
|
|
46
|
+
const app = await NestFactory.create(AppModule);
|
|
47
|
+
const config = app.get(ConfigService);
|
|
48
|
+
await app.listen(config.getOrThrow<number>('PORT'));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Validation runs during bootstrap. Unknown variables (`PATH`, `HOME`) are ignored, and every failing variable is reported as `NAME: message`. In v12, Zod (or another Standard Schema library) is the recommended choice for new code ([Configuration](https://docs.nestjs.com/application/configuration)).
|
|
52
|
+
|
|
53
|
+
## Typed, namespaced config (preferred over string keys)
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// config/database.config.ts
|
|
57
|
+
export const databaseConfig = registerAs('database', () => ({
|
|
58
|
+
host: process.env.DB_HOST ?? 'localhost',
|
|
59
|
+
port: Number(process.env.DB_PORT ?? 5432),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// any provider
|
|
63
|
+
constructor(@Inject(databaseConfig.KEY) private readonly db: ConfigType<typeof databaseConfig>) {}
|
|
64
|
+
this.db.port; // number, fully typed
|
|
65
|
+
|
|
66
|
+
// configure another module from it
|
|
67
|
+
TypeOrmModule.forRootAsync(databaseConfig.asProvider())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- `ConfigModule.forFeature(databaseConfig)` registers a namespace from a feature module (partial registration). Values loaded this way may only be ready in `onModuleInit()`, not the constructor.
|
|
71
|
+
- `configService.get('database.host')` also works; prefer `.KEY` injection for types.
|
|
72
|
+
- In `registerAs` factories, `process.env` is fully resolved (`.env` merged) and has already passed validation.
|
|
73
|
+
|
|
74
|
+
## Reading values
|
|
75
|
+
|
|
76
|
+
| Need | Use |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| Optional value with fallback | `config.get<string>('FEATURE_GREETING', 'Hello')` |
|
|
79
|
+
| Required value | `config.getOrThrow<string>('API_KEY')`: throws if undefined |
|
|
80
|
+
| Typed keys from a schema | `ConfigService<Env, true>` + `get('PORT', { infer: true })` |
|
|
81
|
+
| Another module's async config | `forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (c: ConfigService) => ({ ... }) })`. Omit `imports` when `isGlobal: true`. |
|
|
82
|
+
|
|
83
|
+
## Decision points
|
|
84
|
+
|
|
85
|
+
- **Several env files** → `envFilePath: ['.env.development.local', '.env.development']` (the first match wins per key). `ignoreEnvFile: true` uses `process.env` only, e.g. with `node --env-file=.env`.
|
|
86
|
+
- **`${VAR}` references in `.env`** → `expandVariables: true`.
|
|
87
|
+
- **Load a module only when an env flag is set** → `ConditionalModule.registerWhen(FooModule, 'USE_FOO')`.
|
|
88
|
+
- **Env needed before `AppModule` is imported** (e.g. microservice options in `createMicroservice()`) → `nest start --env-file .env`, or `process.loadEnvFile()` in a file imported first in `main.ts`.
|
|
89
|
+
- **Hot paths reading config often** → `cache: true`.
|
|
90
|
+
- **Custom rules beyond a schema** → a **synchronous** `validate: (raw) => parsed`: return the validated object, or throw.
|
|
91
|
+
|
|
92
|
+
## Gotchas
|
|
93
|
+
|
|
94
|
+
- **Joi in v12** needs **Joi v18+**, and Joi settings (`allowUnknown`, `abortEarly`) move under `validationOptions.libraryOptions`. Top-level `validationOptions.allowUnknown` no longer applies. For new code, use Zod.
|
|
95
|
+
- **`ConfigService` undefined or not found in a feature module:** `ConfigModule` isn't global and isn't imported there. Set `isGlobal: true` once in `AppModule`.
|
|
96
|
+
- **Env values are strings:** a schema without `z.coerce.number()` rejects `PORT=3000`, and a manual `process.env.PORT` read gives a string. Rely on the schema output via `ConfigService`.
|
|
97
|
+
- **Tests and required vars:** e2e tests importing `AppModule` fail validation without the required variables. Provide test values in the test runner config (e.g. `test.env` in `vitest.config.e2e.ts`) or a `.env.test` file via `envFilePath`. Never weaken the schema for tests.
|
|
98
|
+
- **Never commit `.env`** with real secrets. Commit `.env.example` and add `.env` to `.gitignore`.
|
|
99
|
+
- **`process.loadEnvFile()` inside `main.ts`'s body runs too late**, because `forRoot()` already read `process.env` when `app.module` was imported. Node's native `.env` parser isn't fully dotenv-compatible.
|
|
100
|
+
|
|
101
|
+
## Workflow
|
|
102
|
+
|
|
103
|
+
- [ ] 1. `npm i @nestjs/config zod` (skip zod if the project already uses another Standard Schema library).
|
|
104
|
+
- [ ] 2. Write the env schema; mark required variables by leaving out defaults.
|
|
105
|
+
- [ ] 3. `ConfigModule.forRoot({ isGlobal: true, validationSchema })` in `AppModule`; add `registerAs` namespaces for grouped settings.
|
|
106
|
+
- [ ] 4. Read `PORT` etc. in `main.ts` with `app.get(ConfigService)`.
|
|
107
|
+
- [ ] 5. Add `.env.example`, make tests supply required variables, and run build, lint and tests. Start the app once **with and without** a required variable to confirm fail-fast.
|
|
108
|
+
|
|
109
|
+
Sources: [Configuration](https://docs.nestjs.com/application/configuration) · [Migration guide](https://docs.nestjs.com/migration-guide)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs-cqrs
|
|
3
|
+
description: Implements CQRS in NestJS 12 with @nestjs/cqrs — CqrsModule.forRoot options and custom publishers, typed Command/Query classes with CommandBus/QueryBus execute, @CommandHandler/@QueryHandler/@EventsHandler providers, AggregateRoot apply/commit with EventPublisher.mergeObjectContext, WithAggregateRoot mixin and IAggregateRoot, passing a transaction as dispatcher context to commit/publish (12.1+), RxJS @Saga with ofType, durable sagas via @nestjs/workflows/cqrs (@StartOn, @SignalOn), the UnhandledExceptionBus, subscribing to all events, and request-scoped handlers with AsyncContext. Use when the user asks for CQRS, commands and queries, domain events from aggregates, sagas, event sourcing foundations, or when event handler errors disappear or saga commands are lost on restart. Not for simple in-process notifications (nestjs-events), durable delivery to other services (nestjs-outbox) or long-running processes on their own (nestjs-workflows).
|
|
4
|
+
compatibility: NestJS 12.x; @nestjs/cqrs (12.1+ for awaited commit/dispatcher context); @nestjs/workflows for durable sagas
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.1.0"
|
|
7
|
+
nestjs-major: "12"
|
|
8
|
+
docs-crawled: "2026-11-05"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# NestJS CQRS (@nestjs/cqrs)
|
|
12
|
+
|
|
13
|
+
Use CQRS where separate read/write models or domain events pay off; for plain CRUD, services are simpler. Follow the project's existing structure (e.g. `orders/commands`, `orders/queries`, `orders/events`, `orders/sagas`).
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i @nestjs/cqrs
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
@Module({ imports: [CqrsModule.forRoot()] }) // options: commandPublisher, eventPublisher, queryPublisher, unhandledExceptionPublisher, eventIdProvider, rethrowUnhandled
|
|
21
|
+
export class AppModule {}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Commands and queries
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
export class PlaceOrderCommand extends Command<{ orderId: string }> { // generic = typed result
|
|
28
|
+
constructor(readonly customerId: string, readonly items: OrderItem[]) { super(); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@CommandHandler(PlaceOrderCommand)
|
|
32
|
+
export class PlaceOrderHandler implements ICommandHandler<PlaceOrderCommand> {
|
|
33
|
+
constructor(private readonly repo: OrdersRepository, private readonly publisher: EventPublisher) {}
|
|
34
|
+
async execute({ customerId, items }: PlaceOrderCommand) {
|
|
35
|
+
const order = this.publisher.mergeObjectContext(Order.place(customerId, items)); // aggregate can publish
|
|
36
|
+
await this.repo.save(order);
|
|
37
|
+
order.commit(); // dispatch applied events
|
|
38
|
+
return { orderId: order.id };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class GetOrderQuery extends Query<OrderView> { constructor(readonly id: string) { super(); } }
|
|
43
|
+
@QueryHandler(GetOrderQuery)
|
|
44
|
+
export class GetOrderHandler implements IQueryHandler<GetOrderQuery> {
|
|
45
|
+
constructor(private readonly views: OrderViewsRepository) {}
|
|
46
|
+
execute({ id }: GetOrderQuery) { return this.views.find(id); }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// controller
|
|
50
|
+
const { orderId } = await this.commandBus.execute(new PlaceOrderCommand(user.id, dto.items));
|
|
51
|
+
const order = await this.queryBus.execute(new GetOrderQuery(id)); // typed as OrderView
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- Register every handler, event handler and saga class in `providers`.
|
|
55
|
+
- Command handler errors reach exception filters; return values go back to the caller.
|
|
56
|
+
- One handler per command/query; many handlers per event.
|
|
57
|
+
|
|
58
|
+
## Aggregates and events
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
export class Order extends AggregateRoot {
|
|
62
|
+
static place(customerId: string, items: OrderItem[]) {
|
|
63
|
+
const order = new Order(randomUUID(), customerId, items);
|
|
64
|
+
order.apply(new OrderPlacedEvent(order.id, customerId)); // queued until commit()
|
|
65
|
+
return order;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@EventsHandler(OrderPlacedEvent)
|
|
70
|
+
export class OrderPlacedHandler implements IEventHandler<OrderPlacedEvent> {
|
|
71
|
+
async handle(event: OrderPlacedEvent) { try { … } catch (e) { this.logger.error(…); } }
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `autoCommit = true` publishes on `apply()`. `mergeClassContext(Order)` merges into a class. Existing base classes: `class Dragon extends WithAggregateRoot(Monster)`; framework-free domain: implement `IAggregateRoot`.
|
|
76
|
+
- Direct publishing: `eventBus.publish(event)` / `publishAll`.
|
|
77
|
+
- **Event handlers run outside the request**: exception filters don't see their errors and they can't write HTTP responses. Handle errors inside, or subscribe to `UnhandledExceptionBus` (`UnhandledExceptionBus.ofType(X)`); `rethrowUnhandled: true` rethrows instead.
|
|
78
|
+
- 12.1+: `commit()`/`publish()` return what the publisher returns and accept a dispatcher context: `await order.commit({ transaction: tx })` — for publishers that write through the transaction (outbox, workflows). `commit()` clears events immediately; on failure roll back and re-run the command with a fresh aggregate.
|
|
79
|
+
- The default event bus is in-memory: events are lost on crash. For cross-service or must-happen reactions, publish through the outbox (nestjs-outbox).
|
|
80
|
+
|
|
81
|
+
## Sagas
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
@Injectable()
|
|
85
|
+
export class OrderSagas {
|
|
86
|
+
@Saga()
|
|
87
|
+
orderPlaced = (events$: Observable<any>): Observable<ICommand> =>
|
|
88
|
+
events$.pipe(ofType(OrderPlacedEvent), map((e) => new ReserveStockCommand(e.orderId)));
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Sagas are in-memory singletons. For processes that must survive restarts (wait for payment, timeouts), use a durable workflow:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
@Module({ imports: [CqrsModule.forRoot(), WorkflowsModule.forRoot(), WorkflowsCqrsModule], providers: [PlaceOrderHandler, OrderFulfilmentWorkflow] })
|
|
96
|
+
|
|
97
|
+
@Workflow('order-fulfilment')
|
|
98
|
+
@StartOn(OrderPlacedEvent, { id: (e) => `order-${e.order.id}`, input: (e) => e.order })
|
|
99
|
+
@SignalOn(OrderDeliveredEvent, { signal: shipmentDelivered, key: (e) => e.delivery.reference })
|
|
100
|
+
export class OrderFulfilmentWorkflow implements WorkflowRunner<Order, FulfilmentResult> { … }
|
|
101
|
+
|
|
102
|
+
// in the command handler's transaction, so the workflow exists iff the order does
|
|
103
|
+
await this.eventBus.publish(new OrderPlacedEvent(order), { transaction: tx });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
See nestjs-workflows for steps, signals and stores.
|
|
107
|
+
|
|
108
|
+
## Request scope
|
|
109
|
+
|
|
110
|
+
Handlers can be `@CommandHandler(X, { scope: Scope.REQUEST })`. Pass context explicitly: `commandBus.execute(cmd, new MyRequest(user))` where `MyRequest extends AsyncContext`, inject with `@Inject(REQUEST)`. Sagas stay singletons: carry context with `AsyncContext.of(event)` + `AsyncContext.merge(ctx, command)`.
|
|
111
|
+
|
|
112
|
+
## Testing
|
|
113
|
+
|
|
114
|
+
Unit-test handlers by instantiating them with fake repositories and `{ mergeObjectContext: (o) => o }` as publisher; assert `aggregate.getUncommittedEvents()`. Test sagas by feeding `of(new OrderPlacedEvent(…))` into the saga function and collecting emitted commands.
|
|
115
|
+
|
|
116
|
+
## Workflow
|
|
117
|
+
|
|
118
|
+
- [ ] 1. Install; `CqrsModule.forRoot()`.
|
|
119
|
+
- [ ] 2. Command/query classes with typed results; handlers registered as providers.
|
|
120
|
+
- [ ] 3. Aggregates apply events; command handlers merge context and commit after persisting (with the transaction as context when needed).
|
|
121
|
+
- [ ] 4. Event handlers with their own error handling; `UnhandledExceptionBus` subscriber; durable workflows instead of sagas for must-survive processes.
|
|
122
|
+
- [ ] 5. Tests. Build, lint, run tests, start the app.
|
|
123
|
+
|
|
124
|
+
Sources: [CQRS](https://docs.nestjs.com/recipes/cqrs)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs-database-drizzle
|
|
3
|
+
description: Integrates Drizzle ORM with NestJS 12 through the first-party @nestjs/drizzle package — DrizzleModule.forRoot/forRootAsync with a driver's drizzle() function, per-application connection pools closed on shutdown, @InjectDrizzle and getDrizzleToken, pgTable/mysqlTable schemas with $inferSelect types, defineRelations and typed db.query relational reads, transactions with db.transaction(tx), hand-written repositories, drizzle-kit migrations or migrate() at startup, read replicas with withReplicas, multiple named databases, and mocking the database in tests. Use when the project uses or wants Drizzle (PostgreSQL, MySQL, SQLite, Neon, Turso, D1), when adding a table, query, relation, transaction or migration with Drizzle, or when e2e tests break because a shared db instance was closed. Not for TypeORM, Prisma, Mongoose, MikroORM or Sequelize (their own skills).
|
|
4
|
+
compatibility: NestJS 12.x; @nestjs/drizzle; drizzle-orm v1 (rc) or ≥0.35; drizzle-kit
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.1.0"
|
|
7
|
+
nestjs-major: "12"
|
|
8
|
+
docs-crawled: "2026-11-05"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# NestJS + Drizzle (@nestjs/drizzle)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i @nestjs/drizzle drizzle-orm@rc pg
|
|
15
|
+
npm i -D drizzle-kit@rc @types/pg
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The docs target Drizzle v1 (`rc` tag). With v0.x (≥0.35), only relations and relational queries differ. Detect the installed version before writing relations.
|
|
19
|
+
|
|
20
|
+
## Schema and types
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
// src/db/schema.ts
|
|
24
|
+
export const users = pgTable('users', {
|
|
25
|
+
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
|
26
|
+
email: text().notNull().unique(),
|
|
27
|
+
isActive: boolean('is_active').notNull().default(true),
|
|
28
|
+
});
|
|
29
|
+
export const photos = pgTable('photos', {
|
|
30
|
+
id: integer().primaryKey().generatedAlwaysAsIdentity(),
|
|
31
|
+
userId: integer('user_id').notNull().references(() => users.id), // FK constraint
|
|
32
|
+
});
|
|
33
|
+
export type User = typeof users.$inferSelect;
|
|
34
|
+
export type NewUser = typeof users.$inferInsert;
|
|
35
|
+
|
|
36
|
+
// src/db/relations.ts — drives db.query only (v1 API)
|
|
37
|
+
export const relations = defineRelations(schema, (r) => ({
|
|
38
|
+
users: { photos: r.many.photos() },
|
|
39
|
+
photos: { user: r.one.users({ from: r.photos.userId, to: r.users.id }) },
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
// src/db/database.ts
|
|
43
|
+
export type Database = NodePgDatabase<typeof relations>;
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
No entity classes and no `forFeature()`: tables are plain objects you import where you query.
|
|
47
|
+
|
|
48
|
+
## Register (once, globally injectable)
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
DrizzleModule.forRootAsync({
|
|
52
|
+
inject: [ConfigService],
|
|
53
|
+
useFactory: (config: ConfigService) => ({
|
|
54
|
+
drizzle, // from 'drizzle-orm/node-postgres'
|
|
55
|
+
connection: config.getOrThrow<string>('DATABASE_URL'),
|
|
56
|
+
relations, // other drizzle() options (logger, casing) pass through
|
|
57
|
+
}),
|
|
58
|
+
}),
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `DrizzleModule` creates the client **per application instance** and closes `db.$client` on `app.close()` / shutdown signal (`autoCloseConnection: true`). Enable `app.enableShutdownHooks()`.
|
|
62
|
+
- `forRoot()` options are evaluated at import time; prefer `forRootAsync` with `ConfigService`.
|
|
63
|
+
- Bring your own instance (replicas): `useFactory: () => ({ db: withReplicas(drizzle(primaryUrl), [drizzle(replicaUrl)]) })`. Avoid passing a module-level `db` singleton to `forRoot()`: every app shares it and the first `app.close()` breaks the other e2e suites.
|
|
64
|
+
|
|
65
|
+
## Queries, transactions, repositories
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
@Injectable()
|
|
69
|
+
export class UsersService {
|
|
70
|
+
constructor(@InjectDrizzle() private readonly db: Database) {}
|
|
71
|
+
|
|
72
|
+
async findOne(id: number) { const [u] = await this.db.select().from(users).where(eq(users.id, id)); return u; }
|
|
73
|
+
findWithPhotos(id: number) { return this.db.query.users.findFirst({ where: { id }, with: { photos: true } }); }
|
|
74
|
+
async create(data: NewUser) { const [u] = await this.db.insert(users).values(data).returning(); return u; }
|
|
75
|
+
|
|
76
|
+
async deactivate(id: number) {
|
|
77
|
+
await this.db.transaction(async (tx) => { // every statement through tx
|
|
78
|
+
await tx.update(users).set({ isActive: false }).where(eq(users.id, id));
|
|
79
|
+
await tx.delete(photos).where(eq(photos.userId, id));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
- A query runs when awaited. A thrown error or `tx.rollback()` rolls back.
|
|
86
|
+
- Optional repository layer: an `@Injectable()` class with named query methods; it uses the injected db, so **it is not part of a transaction started elsewhere** — accept `tx` as a parameter or propagate it with `nestjs-cls` transactional + Drizzle adapter (nestjs-logging covers CLS).
|
|
87
|
+
- `@nestjs/outbox`, `@nestjs/webhooks` and `@nestjs/authentication` stores use `fromDrizzle(db)` and take your `tx`.
|
|
88
|
+
|
|
89
|
+
## Migrations
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// drizzle.config.ts
|
|
93
|
+
export default defineConfig({ dialect: 'postgresql', schema: './src/db/schema.ts', out: './drizzle', dbCredentials: { url: process.env.DATABASE_URL! } });
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- `npx drizzle-kit generate` → SQL files; `npx drizzle-kit migrate` applies them. Drizzle Kit runs outside Nest (no DI) and loads `.env` itself.
|
|
97
|
+
- Migrate at startup only for single-instance deployments: `useFactory: async () => { const db = drizzle(url, { relations }); await migrate(db, { migrationsFolder: './drizzle' }); return { db }; }`. With several instances, run `drizzle-kit migrate` as a release step.
|
|
98
|
+
- Custom SQL (e.g. other packages' store schemas): `npx drizzle-kit generate --custom --name=<name>`.
|
|
99
|
+
|
|
100
|
+
## Multiple databases
|
|
101
|
+
|
|
102
|
+
`DrizzleModule.forRoot({ name: 'analytics', drizzle, connection })` per extra database (one may stay unnamed = `default`); inject with `@InjectDrizzle('analytics')` or `getDrizzleToken('analytics')`. Drivers can differ.
|
|
103
|
+
|
|
104
|
+
## Testing
|
|
105
|
+
|
|
106
|
+
- Unit: `{ provide: getDrizzleToken(), useValue: mockDb }` — the chain (`select().from().where()`) must be mocked, so prefer mocking your repository class.
|
|
107
|
+
- Integration/e2e: override `getDrizzleToken()` with a PGlite database (`drizzle({ client: new PGlite() })` from `drizzle-orm/pglite`) after running `migrate(db, { migrationsFolder })` — in-process PostgreSQL, no server.
|
|
108
|
+
|
|
109
|
+
## Workflow
|
|
110
|
+
|
|
111
|
+
- [ ] 1. Install driver + `@nestjs/drizzle` + drizzle-kit; write `schema.ts` (+ `relations.ts`, `Database` type).
|
|
112
|
+
- [ ] 2. Register `DrizzleModule.forRootAsync`; enable shutdown hooks.
|
|
113
|
+
- [ ] 3. Inject with `@InjectDrizzle()`; wrap multi-statement writes in `db.transaction`.
|
|
114
|
+
- [ ] 4. `drizzle-kit generate`, review SQL, `drizzle-kit migrate`.
|
|
115
|
+
- [ ] 5. Tests (repository mocks; PGlite e2e). Build, lint, run tests, start the app.
|
|
116
|
+
|
|
117
|
+
Sources: [Drizzle](https://docs.nestjs.com/data/drizzle)
|