@breadstone/archipel-mcp 0.0.16 → 0.0.18

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