opencode-andev-rules 1.0.0 → 1.0.2
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/LICENSE +21 -0
- package/README.md +47 -37
- package/package.json +28 -14
- package/skills/backend/nestJS/SKILL.md +255 -0
- package/skills/backend/nestJS/references/nestjs-module.md +212 -0
- package/skills/backend/nestJS/references/nestjs-testing.md +395 -0
- package/skills/infrastructure/prisma/SKILL.md +65 -0
- package/skills/infrastructure/prisma/references/prisma-queries.md +197 -0
- package/skills/infrastructure/prisma/references/prisma-schema.md +126 -0
- package/skills/registry.json +46 -4
- package/RULES.md +0 -253
- package/plugin.js +0 -96
- /package/skills/{improve-codenase-systems → enhance/improve-codenase-systems}/HTML-REPORT.md +0 -0
- /package/skills/{improve-codenase-systems → enhance/improve-codenase-systems}/SKILL.md +0 -0
- /package/skills/{improve-codenase-systems → enhance/improve-codenase-systems}/agents/openai.yaml +0 -0
- /package/skills/{improve-codenase-systems → enhance/improve-codenase-systems}/meta.json +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 thaiannguyen-05
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,56 +1,66 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opencode-andev-rules
|
|
2
2
|
|
|
3
|
-
Global coding rules for TypeScript & NestJS
|
|
3
|
+
Global coding rules for TypeScript & NestJS + AI agent skills.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://github.com/thaiannguyen-05/rules-for-global-agents/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/opencode-andev-rules)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
opencode plugin @thaiannguyen-05/opencode-rules
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
hoặc:
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
cd ~/.config/opencode
|
|
15
|
-
npm install @thaiannguyen-05/opencode-rules
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
Thêm vào `opencode.json`:
|
|
8
|
+
## Cài đặt
|
|
19
9
|
|
|
20
|
-
|
|
21
|
-
{
|
|
22
|
-
"plugins": ["@thaiannguyen-05/opencode-rules"]
|
|
23
|
-
}
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Cài Skill
|
|
10
|
+
### Cách 1 — npm (khuyên dùng)
|
|
27
11
|
|
|
28
12
|
```bash
|
|
29
|
-
|
|
13
|
+
npm install opencode-andev-rules
|
|
14
|
+
# skills nằm ở node_modules/opencode-andev-rules/skills/
|
|
30
15
|
```
|
|
31
16
|
|
|
32
|
-
|
|
17
|
+
### Cách 2 — copy thủ công
|
|
33
18
|
|
|
34
19
|
```bash
|
|
35
|
-
cp -r skills/improve-codenase-systems ~/.agents/skills/
|
|
20
|
+
cp -r skills/enhance/improve-codenase-systems ~/.agents/skills/
|
|
21
|
+
cp -r skills/backend/nestJS ~/.agents/skills/
|
|
22
|
+
cp -r skills/infrastructure/prisma ~/.agents/skills/
|
|
36
23
|
```
|
|
37
24
|
|
|
38
|
-
##
|
|
39
|
-
|
|
40
|
-
### Rules
|
|
25
|
+
## Skills
|
|
41
26
|
|
|
42
|
-
|
|
27
|
+
| Skill | Đường dẫn | Dùng khi |
|
|
28
|
+
|-------|-----------|----------|
|
|
29
|
+
| `improve-codenase-systems` | `skills/enhance/improve-codenase-systems/` | Muốn scan codebase tìm vấn đề kiến trúc — nói "scan codebase" hoặc "improve codebase" trong prompt |
|
|
30
|
+
| `nestjs` | `skills/backend/nestJS/` | Viết/review code NestJS — module structure, Fastify, zod validation, DI |
|
|
31
|
+
| `prisma` | `skills/infrastructure/prisma/` | Viết/review Prisma — client singleton, queries, transactions, migrations |
|
|
43
32
|
|
|
44
|
-
|
|
33
|
+
Chi tiết từng skill xem file `SKILL.md` trong thư mục tương ứng. Danh mục đầy đủ: `skills/registry.json`.
|
|
45
34
|
|
|
46
|
-
|
|
35
|
+
## Cấu trúc repo
|
|
47
36
|
|
|
48
|
-
|
|
37
|
+
```
|
|
38
|
+
skills/
|
|
39
|
+
├── backend/nestJS/ # rules NestJS
|
|
40
|
+
│ ├── SKILL.md
|
|
41
|
+
│ └── references/nestjs-module.md
|
|
42
|
+
├── infrastructure/prisma/ # rules Prisma
|
|
43
|
+
│ ├── SKILL.md
|
|
44
|
+
│ └── references/
|
|
45
|
+
├── enhance/improve-codenase-systems/ # skill scan kiến trúc
|
|
46
|
+
│ ├── SKILL.md
|
|
47
|
+
│ ├── HTML-REPORT.md
|
|
48
|
+
│ ├── meta.json
|
|
49
|
+
│ └── agents/openai.yaml
|
|
50
|
+
└── registry.json # danh mục skills (version top-level sync với package.json)
|
|
51
|
+
scripts/
|
|
52
|
+
├── validate.sh # kiểm tra frontmatter SKILL.md + registry + npm pack --dry-run
|
|
53
|
+
├── check-version.sh # kiểm tra version chưa tồn tại trên npm
|
|
54
|
+
└── release.sh # bump version an toàn (xem dưới)
|
|
55
|
+
.github/workflows/
|
|
56
|
+
├── ci.yml # 3 jobs async song song (frontmatter/registry/pack) trên mỗi push/PR
|
|
57
|
+
└── publish.yml # nút Run workflow: tự bump version + publish lên npm
|
|
58
|
+
```
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
## Đóng góp
|
|
51
61
|
|
|
52
|
-
|
|
62
|
+
1. Sửa/thêm skill trong `skills/`, mỗi skill phải có `SKILL.md` với frontmatter `name:` + `description:`.
|
|
63
|
+
2. Khai báo skill mới vào `skills/registry.json`.
|
|
64
|
+
3. Chạy `npm run validate` trước khi push.
|
|
65
|
+
4. **Không sửa version bằng tay** — version do workflow publish tự tăng (xem dưới).
|
|
53
66
|
|
|
54
|
-
| Skill | Description |
|
|
55
|
-
|-------|-------------|
|
|
56
|
-
| `improve-codenase-systems` | Scan codebase for architecture issues |
|
package/package.json
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-andev-rules",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Global coding rules for TypeScript & NestJS + AI agent skills.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/thaiannguyen-05/rules-for-global-agents.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/thaiannguyen-05/rules-for-global-agents/issues"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/thaiannguyen-05/rules-for-global-agents#readme",
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public",
|
|
15
|
+
"provenance": true
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"skills/",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"validate": "bash scripts/validate.sh",
|
|
24
|
+
"check-version": "bash scripts/check-version.sh",
|
|
25
|
+
"release": "bash scripts/release.sh",
|
|
26
|
+
"pack": "npm pack --dry-run"
|
|
9
27
|
},
|
|
10
28
|
"keywords": [
|
|
11
|
-
"opencode",
|
|
12
|
-
"opencode-plugin",
|
|
13
29
|
"rules",
|
|
14
30
|
"nestjs",
|
|
15
|
-
"typescript"
|
|
31
|
+
"typescript",
|
|
32
|
+
"skills"
|
|
16
33
|
],
|
|
17
34
|
"author": "thaiannguyen-05",
|
|
18
35
|
"license": "MIT",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
},
|
|
22
|
-
"devDependencies": {
|
|
23
|
-
"@opencode-ai/plugin": "1.17.11"
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
24
38
|
}
|
|
25
39
|
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs
|
|
3
|
+
description: NestJS backend rules — Fastify platform, module structure, zod validation. Use when building or reviewing NestJS code.
|
|
4
|
+
---
|
|
5
|
+
# NestJS Architecture
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
8
|
+
Structure NestJS applications with module-based architecture — domain modules, shared modules, dynamic modules, monorepo projects, and provider scoping.
|
|
9
|
+
|
|
10
|
+
## Agent Protocol
|
|
11
|
+
|
|
12
|
+
### Trigger
|
|
13
|
+
User request includes: `NestJS module structure`, `NestJS architecture`, `NestJS project layout`, `NestJS DI`, `NestJS modules`, `NestJS monorepo`, `NestJS providers`.
|
|
14
|
+
|
|
15
|
+
### Input Context
|
|
16
|
+
- NestJS version (10+)
|
|
17
|
+
- Architecture style (modular, monorepo, microservices)
|
|
18
|
+
- Database (TypeORM, Prisma, MikroORM, Mongoose)
|
|
19
|
+
- Transport (HTTP, WebSocket, TCP, RabbitMQ)
|
|
20
|
+
|
|
21
|
+
### Output Artifact
|
|
22
|
+
Module structure, provider registration, module exports, monorepo layout, DI setup.
|
|
23
|
+
|
|
24
|
+
### Response Format
|
|
25
|
+
Produce artifact directly. No preamble, no postamble, no explanations.
|
|
26
|
+
|
|
27
|
+
### Completion Criteria
|
|
28
|
+
- Root module imports feature modules
|
|
29
|
+
- Each domain has its own module with controllers, services, repositories
|
|
30
|
+
- Modules export only what other modules need
|
|
31
|
+
- Global modules registered sparingly (shared infra only)
|
|
32
|
+
- Dynamic modules configured with forRoot/forFeature
|
|
33
|
+
|
|
34
|
+
### Max Response Length
|
|
35
|
+
4096 tokens
|
|
36
|
+
|
|
37
|
+
# NestJS Skill
|
|
38
|
+
|
|
39
|
+
## Platform
|
|
40
|
+
|
|
41
|
+
- Use Fastify — never Express.
|
|
42
|
+
- Bootstrap with `FastifyAdapter` typed as `NestFastifyApplication` — never `NestFactory.create()` with the default Express platform.
|
|
43
|
+
- Pass `{ trustProxy: true }` so client IPs stay correct behind a reverse proxy.
|
|
44
|
+
- Never use Express-specific APIs (`@Res()` passthrough, `express-*` middleware) — use Fastify equivalents and `app.register()` plugins.
|
|
45
|
+
- Configure CORS, body-parser limits, and global prefix on the Fastify instance in `main.ts`.
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// main.ts
|
|
49
|
+
import { NestFactory } from '@nestjs/core';
|
|
50
|
+
import {
|
|
51
|
+
FastifyAdapter,
|
|
52
|
+
NestFastifyApplication,
|
|
53
|
+
} from '@nestjs/platform-fastify';
|
|
54
|
+
|
|
55
|
+
const app = await NestFactory.create<NestFastifyApplication>(
|
|
56
|
+
AppModule,
|
|
57
|
+
new FastifyAdapter({ trustProxy: true }),
|
|
58
|
+
);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## common/guards
|
|
62
|
+
|
|
63
|
+
- Put only shared guards in `src/common/guards/` — auth, ownership checks reused across features.
|
|
64
|
+
- Feature-specific guards belong in `module/{features}/guards/` — never in `common`.
|
|
65
|
+
- Guards decide access only — no business logic, no DB writes inside guards.
|
|
66
|
+
- Extract request props into explicitly typed locals (e.g., `const user: User | undefined = req.user`) — never branch on `req.*` inline; untyped access is unsafe.
|
|
67
|
+
- Apply via `@UseGuards()` — never call guards manually in controllers or services.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// common/guards/auth.guard.ts
|
|
71
|
+
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
|
72
|
+
import { Reflector } from '@nestjs/core';
|
|
73
|
+
import { FastifyRequest } from 'fastify';
|
|
74
|
+
import { User } from '../type';
|
|
75
|
+
import { IS_PUBLIC_KEY } from '../decorator/public.decorator';
|
|
76
|
+
|
|
77
|
+
@Injectable()
|
|
78
|
+
export class AuthGuard implements CanActivate {
|
|
79
|
+
constructor(private readonly reflector: Reflector) {}
|
|
80
|
+
|
|
81
|
+
canActivate(context: ExecutionContext): boolean {
|
|
82
|
+
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
|
83
|
+
context.getHandler(),
|
|
84
|
+
context.getClass(),
|
|
85
|
+
]);
|
|
86
|
+
if (isPublic) return true;
|
|
87
|
+
const req = context.switchToHttp().getRequest<FastifyRequest>();
|
|
88
|
+
const user: User | undefined = req.user;
|
|
89
|
+
if (!user) throw new UnauthorizedException();
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// usage: @UseGuards(AuthGuard)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## common/interceptor
|
|
98
|
+
|
|
99
|
+
- Put only shared interceptors in `src/common/interceptor/` — logging, transformation, timeout.
|
|
100
|
+
- Never put business logic inside interceptors.
|
|
101
|
+
- Keep interceptors thin — one responsibility per interceptor.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// common/interceptor/logging.interceptor.ts
|
|
105
|
+
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
|
|
106
|
+
import { FastifyRequest } from 'fastify';
|
|
107
|
+
import { Observable, tap } from 'rxjs';
|
|
108
|
+
|
|
109
|
+
@Injectable()
|
|
110
|
+
export class LoggingInterceptor implements NestInterceptor {
|
|
111
|
+
private readonly logger = new Logger('HTTP');
|
|
112
|
+
|
|
113
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
|
114
|
+
const req = context.switchToHttp().getRequest<FastifyRequest>();
|
|
115
|
+
const start = Date.now();
|
|
116
|
+
return next.handle().pipe(
|
|
117
|
+
tap(() => this.logger.log(`${req.method} ${req.url} ${Date.now() - start}ms`)),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## common/decorator
|
|
124
|
+
|
|
125
|
+
- Put only shared decorators in `src/common/decorator/` — param decorators, metadata helpers.
|
|
126
|
+
- Mark public routes with `@Public()` — AuthGuard skips them via Reflector; never bypass the guard manually.
|
|
127
|
+
- Never put validation logic inside decorators that belongs in `dto/`.
|
|
128
|
+
- Never put business logic inside decorators.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// common/decorator/current-user.decorator.ts
|
|
132
|
+
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
|
133
|
+
import { FastifyRequest } from 'fastify';
|
|
134
|
+
import { User } from '../type';
|
|
135
|
+
|
|
136
|
+
export const CurrentUser = createParamDecorator(
|
|
137
|
+
(key: keyof User | undefined, ctx: ExecutionContext) => {
|
|
138
|
+
const req = ctx.switchToHttp().getRequest<FastifyRequest>();
|
|
139
|
+
const user: User | undefined = req.user;
|
|
140
|
+
return key ? user?.[key] : user;
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// usage: handler(@CurrentUser() user) or handler(@CurrentUser('id') id: string)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// common/decorator/public.decorator.ts
|
|
149
|
+
import { SetMetadata } from '@nestjs/common';
|
|
150
|
+
|
|
151
|
+
export const IS_PUBLIC_KEY = 'isPublic';
|
|
152
|
+
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
|
153
|
+
|
|
154
|
+
// usage: @Public() on a controller or route handler to skip AuthGuard
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## common/exception
|
|
158
|
+
|
|
159
|
+
- All exceptions and filters live in `src/common/exception/` — replaces the old `src/error/` location.
|
|
160
|
+
- Define custom `AppException` classes here — never throw raw `Error` for domain failures.
|
|
161
|
+
- `errorCode` = uppercase snake_case (e.g., `RESOURCE_NOT_FOUND`).
|
|
162
|
+
- Always return consistent JSON: `{ statusCode, timestamp, path, message, errorCode?, details? }`.
|
|
163
|
+
- Log unhandled (non-HttpException) errors with logger — never leak internal details to responses.
|
|
164
|
+
- Feature helpers (e.g., `[domain]-error.helper.ts`) live here, not inside features.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// common/exception/app.exception.ts
|
|
168
|
+
import { HttpException, HttpStatus } from '@nestjs/common';
|
|
169
|
+
|
|
170
|
+
export class AppException extends HttpException {
|
|
171
|
+
constructor(
|
|
172
|
+
message: string,
|
|
173
|
+
statusCode: HttpStatus,
|
|
174
|
+
public readonly errorCode?: string,
|
|
175
|
+
public readonly details?: unknown,
|
|
176
|
+
) {
|
|
177
|
+
super({ message, errorCode, details, timestamp: new Date().toISOString() }, statusCode);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class ResourceNotFoundException extends AppException {
|
|
182
|
+
constructor(resourceType: string, identifier: string | number) {
|
|
183
|
+
super(
|
|
184
|
+
`${resourceType} with identifier ${identifier} not found`,
|
|
185
|
+
HttpStatus.NOT_FOUND,
|
|
186
|
+
'RESOURCE_NOT_FOUND',
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
// common/exception/global-exception.filter.ts
|
|
194
|
+
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from '@nestjs/common';
|
|
195
|
+
import { FastifyReply } from 'fastify';
|
|
196
|
+
|
|
197
|
+
@Catch()
|
|
198
|
+
export class GlobalExceptionFilter implements ExceptionFilter {
|
|
199
|
+
private readonly logger = new Logger('Exception');
|
|
200
|
+
|
|
201
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
202
|
+
const reply = host.switchToHttp().getResponse<FastifyReply>();
|
|
203
|
+
if (exception instanceof HttpException) {
|
|
204
|
+
return reply.status(exception.getStatus()).send(exception.getResponse());
|
|
205
|
+
}
|
|
206
|
+
this.logger.error(exception);
|
|
207
|
+
return reply.status(500).send({
|
|
208
|
+
statusCode: 500,
|
|
209
|
+
message: 'Internal server error',
|
|
210
|
+
timestamp: new Date().toISOString(),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// main.ts: app.useGlobalFilters(new GlobalExceptionFilter());
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## common/middleware
|
|
219
|
+
|
|
220
|
+
- Put only HTTP plumbing in `src/common/middleware/` — req/res preprocessing, correlation IDs.
|
|
221
|
+
- Write them as Fastify plugins/hooks registered via `app.register()` — never Express-style `(req, res, next)` middleware.
|
|
222
|
+
- Never put business logic inside middleware.
|
|
223
|
+
- Prefer guards/interceptors over middleware when the concern fits them.
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// common/middleware/correlation-id.plugin.ts
|
|
227
|
+
import { FastifyInstance } from 'fastify';
|
|
228
|
+
import { randomUUID } from 'node:crypto';
|
|
229
|
+
|
|
230
|
+
export async function correlationIdPlugin(app: FastifyInstance) {
|
|
231
|
+
app.addHook('onRequest', async (req) => {
|
|
232
|
+
req.headers['x-correlation-id'] ??= randomUUID();
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// main.ts: await app.register(correlationIdPlugin);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## module/infrastructures (abstract)
|
|
240
|
+
|
|
241
|
+
- `module/infrastructures/` holds tech adapters only — no business logic, no domain types.
|
|
242
|
+
- Subfolders below are examples, not mandates — never hardcode the tech choice:
|
|
243
|
+
- `TypeOrm/` = example relational persistence adapter (entities, repositories, migrations glue).
|
|
244
|
+
- `message/` = example messaging adapter (producers, consumers, topic/queue names).
|
|
245
|
+
- Name a subfolder after the adapter role — one folder per external system.
|
|
246
|
+
- Adapters implement interfaces owned by features — features depend on abstractions, not on adapter internals.
|
|
247
|
+
- Never import `module/{features}/` code into `infrastructures/` — dependency points inward to the abstraction.
|
|
248
|
+
- Never let controllers or services import adapter internals directly — go through DI providers/tokens.
|
|
249
|
+
- Never share state between adapters — no cross-imports between `TypeOrm/` and `message/`.
|
|
250
|
+
- Write adapter code only after the tech is chosen — no placeholder implementations before the decision.
|
|
251
|
+
|
|
252
|
+
## References
|
|
253
|
+
|
|
254
|
+
- Feature slice layout, types, guards, dto, service, controller rules: `references/nestjs-module.md`.
|
|
255
|
+
- Unit, integration, E2E testing strategies, TestContainers, mocking: `references/nestjs-testing.md`.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# NestJS Module Structure Rules
|
|
2
|
+
|
|
3
|
+
## Layout
|
|
4
|
+
|
|
5
|
+
- Follow this tree exactly — no extra nesting, no renamed folders.
|
|
6
|
+
- `src/common/` holds shared code only — never feature-specific code.
|
|
7
|
+
- `src/module/` holds `infrastructures/` + one folder per feature.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
├── common/
|
|
12
|
+
│ ├── guards/
|
|
13
|
+
│ ├── interceptor/
|
|
14
|
+
│ ├── decorator/
|
|
15
|
+
│ ├── exception/
|
|
16
|
+
│ ├── middleware/
|
|
17
|
+
│ └── type.ts
|
|
18
|
+
└── module/
|
|
19
|
+
├── infrastructures/
|
|
20
|
+
│ ├── TypeOrm/
|
|
21
|
+
│ └── message/
|
|
22
|
+
└── {features}/
|
|
23
|
+
├── dto/
|
|
24
|
+
├── guards/
|
|
25
|
+
├── type.ts
|
|
26
|
+
├── index.ts
|
|
27
|
+
├── {features}.service.ts
|
|
28
|
+
├── {features}.controller.ts
|
|
29
|
+
└── {features}.module.ts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## common/type.ts (shared)
|
|
33
|
+
|
|
34
|
+
- Put only cross-feature types in `src/common/type.ts` — enums, interfaces, aliases used by 2+ features.
|
|
35
|
+
- Never put feature-private types in `src/common/type.ts` — they belong in `module/{features}/type.ts`.
|
|
36
|
+
- Never import from `module/{features}/type.ts` into `src/common/type.ts` — `common` never depends on `module/`.
|
|
37
|
+
- Use `enum` for shared constants — no hardcoded strings in business logic.
|
|
38
|
+
|
|
39
|
+
## Global setup (lives in `../SKILL.md`)
|
|
40
|
+
|
|
41
|
+
- `common/interceptor`, `common/decorator`, `common/exception`, `common/middleware`, and `module/infrastructures/` rules are global — see `../SKILL.md`, not here.
|
|
42
|
+
|
|
43
|
+
## module/{features}
|
|
44
|
+
|
|
45
|
+
- One folder per feature (e.g., `users/`, `auth/`, `orders/`) — self-contained slice.
|
|
46
|
+
- Never place feature code outside its folder — except shared code in `src/common/` and adapters in `infrastructures/`.
|
|
47
|
+
- Never import deep paths across features (e.g., `../orders/type`) — import via the feature barrel (see `index.ts` below).
|
|
48
|
+
|
|
49
|
+
### dto/
|
|
50
|
+
|
|
51
|
+
- DTOs are zod schemas only — define `z.object(...)` in `dto/`, export the schema + `z.infer` type, no business logic.
|
|
52
|
+
- Do NOT use `class-validator` decorators or `class-transformer` — zod replaces both (parsing + coercion via `z.coerce`).
|
|
53
|
+
- Do NOT put shared types or constants in `dto/` — shared types belong in `type.ts`, constants in `enum`; derive DTO types via `z.infer`.
|
|
54
|
+
- Validate nested objects with nested schemas — call `.strict()` to reject unknown fields.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// module/users/dto/create-user.dto.ts
|
|
58
|
+
import { z } from 'zod';
|
|
59
|
+
|
|
60
|
+
export const CreateUserSchema = z.object({
|
|
61
|
+
name: z.string().min(1),
|
|
62
|
+
email: z.string().email(),
|
|
63
|
+
address: AddressSchema,
|
|
64
|
+
}).strict();
|
|
65
|
+
|
|
66
|
+
export type CreateUserDto = z.infer<typeof CreateUserSchema>;
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### guards/
|
|
70
|
+
|
|
71
|
+
- Put feature-specific guards here — guards used by one feature only.
|
|
72
|
+
- Promote to `src/common/guards/` the moment a second feature needs it.
|
|
73
|
+
- Same guard rules as global (`../SKILL.md`): access decisions only, applied via `@UseGuards()`.
|
|
74
|
+
|
|
75
|
+
### type.ts + index.ts (private + barrel)
|
|
76
|
+
|
|
77
|
+
- `module/{features}/type.ts` holds feature-private types — enums, interfaces, return types for this feature only.
|
|
78
|
+
- Every feature must re-export its private types from its own `index.ts` barrel.
|
|
79
|
+
- External features import via the barrel — never via a deep `type.ts` path.
|
|
80
|
+
- Internal files import from `./type` — avoid self-importing through `./index` to prevent cycles.
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// module/users/type.ts
|
|
84
|
+
export enum UserRole { ADMIN = 'admin', USER = 'user' }
|
|
85
|
+
export interface User { id: string; name: string; }
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// module/users/index.ts
|
|
90
|
+
export * from './type';
|
|
91
|
+
export * from './users.service';
|
|
92
|
+
export * from './users.module';
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### {features}.service.ts
|
|
96
|
+
|
|
97
|
+
- Services hold business logic only — no HTTP, no req/res, no raw adapter imports.
|
|
98
|
+
- Inject dependencies via constructor — never instantiate manually.
|
|
99
|
+
- Run independent async work in parallel — never `await` sequentially when parallel is possible.
|
|
100
|
+
- Use transactions for multi-step writes — no partial writes, keep scope small, no external calls inside.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
// module/users/users.service.ts
|
|
104
|
+
import { Injectable } from '@nestjs/common';
|
|
105
|
+
import type { CreateUserDto } from './dto/create-user.dto';
|
|
106
|
+
import type { User } from './type';
|
|
107
|
+
|
|
108
|
+
@Injectable()
|
|
109
|
+
export class UsersService {
|
|
110
|
+
async create(dto: CreateUserDto): Promise<User> {
|
|
111
|
+
/* business logic only — persistence arrives via DI after the tech decision */
|
|
112
|
+
void dto;
|
|
113
|
+
throw new Error('not implemented');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async getById(id: string): Promise<User> {
|
|
117
|
+
/* ... */
|
|
118
|
+
void id;
|
|
119
|
+
throw new Error('not implemented');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### {features}.controller.ts
|
|
125
|
+
|
|
126
|
+
- Controllers handle HTTP only — routing, status codes, delegation to the service.
|
|
127
|
+
- No business logic, no DB access, no adapter imports in controllers.
|
|
128
|
+
- Controllers and services receive only valid data — all validation happens in `dto/` schemas + global `ZodValidationPipe` before them.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// module/users/users.controller.ts
|
|
132
|
+
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
|
133
|
+
import type { CreateUserDto } from './dto/create-user.dto';
|
|
134
|
+
import { UsersService } from './users.service';
|
|
135
|
+
|
|
136
|
+
@Controller('users')
|
|
137
|
+
export class UsersController {
|
|
138
|
+
constructor(private readonly users: UsersService) {}
|
|
139
|
+
|
|
140
|
+
@Post()
|
|
141
|
+
create(@Body() dto: CreateUserDto) {
|
|
142
|
+
return this.users.create(dto);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@Get(':id')
|
|
146
|
+
getById(@Param('id') id: string) {
|
|
147
|
+
return this.users.getById(id);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### {features}.module.ts
|
|
153
|
+
|
|
154
|
+
- Modules wire only — `imports`, `providers`, `controllers`, `exports`, no logic.
|
|
155
|
+
- Register feature providers here; export what other features need via the barrel.
|
|
156
|
+
- Import direction is one-way — the sample below depends on users, never the reverse.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// module/users/users.module.ts
|
|
160
|
+
import { Module } from '@nestjs/common';
|
|
161
|
+
import { UsersController } from './users.controller';
|
|
162
|
+
import { UsersService } from './users.service';
|
|
163
|
+
|
|
164
|
+
@Module({
|
|
165
|
+
controllers: [UsersController],
|
|
166
|
+
providers: [UsersService],
|
|
167
|
+
exports: [UsersService],
|
|
168
|
+
})
|
|
169
|
+
export class UsersModule {}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// module/orders/orders.module.ts — depends on users, never the reverse
|
|
174
|
+
import { Module } from '@nestjs/common';
|
|
175
|
+
import { UsersModule } from '../users';
|
|
176
|
+
import { OrdersController } from './orders.controller';
|
|
177
|
+
import { OrdersService } from './orders.service';
|
|
178
|
+
|
|
179
|
+
@Module({
|
|
180
|
+
imports: [UsersModule],
|
|
181
|
+
controllers: [OrdersController],
|
|
182
|
+
providers: [OrdersService],
|
|
183
|
+
})
|
|
184
|
+
export class OrdersModule {}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Cross-module imports (no circular DI)
|
|
188
|
+
|
|
189
|
+
- Keep the import graph one-way — if A imports B, B never imports A.
|
|
190
|
+
- Code needed by both sides moves to a third module (or `src/common/`) that both import.
|
|
191
|
+
- Import the dependency's barrel (`../users`), never reach into its internals.
|
|
192
|
+
- `forwardRef` only as last resort — wrap both module imports and both injections, and comment why the cycle cannot be broken.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// module/orders/orders.service.ts — consumes the exported provider, no cycle
|
|
196
|
+
import { Injectable } from '@nestjs/common';
|
|
197
|
+
import { UsersService } from '../users';
|
|
198
|
+
|
|
199
|
+
@Injectable()
|
|
200
|
+
export class OrdersService {
|
|
201
|
+
constructor(private readonly users: UsersService) {}
|
|
202
|
+
|
|
203
|
+
async create(userId: string) {
|
|
204
|
+
const user = await this.users.getById(userId);
|
|
205
|
+
/* ... */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// last resort only — avoid if possible:
|
|
210
|
+
// @Module({ imports: [forwardRef(() => BModule)] }) on BOTH modules, and
|
|
211
|
+
// constructor(@Inject(forwardRef(() => BService)) ...) on BOTH sides, with a comment why.
|
|
212
|
+
```
|