@malamute/ai-rules 1.0.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/README.md +174 -0
- package/bin/cli.js +5 -0
- package/configs/_shared/.claude/commands/fix-issue.md +38 -0
- package/configs/_shared/.claude/commands/generate-tests.md +49 -0
- package/configs/_shared/.claude/commands/review-pr.md +77 -0
- package/configs/_shared/.claude/rules/accessibility.md +270 -0
- package/configs/_shared/.claude/rules/performance.md +226 -0
- package/configs/_shared/.claude/rules/security.md +188 -0
- package/configs/_shared/.claude/skills/debug/SKILL.md +118 -0
- package/configs/_shared/.claude/skills/learning/SKILL.md +224 -0
- package/configs/_shared/.claude/skills/review/SKILL.md +86 -0
- package/configs/_shared/.claude/skills/spec/SKILL.md +112 -0
- package/configs/_shared/CLAUDE.md +174 -0
- package/configs/angular/.claude/rules/components.md +257 -0
- package/configs/angular/.claude/rules/state.md +250 -0
- package/configs/angular/.claude/rules/testing.md +422 -0
- package/configs/angular/.claude/settings.json +31 -0
- package/configs/angular/CLAUDE.md +251 -0
- package/configs/dotnet/.claude/rules/api.md +370 -0
- package/configs/dotnet/.claude/rules/architecture.md +199 -0
- package/configs/dotnet/.claude/rules/database/efcore.md +408 -0
- package/configs/dotnet/.claude/rules/testing.md +389 -0
- package/configs/dotnet/.claude/settings.json +9 -0
- package/configs/dotnet/CLAUDE.md +319 -0
- package/configs/nestjs/.claude/rules/auth.md +321 -0
- package/configs/nestjs/.claude/rules/database/prisma.md +305 -0
- package/configs/nestjs/.claude/rules/database/typeorm.md +379 -0
- package/configs/nestjs/.claude/rules/modules.md +215 -0
- package/configs/nestjs/.claude/rules/testing.md +315 -0
- package/configs/nestjs/.claude/rules/validation.md +279 -0
- package/configs/nestjs/.claude/settings.json +15 -0
- package/configs/nestjs/CLAUDE.md +263 -0
- package/configs/nextjs/.claude/rules/components.md +211 -0
- package/configs/nextjs/.claude/rules/state/redux-toolkit.md +429 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +299 -0
- package/configs/nextjs/.claude/rules/testing.md +315 -0
- package/configs/nextjs/.claude/settings.json +29 -0
- package/configs/nextjs/CLAUDE.md +376 -0
- package/configs/python/.claude/rules/database/sqlalchemy.md +355 -0
- package/configs/python/.claude/rules/fastapi.md +272 -0
- package/configs/python/.claude/rules/flask.md +332 -0
- package/configs/python/.claude/rules/testing.md +374 -0
- package/configs/python/.claude/settings.json +18 -0
- package/configs/python/CLAUDE.md +273 -0
- package/package.json +41 -0
- package/src/install.js +315 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.entity.ts"
|
|
4
|
+
- "src/**/*.repository.ts"
|
|
5
|
+
- "src/**/typeorm*.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# NestJS with TypeORM
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
### TypeORM Module
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
// app.module.ts
|
|
16
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
17
|
+
|
|
18
|
+
@Module({
|
|
19
|
+
imports: [
|
|
20
|
+
TypeOrmModule.forRootAsync({
|
|
21
|
+
inject: [ConfigService],
|
|
22
|
+
useFactory: (config: ConfigService) => ({
|
|
23
|
+
type: 'postgres',
|
|
24
|
+
host: config.getOrThrow('DB_HOST'),
|
|
25
|
+
port: config.get('DB_PORT', 5432),
|
|
26
|
+
username: config.getOrThrow('DB_USER'),
|
|
27
|
+
password: config.getOrThrow('DB_PASSWORD'),
|
|
28
|
+
database: config.getOrThrow('DB_NAME'),
|
|
29
|
+
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
|
30
|
+
synchronize: config.get('NODE_ENV') !== 'production',
|
|
31
|
+
logging: config.get('NODE_ENV') === 'development',
|
|
32
|
+
}),
|
|
33
|
+
}),
|
|
34
|
+
],
|
|
35
|
+
})
|
|
36
|
+
export class AppModule {}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Entity Design
|
|
40
|
+
|
|
41
|
+
### Entity Conventions
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import {
|
|
45
|
+
Entity,
|
|
46
|
+
PrimaryGeneratedColumn,
|
|
47
|
+
Column,
|
|
48
|
+
CreateDateColumn,
|
|
49
|
+
UpdateDateColumn,
|
|
50
|
+
ManyToOne,
|
|
51
|
+
OneToMany,
|
|
52
|
+
JoinColumn,
|
|
53
|
+
Index,
|
|
54
|
+
} from 'typeorm';
|
|
55
|
+
|
|
56
|
+
@Entity('users')
|
|
57
|
+
export class User {
|
|
58
|
+
@PrimaryGeneratedColumn('uuid')
|
|
59
|
+
id: string;
|
|
60
|
+
|
|
61
|
+
@Column({ unique: true })
|
|
62
|
+
@Index()
|
|
63
|
+
email: string;
|
|
64
|
+
|
|
65
|
+
@Column()
|
|
66
|
+
password: string;
|
|
67
|
+
|
|
68
|
+
@Column({ nullable: true })
|
|
69
|
+
name?: string;
|
|
70
|
+
|
|
71
|
+
@Column({
|
|
72
|
+
type: 'enum',
|
|
73
|
+
enum: ['user', 'admin'],
|
|
74
|
+
default: 'user',
|
|
75
|
+
})
|
|
76
|
+
role: 'user' | 'admin';
|
|
77
|
+
|
|
78
|
+
@CreateDateColumn({ name: 'created_at' })
|
|
79
|
+
createdAt: Date;
|
|
80
|
+
|
|
81
|
+
@UpdateDateColumn({ name: 'updated_at' })
|
|
82
|
+
updatedAt: Date;
|
|
83
|
+
|
|
84
|
+
@OneToMany(() => Post, (post) => post.author)
|
|
85
|
+
posts: Post[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Entity('posts')
|
|
89
|
+
export class Post {
|
|
90
|
+
@PrimaryGeneratedColumn('uuid')
|
|
91
|
+
id: string;
|
|
92
|
+
|
|
93
|
+
@Column()
|
|
94
|
+
title: string;
|
|
95
|
+
|
|
96
|
+
@Column({ type: 'text', nullable: true })
|
|
97
|
+
content?: string;
|
|
98
|
+
|
|
99
|
+
@Column({ default: false })
|
|
100
|
+
published: boolean;
|
|
101
|
+
|
|
102
|
+
@ManyToOne(() => User, (user) => user.posts, { onDelete: 'CASCADE' })
|
|
103
|
+
@JoinColumn({ name: 'author_id' })
|
|
104
|
+
author: User;
|
|
105
|
+
|
|
106
|
+
@Column({ name: 'author_id' })
|
|
107
|
+
@Index()
|
|
108
|
+
authorId: string;
|
|
109
|
+
|
|
110
|
+
@CreateDateColumn({ name: 'created_at' })
|
|
111
|
+
createdAt: Date;
|
|
112
|
+
|
|
113
|
+
@UpdateDateColumn({ name: 'updated_at' })
|
|
114
|
+
updatedAt: Date;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Naming Conventions
|
|
119
|
+
|
|
120
|
+
- Entities: PascalCase (`User`, `BlogPost`)
|
|
121
|
+
- Properties: camelCase (`createdAt`, `authorId`)
|
|
122
|
+
- Tables: snake_case via `@Entity('users')`
|
|
123
|
+
- Columns: snake_case via `{ name: 'created_at' }`
|
|
124
|
+
|
|
125
|
+
## Repository Pattern
|
|
126
|
+
|
|
127
|
+
### Custom Repository
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// users/users.repository.ts
|
|
131
|
+
import { Injectable } from '@nestjs/common';
|
|
132
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
133
|
+
import { Repository } from 'typeorm';
|
|
134
|
+
import { User } from './entities/user.entity';
|
|
135
|
+
|
|
136
|
+
@Injectable()
|
|
137
|
+
export class UsersRepository {
|
|
138
|
+
constructor(
|
|
139
|
+
@InjectRepository(User)
|
|
140
|
+
private readonly repository: Repository<User>,
|
|
141
|
+
) {}
|
|
142
|
+
|
|
143
|
+
async findById(id: string): Promise<User | null> {
|
|
144
|
+
return this.repository.findOne({ where: { id } });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
148
|
+
return this.repository.findOne({ where: { email } });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async findMany(options: {
|
|
152
|
+
skip?: number;
|
|
153
|
+
take?: number;
|
|
154
|
+
where?: Partial<User>;
|
|
155
|
+
}): Promise<User[]> {
|
|
156
|
+
return this.repository.find(options);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async create(data: Partial<User>): Promise<User> {
|
|
160
|
+
const user = this.repository.create(data);
|
|
161
|
+
return this.repository.save(user);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async update(id: string, data: Partial<User>): Promise<User> {
|
|
165
|
+
await this.repository.update(id, data);
|
|
166
|
+
return this.findById(id);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async delete(id: string): Promise<void> {
|
|
170
|
+
await this.repository.delete(id);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Module Registration
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// users/users.module.ts
|
|
179
|
+
import { Module } from '@nestjs/common';
|
|
180
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
181
|
+
import { User } from './entities/user.entity';
|
|
182
|
+
import { UsersRepository } from './users.repository';
|
|
183
|
+
import { UsersService } from './users.service';
|
|
184
|
+
|
|
185
|
+
@Module({
|
|
186
|
+
imports: [TypeOrmModule.forFeature([User])],
|
|
187
|
+
providers: [UsersRepository, UsersService],
|
|
188
|
+
exports: [UsersService],
|
|
189
|
+
})
|
|
190
|
+
export class UsersModule {}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Query Patterns
|
|
194
|
+
|
|
195
|
+
### QueryBuilder
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
async findWithFilters(filters: UserFilters) {
|
|
199
|
+
const qb = this.repository.createQueryBuilder('user');
|
|
200
|
+
|
|
201
|
+
if (filters.search) {
|
|
202
|
+
qb.andWhere(
|
|
203
|
+
'(user.name ILIKE :search OR user.email ILIKE :search)',
|
|
204
|
+
{ search: `%${filters.search}%` },
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (filters.role) {
|
|
209
|
+
qb.andWhere('user.role = :role', { role: filters.role });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (filters.createdAfter) {
|
|
213
|
+
qb.andWhere('user.createdAt >= :date', { date: filters.createdAfter });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return qb
|
|
217
|
+
.orderBy('user.createdAt', 'DESC')
|
|
218
|
+
.skip(filters.skip ?? 0)
|
|
219
|
+
.take(filters.take ?? 20)
|
|
220
|
+
.getMany();
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Relations
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Eager loading
|
|
228
|
+
const user = await this.repository.findOne({
|
|
229
|
+
where: { id },
|
|
230
|
+
relations: ['posts', 'profile'],
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// QueryBuilder with relations
|
|
234
|
+
const users = await this.repository
|
|
235
|
+
.createQueryBuilder('user')
|
|
236
|
+
.leftJoinAndSelect('user.posts', 'post', 'post.published = :published', {
|
|
237
|
+
published: true,
|
|
238
|
+
})
|
|
239
|
+
.where('user.role = :role', { role: 'admin' })
|
|
240
|
+
.getMany();
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Pagination
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
async findPaginated(page: number, limit: number) {
|
|
247
|
+
const [data, total] = await this.repository.findAndCount({
|
|
248
|
+
skip: (page - 1) * limit,
|
|
249
|
+
take: limit,
|
|
250
|
+
order: { createdAt: 'DESC' },
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
data,
|
|
255
|
+
meta: {
|
|
256
|
+
total,
|
|
257
|
+
page,
|
|
258
|
+
limit,
|
|
259
|
+
totalPages: Math.ceil(total / limit),
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Transactions
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
import { DataSource } from 'typeorm';
|
|
269
|
+
|
|
270
|
+
@Injectable()
|
|
271
|
+
export class TransferService {
|
|
272
|
+
constructor(private readonly dataSource: DataSource) {}
|
|
273
|
+
|
|
274
|
+
async transferCredits(fromId: string, toId: string, amount: number) {
|
|
275
|
+
return this.dataSource.transaction(async (manager) => {
|
|
276
|
+
const sender = await manager.findOne(User, { where: { id: fromId } });
|
|
277
|
+
const receiver = await manager.findOne(User, { where: { id: toId } });
|
|
278
|
+
|
|
279
|
+
if (sender.credits < amount) {
|
|
280
|
+
throw new Error('Insufficient credits');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
sender.credits -= amount;
|
|
284
|
+
receiver.credits += amount;
|
|
285
|
+
|
|
286
|
+
await manager.save([sender, receiver]);
|
|
287
|
+
|
|
288
|
+
return { success: true };
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Soft Delete
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { DeleteDateColumn } from 'typeorm';
|
|
298
|
+
|
|
299
|
+
@Entity('users')
|
|
300
|
+
export class User {
|
|
301
|
+
// ...
|
|
302
|
+
|
|
303
|
+
@DeleteDateColumn({ name: 'deleted_at' })
|
|
304
|
+
deletedAt?: Date;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Soft delete
|
|
308
|
+
await this.repository.softDelete(id);
|
|
309
|
+
|
|
310
|
+
// Restore
|
|
311
|
+
await this.repository.restore(id);
|
|
312
|
+
|
|
313
|
+
// Include soft-deleted in query
|
|
314
|
+
await this.repository.find({ withDeleted: true });
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Migrations
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
# Generate migration from entity changes
|
|
321
|
+
npx typeorm migration:generate src/migrations/AddUsersTable -d src/data-source.ts
|
|
322
|
+
|
|
323
|
+
# Create empty migration
|
|
324
|
+
npx typeorm migration:create src/migrations/SeedData
|
|
325
|
+
|
|
326
|
+
# Run migrations
|
|
327
|
+
npx typeorm migration:run -d src/data-source.ts
|
|
328
|
+
|
|
329
|
+
# Revert last migration
|
|
330
|
+
npx typeorm migration:revert -d src/data-source.ts
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Migration Example
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
337
|
+
|
|
338
|
+
export class AddUsersTable1234567890 implements MigrationInterface {
|
|
339
|
+
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
340
|
+
await queryRunner.query(`
|
|
341
|
+
CREATE TABLE users (
|
|
342
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
343
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
344
|
+
password VARCHAR(255) NOT NULL,
|
|
345
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
346
|
+
)
|
|
347
|
+
`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
351
|
+
await queryRunner.query(`DROP TABLE users`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Testing with TypeORM
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import { getRepositoryToken } from '@nestjs/typeorm';
|
|
360
|
+
|
|
361
|
+
const mockRepository = {
|
|
362
|
+
find: jest.fn(),
|
|
363
|
+
findOne: jest.fn(),
|
|
364
|
+
create: jest.fn(),
|
|
365
|
+
save: jest.fn(),
|
|
366
|
+
update: jest.fn(),
|
|
367
|
+
delete: jest.fn(),
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const module = await Test.createTestingModule({
|
|
371
|
+
providers: [
|
|
372
|
+
UsersService,
|
|
373
|
+
{
|
|
374
|
+
provide: getRepositoryToken(User),
|
|
375
|
+
useValue: mockRepository,
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
}).compile();
|
|
379
|
+
```
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/**/*.module.ts"
|
|
4
|
+
- "src/**/*.controller.ts"
|
|
5
|
+
- "src/**/*.service.ts"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# NestJS Module Architecture
|
|
9
|
+
|
|
10
|
+
## Module Design
|
|
11
|
+
|
|
12
|
+
### Single Responsibility
|
|
13
|
+
|
|
14
|
+
Each module should own exactly one domain. If a module does multiple things, split it.
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// Good: Focused modules
|
|
18
|
+
@Module({
|
|
19
|
+
imports: [DatabaseModule],
|
|
20
|
+
controllers: [UsersController],
|
|
21
|
+
providers: [UsersService, UsersRepository],
|
|
22
|
+
exports: [UsersService],
|
|
23
|
+
})
|
|
24
|
+
export class UsersModule {}
|
|
25
|
+
|
|
26
|
+
// Bad: Kitchen sink module
|
|
27
|
+
@Module({
|
|
28
|
+
controllers: [UsersController, OrdersController, PaymentsController],
|
|
29
|
+
providers: [/* too many */],
|
|
30
|
+
})
|
|
31
|
+
export class EverythingModule {}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Module Boundaries
|
|
35
|
+
|
|
36
|
+
- Modules communicate via **exported services only**
|
|
37
|
+
- Never import internal providers from other modules
|
|
38
|
+
- Use barrel exports (`index.ts`) for clean imports
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// users/index.ts
|
|
42
|
+
export { UsersModule } from './users.module';
|
|
43
|
+
export { UsersService } from './users.service';
|
|
44
|
+
export { User } from './entities/user.entity';
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Dynamic Modules for Configuration
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
@Module({})
|
|
51
|
+
export class DatabaseModule {
|
|
52
|
+
static forRoot(options: DatabaseOptions): DynamicModule {
|
|
53
|
+
return {
|
|
54
|
+
module: DatabaseModule,
|
|
55
|
+
global: true,
|
|
56
|
+
providers: [
|
|
57
|
+
{
|
|
58
|
+
provide: DATABASE_OPTIONS,
|
|
59
|
+
useValue: options,
|
|
60
|
+
},
|
|
61
|
+
DatabaseService,
|
|
62
|
+
],
|
|
63
|
+
exports: [DatabaseService],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Controller Rules
|
|
70
|
+
|
|
71
|
+
### Controllers Handle HTTP Only
|
|
72
|
+
|
|
73
|
+
- Parse request (params, query, body)
|
|
74
|
+
- Call service methods
|
|
75
|
+
- Return response
|
|
76
|
+
- NO business logic
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// Good
|
|
80
|
+
@Controller('users')
|
|
81
|
+
export class UsersController {
|
|
82
|
+
constructor(private readonly usersService: UsersService) {}
|
|
83
|
+
|
|
84
|
+
@Get(':id')
|
|
85
|
+
findOne(@Param('id', ParseUUIDPipe) id: string) {
|
|
86
|
+
return this.usersService.findOne(id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Bad: Business logic in controller
|
|
91
|
+
@Controller('users')
|
|
92
|
+
export class UsersController {
|
|
93
|
+
@Get(':id')
|
|
94
|
+
async findOne(@Param('id') id: string) {
|
|
95
|
+
const user = await this.userRepository.findOne(id);
|
|
96
|
+
if (!user) throw new NotFoundException();
|
|
97
|
+
if (user.deletedAt) throw new GoneException();
|
|
98
|
+
user.lastAccessed = new Date();
|
|
99
|
+
await this.userRepository.save(user);
|
|
100
|
+
return user;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Use Pipes for Transformation/Validation
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
@Get(':id')
|
|
109
|
+
findOne(
|
|
110
|
+
@Param('id', ParseUUIDPipe) id: string,
|
|
111
|
+
@Query('include', new ParseArrayPipe({ optional: true })) include?: string[],
|
|
112
|
+
) {
|
|
113
|
+
return this.usersService.findOne(id, { include });
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Service Rules
|
|
118
|
+
|
|
119
|
+
### Services Contain Business Logic
|
|
120
|
+
|
|
121
|
+
- Validation rules
|
|
122
|
+
- Data transformations
|
|
123
|
+
- Orchestration between repositories
|
|
124
|
+
- Error handling with appropriate exceptions
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
@Injectable()
|
|
128
|
+
export class OrdersService {
|
|
129
|
+
constructor(
|
|
130
|
+
private readonly ordersRepository: OrdersRepository,
|
|
131
|
+
private readonly usersService: UsersService,
|
|
132
|
+
private readonly inventoryService: InventoryService,
|
|
133
|
+
) {}
|
|
134
|
+
|
|
135
|
+
async createOrder(userId: string, dto: CreateOrderDto): Promise<Order> {
|
|
136
|
+
// Business logic belongs here
|
|
137
|
+
const user = await this.usersService.findOne(userId);
|
|
138
|
+
|
|
139
|
+
if (!user.verified) {
|
|
140
|
+
throw new ForbiddenException('Unverified users cannot place orders');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const available = await this.inventoryService.checkAvailability(dto.items);
|
|
144
|
+
if (!available) {
|
|
145
|
+
throw new ConflictException('Some items are out of stock');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return this.ordersRepository.create({ userId, ...dto });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Use NestJS Exceptions
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// Use built-in HTTP exceptions
|
|
157
|
+
throw new NotFoundException('Resource not found');
|
|
158
|
+
throw new BadRequestException('Invalid input');
|
|
159
|
+
throw new UnauthorizedException('Authentication required');
|
|
160
|
+
throw new ForbiddenException('Access denied');
|
|
161
|
+
throw new ConflictException('Resource already exists');
|
|
162
|
+
|
|
163
|
+
// Custom exceptions extend HttpException
|
|
164
|
+
export class BusinessRuleException extends HttpException {
|
|
165
|
+
constructor(message: string) {
|
|
166
|
+
super(message, HttpStatus.UNPROCESSABLE_ENTITY);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Dependency Injection
|
|
172
|
+
|
|
173
|
+
### Prefer Constructor Injection
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// Good
|
|
177
|
+
@Injectable()
|
|
178
|
+
export class UsersService {
|
|
179
|
+
constructor(
|
|
180
|
+
private readonly usersRepository: UsersRepository,
|
|
181
|
+
private readonly configService: ConfigService,
|
|
182
|
+
) {}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Avoid property injection
|
|
186
|
+
@Injectable()
|
|
187
|
+
export class UsersService {
|
|
188
|
+
@Inject()
|
|
189
|
+
private usersRepository: UsersRepository; // Harder to test
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Use Injection Tokens for Non-Class Dependencies
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// constants.ts
|
|
197
|
+
export const CONFIG_OPTIONS = Symbol('CONFIG_OPTIONS');
|
|
198
|
+
|
|
199
|
+
// module.ts
|
|
200
|
+
@Module({
|
|
201
|
+
providers: [
|
|
202
|
+
{
|
|
203
|
+
provide: CONFIG_OPTIONS,
|
|
204
|
+
useValue: { apiKey: '...' },
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
})
|
|
208
|
+
export class MyModule {}
|
|
209
|
+
|
|
210
|
+
// service.ts
|
|
211
|
+
@Injectable()
|
|
212
|
+
export class MyService {
|
|
213
|
+
constructor(@Inject(CONFIG_OPTIONS) private options: ConfigOptions) {}
|
|
214
|
+
}
|
|
215
|
+
```
|