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,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs-upgrading
|
|
3
|
+
description: Migrates NestJS applications from v11 to v12 and keeps them current — nest upgrade, Node.js floors for running (20.19+/22.12+) vs CLI schematics (22.22.3+/24.15+/26+), ESM-only packages consumed via require(esm) and the Lambda NODE_OPTIONS flag, Jest on Node 24.9+ vs Vitest, TypeScript 6, lifecycle hook ordering by hierarchy, @Optional no longer inherited, Standard Schema in @Body/@Query/@Param and @nestjs/config (Joi 18 libraryOptions), Terminus HealthIndicatorService, graphql-ws and GraphiQL defaults, NATS v3, Rspack replacing webpack flags, routeConflictPolicy, errorCode, structured logging, and the optional CommonJS-to-ESM switch (type module, nodenext, .js extensions, import.meta.dirname). Use when upgrading Nest majors, fixing ERR_REQUIRE_ESM / ERR_REQUIRE_ASYNC_MODULE or UnknownDependenciesException after an upgrade, or converting a project to ESM. Not for adopting new v12 packages (use their own skills).
|
|
4
|
+
compatibility: Upgrading from NestJS 11.x to 12.x; TypeScript 6
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.1.0"
|
|
7
|
+
nestjs-major: "12"
|
|
8
|
+
docs-crawled: "2026-11-05"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Upgrading NestJS (v11 → v12)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i -g @nestjs/cli@latest @nestjs/schematics@latest # global binary first: `nest upgrade` ships with it
|
|
15
|
+
npm i -D @nestjs/cli@latest @nestjs/schematics@latest typescript@^6
|
|
16
|
+
nest upgrade # bumps every @nestjs/* package to the same major
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Upgrade on a branch with a green test suite. Move **all** `@nestjs/*` packages together, then fix compile errors, then go through the checklist below.
|
|
20
|
+
|
|
21
|
+
## Node.js
|
|
22
|
+
|
|
23
|
+
| Task | Minimum |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| Run a v12 app | **20.19+** or **22.12+** (21.x unsupported) |
|
|
26
|
+
| `nest new` / `generate` / `upgrade` | **22.22.3+**, **24.15+** or **26+** |
|
|
27
|
+
| Jest loading v12 packages | **24.9+** (otherwise `ERR_REQUIRE_ASYNC_MODULE`) |
|
|
28
|
+
|
|
29
|
+
The packages are ESM-only. CommonJS apps load them through `require(esm)`. **AWS Lambda disables it** on its Node 20/22/24 runtimes, so set `NODE_OPTIONS=--experimental-require-module` there (appended to any existing flags). Simplest path: the latest active LTS everywhere (CI, Docker, dev machines).
|
|
30
|
+
|
|
31
|
+
Your app **may stay CommonJS**. `nest upgrade` doesn't change module format.
|
|
32
|
+
|
|
33
|
+
## Breaking or behavioural changes
|
|
34
|
+
|
|
35
|
+
| Area | What to do |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| Lifecycle hooks | Now called by component hierarchy level, so the order of `onModuleInit`/`onApplicationBootstrap`/shutdown hooks may change. Review code that depends on ordering between related providers, and its tests |
|
|
38
|
+
| `@Optional()` | No longer inherited when a subclass redeclares its constructor (`UnknownDependenciesException`). Repeat `@Optional()` on the subclass constructor parameters |
|
|
39
|
+
| `@nestjs/config` | `validationSchema` takes any Standard Schema (Zod recommended). Joi needs **v18+**, and Joi settings move to `validationOptions.libraryOptions` (defaults stay `allowUnknown: true`, `abortEarly: false`) |
|
|
40
|
+
| `@nestjs/terminus` | `HealthIndicator`/`HealthCheckError` are removed. Inject `HealthIndicatorService` and use `check(key).up()/.down()`, or `.attempt(fn).withTimeout(ms)` |
|
|
41
|
+
| `@nestjs/graphql` | `subscriptions-transport-ws` is removed; use `subscriptions: { 'graphql-ws': true }`. GraphiQL is the default IDE; customise it with a `graphiql: { … }` object |
|
|
42
|
+
| NATS | Run `npm uninstall nats && npm i @nats-io/transport-node`. Headers come from `@nats-io/nats-core` |
|
|
43
|
+
| Testing | Jest needs Node 24.9+ (`nest upgrade` bumps Jest to v30), or migrate to Vitest (the default for ESM projects): update the test scripts and globals, and check `supertest` default imports |
|
|
44
|
+
| CLI bundling | `--webpack`/`--webpackPath` (and `webpack`/`webpackConfigPath` in `nest-cli.json`) are deprecated; use `--builder rspack` and `--rspackPath`. Rspack is the monorepo default. The `angular` schematic is removed |
|
|
45
|
+
| Custom pipes | Transform signatures are stricter and `ArgumentMetadata` is generic; fix hand-written types |
|
|
46
|
+
| Adapters | HTTP error mapping was reworked, and Express now drains in-flight requests on shutdown. Re-run e2e tests |
|
|
47
|
+
|
|
48
|
+
## New, opt-in (adopt deliberately)
|
|
49
|
+
|
|
50
|
+
- Standard Schema validation: `@Body({ schema })`, `@Param('id', { schema: z.coerce.number() })`, `StandardSchemaValidationPipe`, `StandardSchemaSerializerInterceptor` + `@SerializeOptions({ schema })`. `ValidationPipe`/`ClassSerializerInterceptor` remain supported (nestjs-validation-serialization).
|
|
51
|
+
- `ValidationPipe({ errorFormat: 'list' | 'grouped' })`.
|
|
52
|
+
- `NestFactory.create(AppModule, { routeConflictPolicy: { duplicate: 'error', shadow: 'warn' }, routeResolutionStrategy: 'specificity' })`. Shadowing such as `@Get(':id')` before `@Get('me')` is common, so turn on `warn` during the upgrade.
|
|
53
|
+
- `new BadRequestException('…', { errorCode: 'WEAK_PASSWORD' })`.
|
|
54
|
+
- Structured logging: `logger.log('User created', { userId })` attaches the object to the same entry.
|
|
55
|
+
- Observability: `createObserveModule()` + `instrument: ObserveInstrument`.
|
|
56
|
+
- gRPC `GrpcExceptionFilter`, Kafka `RegExp` patterns, request-scoped gateways, `handleDisconnect(client, reason)`, microservice pre-request hooks.
|
|
57
|
+
- CLI: `nest deploy` (Mau), `--emit-declarations` (SWC), `--no-type-check`, `--silent`, `nest build --all --parallel`, `includeLibraryAssets`, `bun` as package manager, oxlint for new projects, decorators generated as `Reflector.createDecorator()`.
|
|
58
|
+
|
|
59
|
+
## Optional: switch the project to ESM
|
|
60
|
+
|
|
61
|
+
Steps:
|
|
62
|
+
1. Add `"type": "module"` to `package.json`.
|
|
63
|
+
2. Ensure `tsconfig` has `"module": "nodenext"`, `"moduleResolution": "nodenext"`, `"resolvePackageJsonExports": true`, `"target": "ES2023"`. Projects from v11 schematics 11.0.6+ already have these.
|
|
64
|
+
3. Fix every relative import to end in `.js`.
|
|
65
|
+
4. Replace `__dirname` with `import.meta.dirname`, and `require` with `createRequire(import.meta.url)`.
|
|
66
|
+
5. Use top-level `await bootstrap()`.
|
|
67
|
+
|
|
68
|
+
The whole project switches at once. See `references/esm-switch.md`.
|
|
69
|
+
|
|
70
|
+
## Workflow
|
|
71
|
+
|
|
72
|
+
- [ ] 1. Raise Node.js in CI, Docker, `engines` and Lambda (`NODE_OPTIONS`). Upgrade the global CLI, then run `nest upgrade` and update TypeScript to 6.
|
|
73
|
+
- [ ] 2. Fix compile errors (pipe types, removed Terminus API, config `libraryOptions`, NATS imports, GraphQL subscription transport).
|
|
74
|
+
- [ ] 3. Check lifecycle-ordering assumptions and `@Optional()` subclasses. Enable `routeConflictPolicy` warnings and fix shadowed routes.
|
|
75
|
+
- [ ] 4. Fix the test runner (Jest 30 on Node 24.9+, or Vitest). Run unit and e2e tests, especially error responses and graceful shutdown.
|
|
76
|
+
- [ ] 5. Replace deprecated webpack flags with Rspack where used. Build, lint, run tests, start the app. Then optionally plan the ESM switch as a separate change.
|
|
77
|
+
|
|
78
|
+
## Reference files
|
|
79
|
+
|
|
80
|
+
- `references/esm-switch.md`: the complete ESM `tsconfig`, Jest vs Vitest `types`, import-extension and global replacements, and before/after snippets for the Terminus, config and NATS changes. Read when editing code for the upgrade or converting to ESM.
|
|
81
|
+
|
|
82
|
+
Sources: [Migration guide](https://docs.nestjs.com/migration-guide)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# v12 upgrade snippets and ESM switch
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
- Terminus custom indicators
|
|
5
|
+
- @Optional in subclasses
|
|
6
|
+
- Config validation
|
|
7
|
+
- GraphQL IDE and subscriptions
|
|
8
|
+
- NATS headers
|
|
9
|
+
- Switching to ESM
|
|
10
|
+
- Moving code to ESM
|
|
11
|
+
|
|
12
|
+
## Terminus custom indicators
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// v11 (removed)
|
|
16
|
+
export class DogHealthIndicator extends HealthIndicator {
|
|
17
|
+
async isHealthy(key: string) {
|
|
18
|
+
const badboys = await this.dogService.getBadboys();
|
|
19
|
+
const result = this.getStatus(key, badboys.length === 0, { badboys: badboys.length });
|
|
20
|
+
if (badboys.length) throw new HealthCheckError('Dog check failed', result);
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// v12
|
|
26
|
+
@Injectable()
|
|
27
|
+
export class DogHealthIndicator {
|
|
28
|
+
constructor(private readonly dogService: DogService, private readonly healthIndicatorService: HealthIndicatorService) {}
|
|
29
|
+
async isHealthy(key: string) {
|
|
30
|
+
const indicator = this.healthIndicatorService.check(key);
|
|
31
|
+
const badboys = await this.dogService.getBadboys();
|
|
32
|
+
return badboys.length > 0 ? indicator.down({ badboys: badboys.length }) : indicator.up();
|
|
33
|
+
}
|
|
34
|
+
ping(key: string) {
|
|
35
|
+
return this.healthIndicatorService.check(key).attempt(() => this.dogService.ping()).withTimeout(1000);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## @Optional in subclasses
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
@Injectable()
|
|
44
|
+
class Child extends Base {
|
|
45
|
+
constructor(@Optional() options?: Options) { super(options); } // re-declare the marker
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
From 12.1, a subclass *without* its own constructor keeps its parent's markers again. 12.0.x dropped them in that case too, so upgrade to ≥ 12.1.
|
|
50
|
+
|
|
51
|
+
## Config validation
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
ConfigModule.forRoot({
|
|
55
|
+
validationSchema: z.object({
|
|
56
|
+
NODE_ENV: z.enum(['development', 'production', 'test', 'provision']).default('development'),
|
|
57
|
+
PORT: z.coerce.number().default(3000),
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Joi 18+: library options move
|
|
62
|
+
validationOptions: { libraryOptions: { allowUnknown: false, abortEarly: true } },
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## GraphQL IDE and subscriptions
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
GraphQLModule.forRoot<ApolloDriverConfig>({
|
|
69
|
+
driver: ApolloDriver,
|
|
70
|
+
graphiql: { url: '/graphql', headers: { authorization: 'Bearer <token>' }, shouldPersistHeaders: true, isHeadersEditorEnabled: true },
|
|
71
|
+
subscriptions: { 'graphql-ws': true }, // subscriptions-transport-ws removed
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## NATS headers
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import * as nats from '@nats-io/nats-core';
|
|
79
|
+
const headers = nats.headers();
|
|
80
|
+
headers.set('x-version', '1.0.0');
|
|
81
|
+
this.client.send('record-builder-duplex', new NatsRecordBuilder(payload).setHeaders(headers).build());
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Switching to ESM
|
|
85
|
+
|
|
86
|
+
`package.json`: `"type": "module"`. Projects generated with schematics ≥ 11.0.6 already have the right `tsconfig`. Older projects need `module`/`moduleResolution: "nodenext"`, `resolvePackageJsonExports: true` and `target: "ES2023"`. The complete set generated by `nest new` (ESM):
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"compilerOptions": {
|
|
91
|
+
"module": "nodenext", "moduleResolution": "nodenext", "resolvePackageJsonExports": true,
|
|
92
|
+
"esModuleInterop": true, "isolatedModules": true, "declaration": true, "removeComments": true,
|
|
93
|
+
"emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true,
|
|
94
|
+
"target": "ES2023", "sourceMap": true, "outDir": "./dist", "rootDir": ".", "incremental": true,
|
|
95
|
+
"skipLibCheck": true, "strict": true, "strictPropertyInitialization": false,
|
|
96
|
+
"types": ["vitest/globals", "node"]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- CommonJS + Jest uses `"types": ["node", "jest"]`.
|
|
102
|
+
- `nest-cli.json` and `tsconfig.build.json` don't change.
|
|
103
|
+
- With `nodenext`, every file's emit format follows `package.json`. After adding `type: module`, expect every missing import extension to error at once, and fix them all in one pass.
|
|
104
|
+
|
|
105
|
+
## Moving code to ESM
|
|
106
|
+
|
|
107
|
+
| CommonJS | ESM |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `import { AppModule } from './app.module'` | `'./app.module.js'` (the extension refers to the emitted file) |
|
|
110
|
+
| `__dirname`, `__filename` | `import.meta.dirname` (or `fileURLToPath(import.meta.url)`) |
|
|
111
|
+
| `require(x)` | `createRequire(import.meta.url)` from `node:module` |
|
|
112
|
+
| `bootstrap();` | `await bootstrap();` |
|
|
113
|
+
|
|
114
|
+
Also check custom bootstrap scripts, bundler configs, and test-runner configs that assume CommonJS.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs-validation-serialization
|
|
3
|
+
description: Validates and transforms NestJS 12 request input (body, query, params) and shapes responses — StandardSchemaValidationPipe with Zod/Valibot/ArkType schemas via the decorator `schema` option, or ValidationPipe with class-validator DTOs; coercing query strings to numbers with defaults; rejecting unknown fields; mapped/derived DTOs; and hiding fields such as passwords with StandardSchemaSerializerInterceptor or ClassSerializerInterceptor. Use when adding or fixing request validation, pagination/query parsing, DTOs, "strip/forbid unknown properties", validation error format, or when a sensitive field leaks in responses, even if the user only says "make sure the input is valid" or "don't return X". Not for config/env validation (nestjs-configuration) or OpenAPI docs (nestjs-openapi).
|
|
4
|
+
compatibility: NestJS 12.x; zod (or valibot/arktype) for schema validation, or class-validator + class-transformer for DTO classes
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.1.0"
|
|
7
|
+
nestjs-major: "12"
|
|
8
|
+
docs-crawled: "2026-11-04"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# NestJS validation and serialization
|
|
12
|
+
|
|
13
|
+
NestJS 12 ships two equally supported validation flows. Both pipes can be registered together; each validates only the parameters it understands.
|
|
14
|
+
|
|
15
|
+
| | Schema-first | Class-based DTOs |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| Pipe | `StandardSchemaValidationPipe` | `ValidationPipe` |
|
|
18
|
+
| Rules live in | A Zod/Valibot/ArkType schema; the type is inferred | Decorators on a DTO class |
|
|
19
|
+
| Attach with | `@Body({ schema })`, `@Query({ schema })`, `@Param('id', { schema })` | Parameter typed with the DTO class |
|
|
20
|
+
| Unknown fields | `z.strictObject()` rejects, `z.object()` strips | `whitelist: true` strips, plus `forbidNonWhitelisted: true` rejects |
|
|
21
|
+
| Responses | `StandardSchemaSerializerInterceptor` + `@SerializeOptions({ schema })` | `ClassSerializerInterceptor` + `@Exclude()` |
|
|
22
|
+
|
|
23
|
+
**Pick the flow by detection:** `zod`/`valibot`/`arktype` in dependencies → schema-first. `class-validator` → class-based. Neither → schema-first with Zod: no reflection metadata, and the same library `@nestjs/config` uses for env validation in v12. Don't mix flows inside one feature.
|
|
24
|
+
|
|
25
|
+
## Default recipe: schema-first
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// main.ts — global, safe: only parameters that declare a schema are validated
|
|
29
|
+
app.useGlobalPipes(new StandardSchemaValidationPipe());
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// products/create-product.schema.ts
|
|
34
|
+
export const createProductSchema = z.strictObject({ // strictObject: unknown keys → 400
|
|
35
|
+
name: z.string().trim().min(1),
|
|
36
|
+
price: z.number().positive(), // body numbers are NOT coerced: "5" → 400
|
|
37
|
+
tags: z.array(z.string()).optional(),
|
|
38
|
+
});
|
|
39
|
+
export type CreateProductDto = z.infer<typeof createProductSchema>;
|
|
40
|
+
|
|
41
|
+
// products/list-products.query.ts — query strings ARE strings: coerce + defaults + bounds
|
|
42
|
+
export const listProductsQuery = z.object({
|
|
43
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
44
|
+
limit: z.coerce.number().int().min(1).max(100).default(20),
|
|
45
|
+
});
|
|
46
|
+
export type ListProductsQuery = z.infer<typeof listProductsQuery>;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
@Post() create(@Body({ schema: createProductSchema }) dto: CreateProductDto) {}
|
|
51
|
+
@Get() list(@Query({ schema: listProductsQuery }) query: ListProductsQuery) {}
|
|
52
|
+
@Get(':id') findOne(@Param('id', { schema: z.coerce.number().int().positive() }) id: number) {}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The handler receives the schema's **output** (coerced, defaulted, trimmed). Invalid input returns `400` with `message: ["price: ...", ...]`.
|
|
56
|
+
|
|
57
|
+
## Class-based recipe
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }));
|
|
61
|
+
|
|
62
|
+
export class CreateProductDto {
|
|
63
|
+
@IsString() @IsNotEmpty() name: string;
|
|
64
|
+
@IsNumber() @IsPositive() price: number;
|
|
65
|
+
@IsOptional() @IsArray() @IsString({ each: true }) tags?: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// query values: documented per-parameter conversion with defaults
|
|
69
|
+
@Get() list(
|
|
70
|
+
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
|
|
71
|
+
@Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number, // range check: throw BadRequestException, or use a schema
|
|
72
|
+
) {}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `transform: true` turns payloads into DTO instances and converts primitive path/query params from their declared types. With it off, convert explicitly with `ParseIntPipe`/`ParseBoolPipe`.
|
|
76
|
+
- Arrays of DTOs in a body: `@Body(new ParseArrayPipe({ items: CreateUserDto })) dtos: CreateUserDto[]`. Arrays in a query (`?ids=1,2`): `new ParseArrayPipe({ items: Number, separator: ',' })`.
|
|
77
|
+
- For bounded query numbers (min/max), a schema (`z.coerce.number().int().min(1).max(100)`) is the least code; `StandardSchemaValidationPipe` and `ValidationPipe` can coexist.
|
|
78
|
+
- Derive DTOs with `PartialType`, `PickType`, `OmitType`, `IntersectionType` from `@nestjs/mapped-types` (or `@nestjs/swagger` when using OpenAPI). With schemas, use `.partial()`, `.pick()`, `.omit()`, `.extend()`.
|
|
79
|
+
- v12 adds `errorFormat: 'grouped'` (errors keyed by property path) next to the default `'list'`.
|
|
80
|
+
|
|
81
|
+
## Never hand-parse input in services
|
|
82
|
+
|
|
83
|
+
Validation belongs in the pipe layer. Parsing `page`/`limit` strings or checking `typeof body.price` inside a service bypasses the shared 400 format and OpenAPI generation, and repeats work in every handler. Use the schema/DTO recipes above, or the `Parse*Pipe` + `DefaultValuePipe` pair for a single param: `@Query('page', new DefaultValuePipe(1), ParseIntPipe)`.
|
|
84
|
+
|
|
85
|
+
## Hiding response fields
|
|
86
|
+
|
|
87
|
+
Enforce it once, centrally, so future endpoints are covered too:
|
|
88
|
+
|
|
89
|
+
- **Schema allowlist (default):** a response schema lists exactly what may leave the API; `z.object()` strips everything else.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
export const userResponseSchema = z.object({ id: z.number(), email: z.string(), name: z.string() });
|
|
93
|
+
|
|
94
|
+
@Controller('users')
|
|
95
|
+
@UseInterceptors(StandardSchemaSerializerInterceptor)
|
|
96
|
+
@SerializeOptions({ schema: userResponseSchema }) // class level: every handler; arrays → per element
|
|
97
|
+
export class UsersController {}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
App-wide: `{ provide: APP_INTERCEPTOR, useClass: StandardSchemaSerializerInterceptor }` (Nest injects `Reflector`). Handlers without a schema pass through unchanged.
|
|
101
|
+
|
|
102
|
+
- **Class-based:** `@Exclude() passwordHash` on the entity, plus `ClassSerializerInterceptor` (global via `APP_INTERCEPTOR`). It only applies to **class instances**. If services return plain objects, add `@SerializeOptions({ type: UserEntity })` so they are converted first; otherwise the field still leaks.
|
|
103
|
+
|
|
104
|
+
## Gotchas
|
|
105
|
+
|
|
106
|
+
- Zod requires `strictNullChecks` in `tsconfig.json`.
|
|
107
|
+
- `ValidationPipe` validates only class-typed parameters. Interfaces are erased (`metatype` is `Object`), so nothing is validated.
|
|
108
|
+
- `ValidationPipe` skips custom param decorators unless `validateCustomDecorators: true`.
|
|
109
|
+
- Global pipes in a hybrid app don't cover connected microservices by default. For WebSockets/microservices, return `WsException`/`RpcException` from `exceptionFactory`.
|
|
110
|
+
- Path and query values always arrive as strings: coerce them explicitly (`z.coerce.number()`, `ParseIntPipe`, or `transform: true` for primitive params).
|
|
111
|
+
|
|
112
|
+
## Workflow
|
|
113
|
+
|
|
114
|
+
- [ ] 1. Detect the flow (dependencies), and install what's missing (`npm i zod`, or `npm i class-validator class-transformer`).
|
|
115
|
+
- [ ] 2. Register the pipe once, globally.
|
|
116
|
+
- [ ] 3. Write the schema/DTO next to the feature; attach it to the parameter.
|
|
117
|
+
- [ ] 4. For responses with sensitive or internal fields, add a response schema (or `@Exclude` + `type`).
|
|
118
|
+
- [ ] 5. Add e2e cases for the 400s (missing field, wrong type, unknown field) and for hidden fields; run build, lint and tests.
|
|
119
|
+
|
|
120
|
+
## Reference files
|
|
121
|
+
|
|
122
|
+
- `references/validation-options.md`: every `ValidationPipe` and `StandardSchemaValidationPipe` option, Valibot/ArkType equivalents, arrays and custom decorators, and schema serialization errors. Read when tuning options or using a library other than Zod.
|
|
123
|
+
|
|
124
|
+
Sources: [Validation](https://docs.nestjs.com/application/validation) · [Serialization](https://docs.nestjs.com/application/serialization) · [Pipes](https://docs.nestjs.com/pipes)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Validation and serialization options
|
|
2
|
+
|
|
3
|
+
Sources: [Validation](https://docs.nestjs.com/application/validation) · [Serialization](https://docs.nestjs.com/application/serialization)
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
- ValidationPipe options
|
|
7
|
+
- StandardSchemaValidationPipe options
|
|
8
|
+
- Custom error shape
|
|
9
|
+
- Valibot and ArkType
|
|
10
|
+
- Custom param decorators
|
|
11
|
+
- StandardSchemaSerializerInterceptor options and errors
|
|
12
|
+
- Non-HTTP transports
|
|
13
|
+
|
|
14
|
+
## ValidationPipe options
|
|
15
|
+
|
|
16
|
+
`new ValidationPipe({ ... })`; it extends class-validator's `ValidatorOptions`.
|
|
17
|
+
|
|
18
|
+
| Option | Effect |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `transform` | Convert payloads to DTO instances, and primitive params to their declared types |
|
|
21
|
+
| `whitelist` | Strip properties without validation decorators |
|
|
22
|
+
| `forbidNonWhitelisted` | With `whitelist`: reject such properties instead (400) |
|
|
23
|
+
| `forbidUnknownValues` | Fail validation of unknown objects |
|
|
24
|
+
| `skipMissingProperties` / `skipNullProperties` / `skipUndefinedProperties` | Skip validating missing / null / undefined properties |
|
|
25
|
+
| `stopAtFirstError` | Stop at the first failed constraint per property |
|
|
26
|
+
| `errorFormat` | `'list'` (default, array of strings) or `'grouped'` (object keyed by property path), v12 |
|
|
27
|
+
| `disableErrorMessages` | Omit details from 400 responses (production hardening) |
|
|
28
|
+
| `errorHttpStatusCode` | Status for failures (default 400) |
|
|
29
|
+
| `exceptionFactory` | `(errors: ValidationError[]) => any`: build your own exception |
|
|
30
|
+
| `groups`, `always`, `strictGroups` | class-validator groups |
|
|
31
|
+
| `dismissDefaultMessages` | Drop class-validator default messages |
|
|
32
|
+
| `validationError.target` / `.value` | Include target/value in `ValidationError` |
|
|
33
|
+
| `enableDebugMessages` | Warn when something is misconfigured |
|
|
34
|
+
|
|
35
|
+
## StandardSchemaValidationPipe options
|
|
36
|
+
|
|
37
|
+
| Option | Default | Effect |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `transform` | `true` | Handler gets the schema output (coercion, defaults, transforms); `false` passes the original input |
|
|
40
|
+
| `validateCustomDecorators` | `false` | Also validate `createParamDecorator()` params that declare a schema |
|
|
41
|
+
| `validateOptions` | — | Forwarded to `~standard.validate()` |
|
|
42
|
+
| `errorHttpStatusCode` | `400` | Status on failure |
|
|
43
|
+
| `exceptionFactory` | — | `(issues) => exception`; each issue has `message` and an optional `path` |
|
|
44
|
+
|
|
45
|
+
## Custom error shape
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
new StandardSchemaValidationPipe({
|
|
49
|
+
exceptionFactory: (issues) => new UnprocessableEntityException({
|
|
50
|
+
message: 'Validation failed',
|
|
51
|
+
errors: issues.map((i) => ({
|
|
52
|
+
path: i.path?.map((s) => (typeof s === 'object' ? s.key : s)).join('.'),
|
|
53
|
+
message: i.message,
|
|
54
|
+
})),
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
To keep a global error format (e.g. `errorCode`), throw `new BadRequestException('Validation failed', { errorCode: 'VALIDATION_FAILED' })` from `exceptionFactory`, and let the app's global exception filter add the rest.
|
|
60
|
+
|
|
61
|
+
## Valibot and ArkType
|
|
62
|
+
|
|
63
|
+
Any Standard Schema library works the same way with the `schema` option:
|
|
64
|
+
- Valibot: `v.object({...})` strips, `v.strictObject({...})` rejects unknown keys. Use `v.pipe(v.string(), v.transform(Number))` for coercion.
|
|
65
|
+
- ArkType: `type({ ... })`.
|
|
66
|
+
|
|
67
|
+
Zod quick reference: `z.object` strips, `z.strictObject` rejects, `z.looseObject` keeps unknown keys. Use `z.coerce.number()` for query/path numbers, and `.default()` for defaults. Derive schemas with `.partial()`, `.pick()`, `.omit()`, `.extend()`.
|
|
68
|
+
|
|
69
|
+
## Custom param decorators
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
@Get() find(@User('email', { schema: z.email() }) email: string) {} // needs validateCustomDecorators: true on the pipe
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## StandardSchemaSerializerInterceptor options and errors
|
|
76
|
+
|
|
77
|
+
- `@SerializeOptions({ schema })` on a handler or class; the handler-level setting wins. Arrays get the item schema applied per element.
|
|
78
|
+
- `null`, `undefined`, primitives and `StreamableFile` responses are not processed.
|
|
79
|
+
- Constructor defaults: `new StandardSchemaSerializerInterceptor(reflector, { schema, validateOptions })`. When registered via `APP_INTERCEPTOR`, Reflector is injected.
|
|
80
|
+
- A response that fails its schema (missing or mistyped property) is treated as a server bug: the client gets `500` instead of partial data, and the issues are logged.
|
|
81
|
+
|
|
82
|
+
## Non-HTTP transports
|
|
83
|
+
|
|
84
|
+
Both validation pipes throw HTTP exceptions by default. For WebSockets/microservices, return `new WsException(...)` / `new RpcException(...)` from `exceptionFactory`.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs-webhooks
|
|
3
|
+
description: Sends and receives webhooks in NestJS 12 with @nestjs/webhooks — outgoing Standard Webhooks dispatched in your database transaction through @nestjs/outbox, signed and retried by a worker with backoff, endpoint subscriptions per tenant, secret rotation and encryption at rest, the delivery log and replay, SSRF protection; and incoming webhooks verified on the raw body with @VerifyWebhook (standard, Stripe-style, GitHub or custom schemes), deduplicated and processed exactly once with processInTransaction(). Use when the user wants to notify partners or customers of events by webhook, let them register endpoints, retry or replay deliveries, or accept and verify webhooks from Stripe, GitHub, a payment provider or any signed sender. Not for raw body setup alone (nestjs-http-platform), the outbox itself (nestjs-outbox) or calling third-party APIs (nestjs-http-client).
|
|
4
|
+
compatibility: NestJS 12.x; @nestjs/webhooks + @nestjs/outbox; PostgreSQL or MySQL 8.4+ via Drizzle, TypeORM, Prisma (driver adapter), Kysely, pg or mysql2
|
|
5
|
+
metadata:
|
|
6
|
+
version: "0.1.0"
|
|
7
|
+
nestjs-major: "12"
|
|
8
|
+
docs-crawled: "2026-11-05"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# NestJS webhooks (@nestjs/webhooks)
|
|
12
|
+
|
|
13
|
+
Don't hand-roll HMAC signing, retry loops or `crypto.timingSafeEqual` checks when `@nestjs/webhooks` is installed or acceptable: it implements the Standard Webhooks spec, at-least-once delivery after commit, SSRF guards and an inbox for exactly-once processing.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm i @nestjs/webhooks @nestjs/outbox
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Setup (outgoing + incoming)
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
@Module({
|
|
23
|
+
imports: [
|
|
24
|
+
DrizzleModule.forRootAsync({ useFactory: () => ({ drizzle, connection: process.env.DATABASE_URL!, schema }) }),
|
|
25
|
+
OutboxModule.forRootAsync({ useFactory: () => ({ relay: { enabled: process.env.OUTBOX_RELAY !== 'off', pollInterval: '1s' } }) }),
|
|
26
|
+
WebhooksModule.forRootAsync({
|
|
27
|
+
useFactory: () => ({
|
|
28
|
+
eventTypes: ['order.shipped', 'order.cancelled'], // dispatch() refuses others
|
|
29
|
+
retry: { attempts: 10, backoff: { delay: '5s', factor: 4, maxDelay: '1d' } },
|
|
30
|
+
worker: { enabled: process.env.WEBHOOKS_WORKER !== 'off' },
|
|
31
|
+
encryption: { keys: process.env.WEBHOOKS_ENCRYPTION_KEYS!.split(',') }, // seal endpoint secrets
|
|
32
|
+
receivers: {
|
|
33
|
+
payments: { scheme: 'standard', secret: process.env.PAYMENTS_WEBHOOK_SECRET! },
|
|
34
|
+
carrier: { scheme: 'stripe', header: 'Carrier-Signature', secret: process.env.CARRIER_WEBHOOK_SECRET! },
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
providers: [
|
|
40
|
+
{ provide: PostgresOutboxStore, inject: [getDrizzleToken(), OutboxStorage],
|
|
41
|
+
useFactory: (db, s: OutboxStorage) => new PostgresOutboxStore({ executor: fromDrizzle(db) }, s) },
|
|
42
|
+
{ provide: PostgresWebhookStore, inject: [getDrizzleToken(), WebhooksStorage],
|
|
43
|
+
useFactory: (db, s: WebhooksStorage) => new PostgresWebhookStore({ executor: fromDrizzle(db) }, s) },
|
|
44
|
+
],
|
|
45
|
+
})
|
|
46
|
+
export class AppModule {}
|
|
47
|
+
|
|
48
|
+
// main.ts
|
|
49
|
+
const app = await NestFactory.create(AppModule, { rawBody: true }); // required to verify incoming webhooks
|
|
50
|
+
app.enableShutdownHooks();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- `WebhooksModule` requires `OutboxModule`; startup fails without it.
|
|
54
|
+
- Stores create and migrate their own schemas (`nest_webhooks`, `nest_outbox`); no entities to write. With `NODE_ENV=production` they don't auto-migrate (run `npx @nestjs/webhooks migrate` and `npx @nestjs/outbox migrate`, check with `status` in CI) and startup fails with no store registered.
|
|
55
|
+
- Import `fromDrizzle`/`fromTypeOrm`/`fromPrisma`/`fromKysely`/`fromPg` from the store's subpath (`@nestjs/webhooks/postgres` or `/mysql`).
|
|
56
|
+
- Load secrets through validated config (nestjs-configuration) in real code.
|
|
57
|
+
|
|
58
|
+
## Send: dispatch inside the transaction
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
constructor(@InjectDrizzle() private readonly db: Database, private readonly webhooks: Webhooks<Transaction>) {}
|
|
62
|
+
|
|
63
|
+
await this.db.transaction(async (tx) => {
|
|
64
|
+
await tx.update(orders).set({ status: 'cancelled' }).where(eq(orders.id, id));
|
|
65
|
+
await this.webhooks.dispatch(tx, { type: 'order.cancelled', tenant: partnerId, data: { orderId: id, reason } });
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- Pass the **transaction** (Drizzle `tx`, TypeORM `EntityManager` from `dataSource.transaction()`, Prisma interactive tx client), never the database: rollback means nothing is sent.
|
|
70
|
+
- Delivery is **at least once and unordered**; receivers dedupe by `webhook-id`. Payload `timestamp` is dispatch time.
|
|
71
|
+
- Version payloads: only add fields; for breaking changes add `order.shipped.v2` and dispatch both.
|
|
72
|
+
|
|
73
|
+
## Let partners subscribe and inspect deliveries
|
|
74
|
+
|
|
75
|
+
Inject `WebhookEndpoints` (`create({ url, eventTypes, tenant })`, `list`, `update`, `delete`, `getSecret`, `rotateSecret(id, { tenant, overlap })`) and `WebhookDeliveries` (`list`, `get`, `retry(idOrFilter, scope)`, `stats()`, `prune('30d')`). Always pass the caller's tenant as scope. Map `WebhooksError` (has `status`) to HTTP responses with a small `BaseExceptionFilter` subclass.
|
|
76
|
+
|
|
77
|
+
## Receive: verify, dedupe, process once
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
@Post() @HttpCode(HttpStatus.OK)
|
|
81
|
+
@VerifyWebhook('payments')
|
|
82
|
+
async receive(@IncomingWebhook() webhook: IncomingWebhook<PaymentEvent>) {
|
|
83
|
+
if (webhook.payload.type !== 'payment.succeeded') return;
|
|
84
|
+
const result = await this.db.transaction((tx) =>
|
|
85
|
+
webhook.processInTransaction(tx, () => this.orders.markPaid(tx, webhook.payload.data.orderId)));
|
|
86
|
+
if (result.duplicate) return;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- `@VerifyWebhook()` answers 401 on bad signatures (400 for non-JSON); missing `rawBody: true` → 500.
|
|
91
|
+
- GitHub (`scheme: 'github'`) signs no timestamp or id: set `id: (payload) => sha256(JSON.stringify(payload))` so replays dedupe.
|
|
92
|
+
- During a sender's rotation, list both secrets: `secret: [newSecret, oldSecret]`.
|
|
93
|
+
- A service that only receives: `WebhooksModule.forRootAsync({ outgoing: false, useFactory: () => ({ receivers }) })` plus `OutboxModule.forRoot({ relay: { enabled: false } })`. The outbox inbox does the deduplication, so register the outbox store on your database in production; no webhook store is needed.
|
|
94
|
+
|
|
95
|
+
## Gotchas
|
|
96
|
+
|
|
97
|
+
- Leave `delivery.allowHttp` / `allowPrivateNetworks` off outside development; partner URLs are untrusted (SSRF). Use `allowedAddresses` for a specific private range.
|
|
98
|
+
- Run the worker in a few instances (`worker.enabled: false` elsewhere), not every API pod.
|
|
99
|
+
- PostgreSQL default isolation must stay READ COMMITTED; the stores refuse stricter defaults.
|
|
100
|
+
- Nothing prunes the log for you: schedule `deliveries.prune('30d')` and `OutboxInbox.prune()` (nestjs-scheduling).
|
|
101
|
+
- Rotate endpoint secrets with the default 24h overlap; `overlap: 0` only after a leak.
|
|
102
|
+
|
|
103
|
+
## Workflow
|
|
104
|
+
|
|
105
|
+
- [ ] 1. Install both packages; register Drizzle/TypeORM/Prisma, `OutboxModule`, `WebhooksModule`, and both stores.
|
|
106
|
+
- [ ] 2. Set `rawBody: true` and `enableShutdownHooks()` in `main.ts` (and in e2e `createNestApplication({ rawBody: true })`).
|
|
107
|
+
- [ ] 3. Dispatch inside the transaction that changes state; declare every type in `eventTypes`.
|
|
108
|
+
- [ ] 4. For incoming webhooks, add a receiver per sender and `@VerifyWebhook()` + `processInTransaction()`.
|
|
109
|
+
- [ ] 5. E2E test with PGlite, `InMemoryWebhookTransport`, relay/worker off and driven by `runOnce()`, `signWebhook()` for incoming (see reference).
|
|
110
|
+
- [ ] 6. Build, lint, run unit and e2e tests, start the app.
|
|
111
|
+
|
|
112
|
+
## Reference files
|
|
113
|
+
|
|
114
|
+
- `references/webhooks-reference.md`: module and receiver options, store executors and migrations, services, testing recipe, events, errors, custom transports and production checklist. Read when configuring options beyond the example, writing tests, or preparing production.
|
|
115
|
+
|
|
116
|
+
Sources: [Webhooks](https://docs.nestjs.com/http/webhooks)
|