@breadstone/archipel-mcp 0.0.16 → 0.0.17

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.
@@ -0,0 +1,766 @@
1
+ ---
2
+ title: Controller Best Practices
3
+ description: Structure, annotate, and implement NestJS controllers following Archipel conventions — regions, dependency injection, ResponseReturn, guards, and the @Api decorator.
4
+ order: 4
5
+ ---
6
+
7
+ # Controller Best Practices
8
+
9
+ This guide defines the canonical structure of a NestJS controller in an Archipel-powered application. Controllers are the HTTP boundary — they map transport concerns (routes, headers, status codes) to service calls and response objects. **No business logic lives here.**
10
+
11
+ If you haven't set up OpenAPI documentation yet, read the [OpenAPI & Feature Discovery](/guides/openapi-and-feature-discovery) guide first.
12
+
13
+ ---
14
+
15
+ ## Anatomy of a Controller
16
+
17
+ Every controller follows the same skeleton: **Imports → Class decorator → Regions (Fields, Ctor, Methods)**. No other regions are allowed.
18
+
19
+ ```typescript
20
+ // #region Imports
21
+
22
+ import { Body, Controller, Get, HttpStatus, Post, Query, UseGuards } from '@nestjs/common';
23
+ import { Api } from '@breadstone/archipel-platform-openapi';
24
+ import { Public, ResponseReturn, IpAddress, UserAgent } from '@breadstone/archipel-platform-core';
25
+ import { JwtAuthGuard, Token, User, type IAuthSubject } from '@breadstone/archipel-platform-authentication';
26
+ import { CreateOrderRequest, OrderResponse, OrderListResponse } from '../models';
27
+
28
+ // #endregion
29
+
30
+ @Api({ tags: ['Orders'] })
31
+ @Controller('api/orders')
32
+ export class OrderController {
33
+
34
+ // #region Fields
35
+
36
+ private readonly _orderService: OrderService;
37
+
38
+ // #endregion
39
+
40
+ // #region Ctor
41
+
42
+ /**
43
+ * Constructs a new instance of the `OrderController` class.
44
+ *
45
+ * @public
46
+ */
47
+ public constructor(orderService: OrderService) {
48
+ this._orderService = orderService;
49
+ }
50
+
51
+ // #endregion
52
+
53
+ // #region Methods
54
+
55
+ // ... endpoint methods
56
+
57
+ // #endregion
58
+
59
+ }
60
+ ```
61
+
62
+ ### Key Rules
63
+
64
+ | Rule | Why |
65
+ |------|-----|
66
+ | Controllers use **constructor injection** (not `inject()`) | Framework requires it for request-scoped providers; keeps DI explicit |
67
+ | Private fields prefixed with `_` | Coding standard — distinguishes injected dependencies from local variables |
68
+ | Regions in strict order: Fields → Ctor → Methods | Consistency across the entire codebase |
69
+ | No business logic in controllers | Controllers are I/O adapters; logic belongs in services |
70
+ | No direct Prisma/repository calls | Services orchestrate data access; controllers never touch persistence |
71
+
72
+ ---
73
+
74
+ ## Class-Level Decorators
75
+
76
+ Apply `@Api({ tags })` and `@Controller(path)` at class level. The tag groups all endpoints under a single OpenAPI section.
77
+
78
+ ```typescript
79
+ @Api({ tags: ['Auth'] })
80
+ @Controller('api/auth')
81
+ export class AuthController { ... }
82
+ ```
83
+
84
+ For versioned APIs, include the version in the path:
85
+
86
+ ```typescript
87
+ @Api({ tags: ['Users'] })
88
+ @Controller('api/v1/users')
89
+ export class UserController { ... }
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Dependency Injection
95
+
96
+ Controllers receive services via constructor parameters. Store them in private `readonly` fields.
97
+
98
+ ```typescript
99
+ // #region Fields
100
+
101
+ private readonly _userService: UserService;
102
+ private readonly _verificationService: VerificationService;
103
+ private readonly _userNameGeneratorService: UserNameGeneratorService;
104
+
105
+ // #endregion
106
+
107
+ // #region Ctor
108
+
109
+ public constructor(
110
+ userService: UserService,
111
+ verificationService: VerificationService,
112
+ userNameGeneratorService: UserNameGeneratorService
113
+ ) {
114
+ this._userService = userService;
115
+ this._verificationService = verificationService;
116
+ this._userNameGeneratorService = userNameGeneratorService;
117
+ }
118
+
119
+ // #endregion
120
+ ```
121
+
122
+ **Never** create service instances manually. NestJS resolves the dependency graph automatically via the module system.
123
+
124
+ ---
125
+
126
+ ## ResponseReturn
127
+
128
+ Every endpoint returns `Promise<ResponseReturn<T>>`. This wrapper provides a consistent response envelope with `success`, `status`, `data`, `error`, and `timestamp` fields.
129
+
130
+ ### Success Responses
131
+
132
+ | Factory Method | HTTP Status | Use Case |
133
+ |---------------|-------------|----------|
134
+ | `ResponseReturn.ok(Type, data)` | 200 | Returning data (GET, POST with result) |
135
+ | `ResponseReturn.created(Type, data)` | 201 | Resource creation |
136
+ | `ResponseReturn.accepted(Type, data)` | 202 | Async processing started |
137
+ | `ResponseReturn.noContent()` | 204 | Mutations with no response body |
138
+
139
+ ### Examples
140
+
141
+ ```typescript
142
+ // 200 OK — return data
143
+ public async login(
144
+ @Body() body: AuthLoginRequest
145
+ ): Promise<ResponseReturn<AuthLoginResponse>> {
146
+ const result = await this._userService.signIn(body.login, body.password);
147
+ return ResponseReturn.ok(AuthLoginResponse, {
148
+ accessToken: result.accessToken,
149
+ mfaRequired: result.mfaRequired
150
+ });
151
+ }
152
+
153
+ // 204 No Content — mutation without response body
154
+ public async register(
155
+ @Body() body: AuthRegisterRequest
156
+ ): Promise<ResponseReturn<void>> {
157
+ await this._userService.signUp(body.email, body.password, body.type);
158
+ return ResponseReturn.noContent();
159
+ }
160
+ ```
161
+
162
+ The first argument to `ok()` / `created()` is the **response DTO class** — `ResponseReturn` uses `class-transformer` internally to ensure the response matches the declared shape. This prevents accidentally leaking internal fields.
163
+
164
+ ### What NOT to Do
165
+
166
+ ```typescript
167
+ // ❌ BAD — inline object literal without ResponseReturn
168
+ public async getUser(): Promise<{ id: string; name: string }> {
169
+ const user = await this._userService.find(id);
170
+ return { id: user.id, name: user.name };
171
+ }
172
+
173
+ // ❌ BAD — raw service entity as response
174
+ public async getUser(): Promise<UserEntity> {
175
+ return this._userService.find(id);
176
+ }
177
+
178
+ // ✅ GOOD — ResponseReturn with typed DTO
179
+ public async getUser(): Promise<ResponseReturn<UserResponse>> {
180
+ const result = await this._userService.find(id);
181
+ return ResponseReturn.ok(UserResponse, result);
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Guards and Decorators
188
+
189
+ ### Authentication Guard
190
+
191
+ Use `@UseGuards(JwtAuthGuard)` for protected endpoints. Extract the authenticated user with `@User()`:
192
+
193
+ ```typescript
194
+ @Get('me')
195
+ @UseGuards(JwtAuthGuard)
196
+ @Api({
197
+ auth: 'bearer',
198
+ operation: {
199
+ operationId: 'me',
200
+ description: 'Gets the current user.',
201
+ summary: 'Get current user'
202
+ },
203
+ responses: [{
204
+ status: HttpStatus.OK,
205
+ description: 'Authenticated user profile.',
206
+ type: UserResponse
207
+ }, {
208
+ status: HttpStatus.UNAUTHORIZED,
209
+ description: 'Authentication is required.'
210
+ }]
211
+ })
212
+ public async me(
213
+ @User() user: IAuthSubject
214
+ ): Promise<ResponseReturn<UserResponse>> {
215
+ const result = await this._userService.find({ id: user.id });
216
+ return ResponseReturn.ok(UserResponse, result);
217
+ }
218
+ ```
219
+
220
+ ### Public Routes
221
+
222
+ Mark unauthenticated endpoints with `@Public()`. This bypasses the global JWT guard if one is registered:
223
+
224
+ ```typescript
225
+ @Post('register')
226
+ @Public()
227
+ public async register(@Body() body: RegisterRequest): Promise<ResponseReturn<void>> { ... }
228
+ ```
229
+
230
+ ### Token Extraction
231
+
232
+ Use `@Token()` when you need the raw JWT string (e.g., for logout or refresh):
233
+
234
+ ```typescript
235
+ @Post('logout')
236
+ @UseGuards(JwtAuthGuard)
237
+ public async logout(@Token() token: string): Promise<ResponseReturn<void>> {
238
+ await this._userService.signOut(token);
239
+ return ResponseReturn.noContent();
240
+ }
241
+ ```
242
+
243
+ ### Custom Parameter Decorators
244
+
245
+ Archipel provides built-in decorators for common request data:
246
+
247
+ | Decorator | Extracts | Typical Use |
248
+ |-----------|----------|-------------|
249
+ | `@User()` | `request.user` as `IAuthSubject` | Authenticated user identity |
250
+ | `@Token()` | Bearer token string | Logout, refresh |
251
+ | `@IpAddress()` | Client IP address | Audit logging, rate limiting |
252
+ | `@UserAgent()` | User-Agent header | Device tracking, session metadata |
253
+
254
+ ---
255
+
256
+ ## Annotating with @Api
257
+
258
+ Every endpoint **must** have an `@Api()` decorator with `operation` and `responses`. This generates accurate OpenAPI documentation.
259
+
260
+ ### Minimal Annotation
261
+
262
+ ```typescript
263
+ @Get()
264
+ @Api({
265
+ operation: {
266
+ operationId: 'listOrders',
267
+ description: 'Lists all orders for the authenticated user.',
268
+ summary: 'List orders'
269
+ },
270
+ responses: [{
271
+ status: HttpStatus.OK,
272
+ description: 'List of orders.',
273
+ type: OrderListResponse
274
+ }]
275
+ })
276
+ public async listOrders(): Promise<ResponseReturn<OrderListResponse>> { ... }
277
+ ```
278
+
279
+ ### Full Annotation with Body, Query, and Auth
280
+
281
+ ```typescript
282
+ @Post('login')
283
+ @Api({
284
+ operation: {
285
+ operationId: 'login',
286
+ description: 'Logs the user in and returns an access token.',
287
+ summary: 'Authenticate user'
288
+ },
289
+ body: {
290
+ description: 'Credentials used for authentication.',
291
+ type: AuthLoginRequest
292
+ },
293
+ responses: [{
294
+ status: HttpStatus.OK,
295
+ description: 'Access token successfully generated.',
296
+ type: AuthLoginResponse
297
+ }, {
298
+ status: HttpStatus.UNAUTHORIZED,
299
+ description: 'Provided credentials are invalid.'
300
+ }]
301
+ })
302
+ public async login(
303
+ @IpAddress() ipAddress: string,
304
+ @UserAgent() userAgent: string,
305
+ @Body() body: AuthLoginRequest
306
+ ): Promise<ResponseReturn<AuthLoginResponse>> { ... }
307
+ ```
308
+
309
+ ### Protected Endpoint with Params
310
+
311
+ ```typescript
312
+ @Get(':provider')
313
+ @UseGuards(JwtAuthGuard)
314
+ @Api({
315
+ auth: 'bearer',
316
+ operation: {
317
+ operationId: 'getProvider',
318
+ description: 'Returns details about a payment provider.',
319
+ summary: 'Get provider'
320
+ },
321
+ params: [{
322
+ name: 'provider',
323
+ description: 'Provider identifier.',
324
+ required: true,
325
+ type: String
326
+ }],
327
+ responses: [{
328
+ status: HttpStatus.OK,
329
+ description: 'Provider details.',
330
+ type: ProviderResponse
331
+ }, {
332
+ status: HttpStatus.NOT_FOUND,
333
+ description: 'Provider not found.'
334
+ }]
335
+ })
336
+ ```
337
+
338
+ ### Query Parameters
339
+
340
+ ```typescript
341
+ @Get('availability')
342
+ @Api({
343
+ operation: {
344
+ operationId: 'checkAvailability',
345
+ description: 'Checks if a username is available.',
346
+ summary: 'Check availability'
347
+ },
348
+ query: [{
349
+ name: 'username',
350
+ description: 'The username to check.',
351
+ required: true,
352
+ type: String
353
+ }],
354
+ responses: [{
355
+ status: HttpStatus.OK,
356
+ description: 'Availability status.',
357
+ type: AvailabilityResponse
358
+ }]
359
+ })
360
+ public async checkAvailability(
361
+ @Query('username') username: string
362
+ ): Promise<ResponseReturn<AvailabilityResponse>> { ... }
363
+ ```
364
+
365
+ ---
366
+
367
+ ## Method Naming Conventions
368
+
369
+ Controller methods should clearly reflect the action and resource:
370
+
371
+ | Pattern | HTTP Method | Example |
372
+ |---------|-------------|---------|
373
+ | `listX` | GET (collection) | `listOrders()` |
374
+ | `getX` / `X` | GET (single) | `getOrder()`, `me()` |
375
+ | `createX` / domain verb | POST | `createOrder()`, `login()`, `register()` |
376
+ | `updateX` | PUT / PATCH | `updateProfile()` |
377
+ | `deleteX` | DELETE | `deleteOrder()` |
378
+ | `verbNoun` | POST (action) | `changePassword()`, `forgotPassword()`, `resendVerification()` |
379
+
380
+ Methods are always `public async` and return `Promise<ResponseReturn<T>>`.
381
+
382
+ ---
383
+
384
+ ## Rate Limiting
385
+
386
+ Use `@Throttle()` from `@nestjs/throttler` for endpoints that need rate limiting:
387
+
388
+ ```typescript
389
+ import { Throttle } from '@nestjs/throttler';
390
+
391
+ @Get('availability')
392
+ @Public()
393
+ @Throttle({ default: { limit: 10, ttl: 60000 } })
394
+ public async checkAvailability(
395
+ @Query('username') username: string
396
+ ): Promise<ResponseReturn<AvailabilityResponse>> { ... }
397
+ ```
398
+
399
+ Always document the rate limit in the `@Api` responses:
400
+
401
+ ```typescript
402
+ responses: [{
403
+ status: HttpStatus.OK,
404
+ description: 'Availability result.'
405
+ }, {
406
+ status: HttpStatus.TOO_MANY_REQUESTS,
407
+ description: 'Rate limit exceeded (max 10 requests per minute).'
408
+ }]
409
+ ```
410
+
411
+ ---
412
+
413
+ ## Social Authentication Pattern
414
+
415
+ Social login uses a guard factory and a two-endpoint flow: initiate → callback.
416
+
417
+ ```typescript
418
+ @Public()
419
+ @Get('social/:provider')
420
+ @UseGuards(socialAuthGuardFactory(':provider'))
421
+ @Api({
422
+ operation: {
423
+ operationId: 'socialAuthentication',
424
+ description: 'Redirects the user to the social provider.',
425
+ summary: 'Start social authentication'
426
+ },
427
+ params: [{
428
+ name: 'provider',
429
+ description: 'Authentication provider (github, google, microsoft, apple).',
430
+ required: true,
431
+ type: String
432
+ }],
433
+ responses: [{
434
+ status: HttpStatus.TEMPORARY_REDIRECT,
435
+ description: 'Redirects to the social provider.'
436
+ }]
437
+ })
438
+ public async socialAuthentication(): Promise<void> {
439
+ // Guard handles the redirect flow.
440
+ }
441
+
442
+ @Public()
443
+ @Get('social/:provider/callback')
444
+ @UseGuards(socialAuthGuardFactory(':provider'))
445
+ @Api({
446
+ operation: {
447
+ operationId: 'socialAuthenticationCallback',
448
+ description: 'Handles the callback from the authentication provider.',
449
+ summary: 'Handle social callback'
450
+ },
451
+ params: [{
452
+ name: 'provider',
453
+ description: 'Authentication provider that initiated the callback.',
454
+ required: true,
455
+ type: String
456
+ }],
457
+ responses: [{
458
+ status: HttpStatus.TEMPORARY_REDIRECT,
459
+ description: 'Redirects to the application.'
460
+ }]
461
+ })
462
+ public async socialAuthenticationCallback(
463
+ @Req() req: Request,
464
+ @Res({ passthrough: true }) res: Response
465
+ ): Promise<void> {
466
+ const user = req.user as IAuthSubject;
467
+ await new Promise<void>((resolve, reject) => {
468
+ req.login(user, (err) => err ? reject(err) : resolve());
469
+ });
470
+
471
+ const redirectUri = (req.authInfo as { state: { redirectUri?: string } }).state?.redirectUri;
472
+ res.redirect(redirectUri ?? '/');
473
+ }
474
+ ```
475
+
476
+ > **Note:** Social callback endpoints are the only place where `@Req()` and `@Res()` are acceptable. In all other endpoints, use typed parameter decorators (`@Body()`, `@Query()`, `@User()`, etc.).
477
+
478
+ ---
479
+
480
+ ## Complete Controller Example
481
+
482
+ Below is a full authentication controller demonstrating every pattern covered in this guide:
483
+
484
+ ```typescript
485
+ // #region Imports
486
+
487
+ import { Body, Controller, Get, HttpStatus, Post, Query, Req, Res, UseGuards } from '@nestjs/common';
488
+ import { Request, Response } from 'express';
489
+ import { Throttle } from '@nestjs/throttler';
490
+ import { Api } from '@breadstone/archipel-platform-openapi';
491
+ import { IpAddress, Public, ResponseReturn, UserAgent } from '@breadstone/archipel-platform-core';
492
+ import {
493
+ type IAuthSubject,
494
+ JwtAuthGuard,
495
+ Token,
496
+ User,
497
+ socialAuthGuardFactory
498
+ } from '@breadstone/archipel-platform-authentication';
499
+ import {
500
+ AuthLoginRequest,
501
+ AuthLoginResponse,
502
+ AuthRefreshResponse,
503
+ AuthRegisterRequest,
504
+ AuthChangePasswordRequest,
505
+ AuthForgotPasswordRequest,
506
+ AuthVerifyPinRequest,
507
+ AuthVerifyResponse,
508
+ UserResponse
509
+ } from '../models';
510
+
511
+ // #endregion
512
+
513
+ @Api({ tags: ['Auth'] })
514
+ @Controller('api/auth')
515
+ export class AuthController {
516
+
517
+ // #region Fields
518
+
519
+ private readonly _userService: UserService;
520
+ private readonly _verificationService: VerificationService;
521
+
522
+ // #endregion
523
+
524
+ // #region Ctor
525
+
526
+ /**
527
+ * Constructs a new instance of the `AuthController` class.
528
+ *
529
+ * @public
530
+ */
531
+ public constructor(
532
+ userService: UserService,
533
+ verificationService: VerificationService
534
+ ) {
535
+ this._userService = userService;
536
+ this._verificationService = verificationService;
537
+ }
538
+
539
+ // #endregion
540
+
541
+ // #region Methods
542
+
543
+ /**
544
+ * Logs the user in and returns an access token.
545
+ *
546
+ * @public
547
+ */
548
+ @Post('login')
549
+ @Api({
550
+ operation: {
551
+ operationId: 'login',
552
+ description: 'Logs the user in and returns an access token.',
553
+ summary: 'Authenticate user'
554
+ },
555
+ body: {
556
+ description: 'Credentials used for authentication.',
557
+ type: AuthLoginRequest
558
+ },
559
+ responses: [{
560
+ status: HttpStatus.OK,
561
+ description: 'Access token successfully generated.',
562
+ type: AuthLoginResponse
563
+ }, {
564
+ status: HttpStatus.UNAUTHORIZED,
565
+ description: 'Provided credentials are invalid.'
566
+ }]
567
+ })
568
+ public async login(
569
+ @IpAddress() ipAddress: string,
570
+ @UserAgent() userAgent: string,
571
+ @Body() body: AuthLoginRequest
572
+ ): Promise<ResponseReturn<AuthLoginResponse>> {
573
+ const result = await this._userService.signIn(
574
+ body.login, body.password, ipAddress, userAgent
575
+ );
576
+ return ResponseReturn.ok(AuthLoginResponse, {
577
+ accessToken: result.accessToken,
578
+ mfaRequired: result.mfaRequired
579
+ });
580
+ }
581
+
582
+ /**
583
+ * Logs the user out.
584
+ *
585
+ * @public
586
+ */
587
+ @Post('logout')
588
+ @UseGuards(JwtAuthGuard)
589
+ @Api({
590
+ auth: 'bearer',
591
+ operation: {
592
+ operationId: 'logout',
593
+ description: 'Logs the authenticated user out.',
594
+ summary: 'Log out user'
595
+ },
596
+ responses: [{
597
+ status: HttpStatus.NO_CONTENT,
598
+ description: 'User has been logged out.'
599
+ }, {
600
+ status: HttpStatus.UNAUTHORIZED,
601
+ description: 'Authentication is required.'
602
+ }]
603
+ })
604
+ public async logout(
605
+ @Token() token: string
606
+ ): Promise<ResponseReturn<void>> {
607
+ await this._userService.signOut(token);
608
+ return ResponseReturn.noContent();
609
+ }
610
+
611
+ /**
612
+ * Registers a new user.
613
+ *
614
+ * @public
615
+ */
616
+ @Post('register')
617
+ @Api({
618
+ operation: {
619
+ operationId: 'register',
620
+ description: 'Registers a new user.',
621
+ summary: 'Register user'
622
+ },
623
+ body: {
624
+ description: 'Registration payload.',
625
+ type: AuthRegisterRequest
626
+ },
627
+ responses: [{
628
+ status: HttpStatus.NO_CONTENT,
629
+ description: 'The user has been registered.'
630
+ }, {
631
+ status: HttpStatus.BAD_REQUEST,
632
+ description: 'Email or password are missing.'
633
+ }, {
634
+ status: HttpStatus.CONFLICT,
635
+ description: 'The email address is already registered.'
636
+ }]
637
+ })
638
+ public async register(
639
+ @Body() body: AuthRegisterRequest
640
+ ): Promise<ResponseReturn<void>> {
641
+ await this._userService.signUp(body.email, body.password, body.type, {
642
+ avatar: body.profile?.avatar,
643
+ firstName: body.profile?.firstName,
644
+ lastName: body.profile?.lastName,
645
+ userName: body.profile?.userName
646
+ });
647
+ return ResponseReturn.noContent();
648
+ }
649
+
650
+ /**
651
+ * Gets the current user.
652
+ *
653
+ * @public
654
+ */
655
+ @Get('me')
656
+ @UseGuards(JwtAuthGuard)
657
+ @Api({
658
+ auth: 'bearer',
659
+ operation: {
660
+ operationId: 'me',
661
+ description: 'Gets the current user.',
662
+ summary: 'Get current user'
663
+ },
664
+ responses: [{
665
+ status: HttpStatus.OK,
666
+ description: 'Authenticated user profile.',
667
+ type: UserResponse
668
+ }, {
669
+ status: HttpStatus.UNAUTHORIZED,
670
+ description: 'Authentication is required.'
671
+ }]
672
+ })
673
+ public async me(
674
+ @User() user: IAuthSubject
675
+ ): Promise<ResponseReturn<UserResponse>> {
676
+ const result = await this._userService.find({ id: user.id });
677
+ return ResponseReturn.ok(UserResponse, result);
678
+ }
679
+
680
+ /**
681
+ * Changes the password.
682
+ *
683
+ * @public
684
+ */
685
+ @Post('change-password')
686
+ @UseGuards(JwtAuthGuard)
687
+ @Api({
688
+ auth: 'bearer',
689
+ operation: {
690
+ operationId: 'changePassword',
691
+ description: 'Changes the authenticated user\'s password.',
692
+ summary: 'Change password'
693
+ },
694
+ body: {
695
+ description: 'Payload containing the new password.',
696
+ type: AuthChangePasswordRequest
697
+ },
698
+ responses: [{
699
+ status: HttpStatus.NO_CONTENT,
700
+ description: 'Password has been changed.'
701
+ }, {
702
+ status: HttpStatus.UNAUTHORIZED,
703
+ description: 'Authentication is required.'
704
+ }]
705
+ })
706
+ public async changePassword(
707
+ @Body() body: AuthChangePasswordRequest,
708
+ @User() user: IAuthSubject
709
+ ): Promise<ResponseReturn<void>> {
710
+ await this._userService.changePassword({ id: user.id }, body.password);
711
+ return ResponseReturn.noContent();
712
+ }
713
+
714
+ /**
715
+ * Sends a password reset email.
716
+ *
717
+ * @public
718
+ */
719
+ @Post('forgot-password')
720
+ @Api({
721
+ operation: {
722
+ operationId: 'forgotPassword',
723
+ description: 'Sends a password reset email.',
724
+ summary: 'Forgot password'
725
+ },
726
+ body: {
727
+ description: 'Payload identifying the account.',
728
+ type: AuthForgotPasswordRequest
729
+ },
730
+ responses: [{
731
+ status: HttpStatus.NO_CONTENT,
732
+ description: 'Password reset instructions sent.'
733
+ }, {
734
+ status: HttpStatus.BAD_REQUEST,
735
+ description: 'Login identifier is missing.'
736
+ }]
737
+ })
738
+ public async forgotPassword(
739
+ @Body() body: AuthForgotPasswordRequest
740
+ ): Promise<ResponseReturn<void>> {
741
+ await this._userService.forgotPassword(body.login);
742
+ return ResponseReturn.noContent();
743
+ }
744
+
745
+ // #endregion
746
+
747
+ }
748
+ ```
749
+
750
+ ---
751
+
752
+ ## Checklist
753
+
754
+ Before submitting a controller, verify:
755
+
756
+ - [ ] Class has `@Api({ tags })` and `@Controller(path)` decorators
757
+ - [ ] Every method has `@Api()` with `operation` and `responses`
758
+ - [ ] Protected endpoints use `@UseGuards(JwtAuthGuard)` and `@Api({ auth: 'bearer' })`
759
+ - [ ] Public endpoints have `@Public()` decorator
760
+ - [ ] All methods return `Promise<ResponseReturn<T>>`
761
+ - [ ] No business logic — only delegation to services
762
+ - [ ] No direct repository or Prisma calls
763
+ - [ ] Constructor injection with private `readonly` fields
764
+ - [ ] Regions: Fields → Ctor → Methods (no other regions)
765
+ - [ ] TSDoc on every public method
766
+ - [ ] Rate-limited endpoints document `TOO_MANY_REQUESTS` response
@@ -9,13 +9,17 @@ Practical guides for working with Archipel packages in your NestJS application.
9
9
 
10
10
  ## Foundations
11
11
 
12
- | Guide | What you'll learn |
13
- | ------------------------------------------- | -------------------------------------------------------------------------------------- |
14
- | [Getting Started](./getting-started) | Install packages, register your first module, implement ports, and run the app. |
15
- | [Configuration Management](./configuration) | Type-safe config keys, strategy-based resolution, central registry, and validation. |
16
- | [Implementing Ports](./implementing-ports) | How the port/adapter pattern works, how to write adapters, and how to test them. |
17
- | [Testing](./testing) | Unit testing services, testing port adapters, integration testing with real databases. |
18
- | [TSDoc Guidelines](./tsdoc-guidelines) | Documentation standards for public APIs and generated reference pages. |
12
+ | Guide | What you'll learn |
13
+ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
14
+ | [Getting Started](./getting-started) | Install packages, register your first module, implement ports, and run the app. |
15
+ | [Configuration Management](./configuration) | Type-safe config keys, strategy-based resolution, central registry, and validation. |
16
+ | [Implementing Ports](./implementing-ports) | How the port/adapter pattern works, how to write adapters, and how to test them. |
17
+ | [Controller Best Practices](./controller-best-practices) | Structure, annotate, and implement controllers — regions, DI, ResponseReturn, guards, and the @Api decorator. |
18
+ | [Request & Response DTOs](./request-response-dtos) | DTO naming conventions, validation decorators, cross-field validation, and class-transformer integration. |
19
+ | [Testing](./testing) | Unit testing services, testing port adapters, integration testing with real databases. |
20
+ | [TSDoc Guidelines](./tsdoc-guidelines) | Documentation standards for public APIs and generated reference pages. ld validation, and class-transformer integration. |
21
+ | [Testing](./testing) | Unit testing services, testing port adapters, integration testing with real databases. |
22
+ | [TSDoc Guidelines](./tsdoc-guidelines) | Documentation standards for public APIs and generated reference pages. |
19
23
 
20
24
  ## Security & Identity
21
25
 
@@ -0,0 +1,411 @@
1
+ ---
2
+ title: Request & Response DTOs
3
+ description: Naming conventions, validation decorators, and class-transformer integration for request and response data transfer objects in Archipel NestJS applications.
4
+ order: 4
5
+ ---
6
+
7
+ # Request & Response DTOs
8
+
9
+ This guide covers how to define, validate, and transform request and response DTOs in an Archipel-powered NestJS application. DTOs are the contract between your API and its consumers — they define what goes in and what comes out.
10
+
11
+ For how controllers use DTOs together with `ResponseReturn`, see the [Controller Best Practices](/guides/controller-best-practices) guide.
12
+
13
+ ---
14
+
15
+ ## Naming Conventions
16
+
17
+ DTOs follow a strict naming pattern:
18
+
19
+ | Type | Pattern | Example |
20
+ |------|---------|---------|
21
+ | Create request | `Create{Resource}Request` | `CreateOrderRequest` |
22
+ | Update request | `Update{Resource}Request` | `UpdateProfileRequest` |
23
+ | Action request | `{Action}{Resource}Request` | `AuthLoginRequest`, `AuthVerifyPinRequest` |
24
+ | Single response | `{Resource}Response` | `UserResponse`, `OrderResponse` |
25
+ | List response | `{Resource}ListResponse` | `OrderListResponse` |
26
+ | Action response | `{Action}{Resource}Response` | `AuthLoginResponse`, `AuthRefreshResponse` |
27
+ | Availability / status | `{Resource}{Status}Response` | `AuthUsernameAvailabilityResponse` |
28
+
29
+ ### Grouping by Feature
30
+
31
+ When a controller has many DTOs, prefix them with the feature name to keep imports clean:
32
+
33
+ ```typescript
34
+ // Auth feature DTOs
35
+ AuthLoginRequest
36
+ AuthLoginResponse
37
+ AuthRegisterRequest
38
+ AuthRefreshResponse
39
+ AuthChangePasswordRequest
40
+ AuthChangeEmailRequest
41
+ AuthForgotPasswordRequest
42
+ AuthVerifyPinRequest
43
+ AuthVerifyResponse
44
+ AuthResendVerificationRequest
45
+ AuthUsernameSuggestionsResponse
46
+ AuthUsernameAvailabilityResponse
47
+ AuthAvatarSuggestionResponse
48
+ ```
49
+
50
+ All DTOs for a feature are exported from a `models/` barrel file:
51
+
52
+ ```typescript
53
+ // models/index.ts
54
+ export { AuthLoginRequest } from './AuthLoginRequest';
55
+ export { AuthLoginResponse } from './AuthLoginResponse';
56
+ export { AuthRegisterRequest } from './AuthRegisterRequest';
57
+ // ...
58
+ ```
59
+
60
+ ---
61
+
62
+ ## File Organization
63
+
64
+ **One public export per file.** Each DTO class gets its own file, named after the class.
65
+
66
+ ```
67
+ src/
68
+ auth/
69
+ models/
70
+ AuthLoginRequest.ts
71
+ AuthLoginResponse.ts
72
+ AuthRegisterRequest.ts
73
+ AuthRefreshResponse.ts
74
+ UserResponse.ts
75
+ index.ts
76
+ AuthController.ts
77
+ AuthModule.ts
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Request DTOs (Validation)
83
+
84
+ Request DTOs are classes decorated with `class-validator` and `@ApiProperty` from `@nestjs/swagger`.
85
+
86
+ ### Basic Request
87
+
88
+ ```typescript
89
+ import { ApiProperty } from '@nestjs/swagger';
90
+ import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
91
+
92
+ export class AuthLoginRequest {
93
+
94
+ /**
95
+ * The user's email address or username.
96
+ *
97
+ * @public
98
+ */
99
+ @ApiProperty({ description: 'Email address or username.' })
100
+ @IsNotEmpty()
101
+ @IsString()
102
+ public login!: string;
103
+
104
+ /**
105
+ * The user's password.
106
+ *
107
+ * @public
108
+ */
109
+ @ApiProperty({ description: 'Account password.' })
110
+ @IsNotEmpty()
111
+ @IsString()
112
+ @MinLength(8)
113
+ public password!: string;
114
+
115
+ }
116
+ ```
117
+
118
+ ### Request with Optional Fields
119
+
120
+ ```typescript
121
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
122
+ import { IsEmail, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator';
123
+ import { Type } from 'class-transformer';
124
+
125
+ export class AuthRegisterRequest {
126
+
127
+ @ApiProperty({ description: 'Email address for the new account.' })
128
+ @IsNotEmpty()
129
+ @IsEmail()
130
+ public email!: string;
131
+
132
+ @ApiProperty({ description: 'Password for the new account.', minLength: 8 })
133
+ @IsNotEmpty()
134
+ @IsString()
135
+ @MinLength(8)
136
+ public password!: string;
137
+
138
+ @ApiProperty({ description: 'Account type.', example: 'personal' })
139
+ @IsNotEmpty()
140
+ @IsString()
141
+ public type!: string;
142
+
143
+ @ApiPropertyOptional({ description: 'Optional profile information.' })
144
+ @IsOptional()
145
+ @ValidateNested()
146
+ @Type(() => ProfilePayload)
147
+ public profile?: ProfilePayload;
148
+
149
+ }
150
+ ```
151
+
152
+ ### Nested Payloads
153
+
154
+ When a request contains nested objects, define them as separate classes with `@ValidateNested()` and `@Type()`:
155
+
156
+ ```typescript
157
+ import { ApiPropertyOptional } from '@nestjs/swagger';
158
+ import { IsOptional, IsString } from 'class-validator';
159
+
160
+ export class ProfilePayload {
161
+
162
+ @ApiPropertyOptional()
163
+ @IsOptional()
164
+ @IsString()
165
+ public avatar?: string;
166
+
167
+ @ApiPropertyOptional()
168
+ @IsOptional()
169
+ @IsString()
170
+ public firstName?: string;
171
+
172
+ @ApiPropertyOptional()
173
+ @IsOptional()
174
+ @IsString()
175
+ public lastName?: string;
176
+
177
+ @ApiPropertyOptional()
178
+ @IsOptional()
179
+ @IsString()
180
+ public userName?: string;
181
+
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Response DTOs (Transformation)
188
+
189
+ Response DTOs use `@ApiProperty` for documentation and `@Expose()` from `class-transformer` for serialization control.
190
+
191
+ ### Basic Response
192
+
193
+ ```typescript
194
+ import { ApiProperty } from '@nestjs/swagger';
195
+ import { Expose } from 'class-transformer';
196
+
197
+ export class AuthLoginResponse {
198
+
199
+ @ApiProperty({ description: 'JWT access token.' })
200
+ @Expose()
201
+ public accessToken!: string;
202
+
203
+ @ApiProperty({ description: 'Whether multi-factor authentication is required.' })
204
+ @Expose()
205
+ public mfaRequired!: boolean;
206
+
207
+ }
208
+ ```
209
+
210
+ ### Response with Optional Fields
211
+
212
+ ```typescript
213
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
214
+ import { Expose } from 'class-transformer';
215
+
216
+ export class AuthLoginResponse {
217
+
218
+ @ApiProperty({ description: 'JWT access token.' })
219
+ @Expose()
220
+ public accessToken!: string;
221
+
222
+ @ApiProperty({ description: 'Whether MFA is required.' })
223
+ @Expose()
224
+ public mfaRequired!: boolean;
225
+
226
+ @ApiProperty({ description: 'Whether email verification is required.' })
227
+ @Expose()
228
+ public verificationRequired!: boolean;
229
+
230
+ @ApiPropertyOptional({ description: 'MFA challenge identifier.' })
231
+ @Expose()
232
+ public challengeId?: string;
233
+
234
+ @ApiPropertyOptional({ description: 'Available MFA methods.', type: [String] })
235
+ @Expose()
236
+ public methods?: Array<string>;
237
+
238
+ @ApiPropertyOptional({ description: 'Preferred MFA method.' })
239
+ @Expose()
240
+ public preferredMethod?: string;
241
+
242
+ }
243
+ ```
244
+
245
+ ### User / Entity Response
246
+
247
+ ```typescript
248
+ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
249
+ import { Expose } from 'class-transformer';
250
+
251
+ export class UserResponse {
252
+
253
+ @ApiProperty({ description: 'Unique user identifier.' })
254
+ @Expose()
255
+ public id!: string;
256
+
257
+ @ApiProperty({ description: 'Email address.' })
258
+ @Expose()
259
+ public email!: string;
260
+
261
+ @ApiPropertyOptional({ description: 'Display username.' })
262
+ @Expose()
263
+ public userName?: string;
264
+
265
+ @ApiPropertyOptional({ description: 'First name.' })
266
+ @Expose()
267
+ public firstName?: string;
268
+
269
+ @ApiPropertyOptional({ description: 'Last name.' })
270
+ @Expose()
271
+ public lastName?: string;
272
+
273
+ @ApiPropertyOptional({ description: 'Avatar URL or base64 image.' })
274
+ @Expose()
275
+ public avatar?: string;
276
+
277
+ @ApiProperty({ description: 'Whether the email is verified.' })
278
+ @Expose()
279
+ public isVerified!: boolean;
280
+
281
+ }
282
+ ```
283
+
284
+ ---
285
+
286
+ ## How ResponseReturn Uses DTOs
287
+
288
+ When a controller calls `ResponseReturn.ok(AuthLoginResponse, data)`, the response pipeline:
289
+
290
+ 1. Instantiates `AuthLoginResponse` via `class-transformer`'s `plainToInstance()`
291
+ 2. Applies `@Expose()` / `@Exclude()` rules — only explicitly exposed fields appear in the output
292
+ 3. Wraps the result in the standard envelope: `{ success, status, data, timestamp }`
293
+
294
+ This means the DTO class **controls what the client sees**. Even if the service returns extra fields, they won't leak unless the DTO exposes them.
295
+
296
+ ```typescript
297
+ // Service returns: { id, email, password, internalFlag, ... }
298
+ // DTO only exposes: { id, email, userName }
299
+ // Client receives: { success: true, status: 200, data: { id, email, userName }, timestamp: ... }
300
+ ```
301
+
302
+ ---
303
+
304
+ ## Validation Pipeline
305
+
306
+ Archipel applications use a global `ValidationPipe` with strict settings:
307
+
308
+ ```typescript
309
+ app.useGlobalPipes(new ValidationPipe({
310
+ whitelist: true, // Strip properties not in the DTO
311
+ forbidNonWhitelisted: true, // Throw if unknown properties are sent
312
+ transform: true, // Auto-transform payloads to DTO instances
313
+ }));
314
+ ```
315
+
316
+ This means:
317
+ - Properties without decorators are **silently removed** (`whitelist`)
318
+ - Unknown properties cause a **400 Bad Request** (`forbidNonWhitelisted`)
319
+ - Request bodies are automatically transformed to class instances (`transform`)
320
+
321
+ ---
322
+
323
+ ## Cross-Field Validation
324
+
325
+ When fields depend on each other (e.g., either `email` or `phone` is required, but not both), implement a custom class-level validator:
326
+
327
+ ```typescript
328
+ import { ValidatorConstraint, ValidatorConstraintInterface, Validate, ValidationArguments } from 'class-validator';
329
+
330
+ @ValidatorConstraint({ name: 'eitherEmailOrPhone', async: false })
331
+ export class EitherEmailOrPhoneConstraint implements ValidatorConstraintInterface {
332
+
333
+ public validate(_: unknown, args: ValidationArguments): boolean {
334
+ const obj = args.object as { email?: string; phone?: string };
335
+ return Boolean(obj.email) !== Boolean(obj.phone);
336
+ }
337
+
338
+ public defaultMessage(): string {
339
+ return 'Exactly one of email or phone must be provided.';
340
+ }
341
+
342
+ }
343
+
344
+ @Validate(EitherEmailOrPhoneConstraint)
345
+ export class ContactRequest {
346
+
347
+ @ApiPropertyOptional()
348
+ @IsOptional()
349
+ @IsEmail()
350
+ public email?: string;
351
+
352
+ @ApiPropertyOptional()
353
+ @IsOptional()
354
+ @IsString()
355
+ public phone?: string;
356
+
357
+ }
358
+ ```
359
+
360
+ **Never** do cross-field validation in controllers or services — always use class-level validators so it's reusable and testable.
361
+
362
+ ---
363
+
364
+ ## Constants for Limits
365
+
366
+ Define and reuse constants for bounds that appear in validation and pagination:
367
+
368
+ ```typescript
369
+ // constants.ts
370
+ export const MAX_PAGE_SIZE = 100;
371
+ export const MAX_SEARCH_LIMIT = 50;
372
+ export const DEFAULT_PAGE_SIZE = 20;
373
+ ```
374
+
375
+ ```typescript
376
+ import { Max, Min, IsOptional, IsInt } from 'class-validator';
377
+ import { MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE } from '../constants';
378
+
379
+ export class PaginationQuery {
380
+
381
+ @ApiPropertyOptional({ default: DEFAULT_PAGE_SIZE, maximum: MAX_PAGE_SIZE })
382
+ @IsOptional()
383
+ @IsInt()
384
+ @Min(1)
385
+ @Max(MAX_PAGE_SIZE)
386
+ public limit?: number = DEFAULT_PAGE_SIZE;
387
+
388
+ @ApiPropertyOptional({ default: 0 })
389
+ @IsOptional()
390
+ @IsInt()
391
+ @Min(0)
392
+ public offset?: number = 0;
393
+
394
+ }
395
+ ```
396
+
397
+ ---
398
+
399
+ ## Checklist
400
+
401
+ Before submitting DTOs, verify:
402
+
403
+ - [ ] One DTO class per file, named after the class
404
+ - [ ] Request DTOs have `class-validator` decorators on every property
405
+ - [ ] Response DTOs have `@Expose()` on every public property
406
+ - [ ] Nested objects use `@ValidateNested()` + `@Type()`
407
+ - [ ] All properties have `@ApiProperty` or `@ApiPropertyOptional`
408
+ - [ ] No `any` types — use `unknown` or specific types
409
+ - [ ] Cross-field validation uses class-level validators, not imperative checks
410
+ - [ ] Pagination limits use shared constants with `@Max()`
411
+ - [ ] DTOs exported from a `models/index.ts` barrel
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@breadstone/archipel-mcp",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "MCP server providing Archipel platform knowledge — documentation, query patterns, and coding conventions — to AI development tools.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/main.js",