@atlashub/smartstack-cli 4.32.0 → 4.34.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.
Files changed (46) hide show
  1. package/.documentation/index.html +2 -2
  2. package/.documentation/init.html +358 -174
  3. package/dist/index.js +45 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/mcp-entry.mjs +271 -44
  6. package/dist/mcp-entry.mjs.map +1 -1
  7. package/package.json +1 -1
  8. package/templates/mcp-scaffolding/controller.cs.hbs +54 -128
  9. package/templates/project/README.md +19 -0
  10. package/templates/project/claude-md/api.CLAUDE.md.template +315 -0
  11. package/templates/project/claude-md/application.CLAUDE.md.template +181 -0
  12. package/templates/project/claude-md/domain.CLAUDE.md.template +125 -0
  13. package/templates/project/claude-md/infrastructure.CLAUDE.md.template +168 -0
  14. package/templates/project/claude-md/root.CLAUDE.md.template +339 -0
  15. package/templates/project/claude-md/web.CLAUDE.md.template +339 -0
  16. package/templates/skills/apex/SKILL.md +16 -10
  17. package/templates/skills/apex/_shared.md +1 -1
  18. package/templates/skills/apex/references/checks/architecture-checks.sh +154 -0
  19. package/templates/skills/apex/references/checks/backend-checks.sh +194 -0
  20. package/templates/skills/apex/references/checks/frontend-checks.sh +448 -0
  21. package/templates/skills/apex/references/checks/infrastructure-checks.sh +255 -0
  22. package/templates/skills/apex/references/checks/security-checks.sh +153 -0
  23. package/templates/skills/apex/references/checks/seed-checks.sh +536 -0
  24. package/templates/skills/apex/references/frontend-route-wiring-app-tsx.md +49 -192
  25. package/templates/skills/apex/references/post-checks.md +124 -2156
  26. package/templates/skills/apex/references/smartstack-api.md +160 -957
  27. package/templates/skills/apex/references/smartstack-frontend.md +134 -1022
  28. package/templates/skills/apex/references/smartstack-layers.md +12 -6
  29. package/templates/skills/apex/steps/step-00-init.md +81 -238
  30. package/templates/skills/apex/steps/step-03-execute.md +25 -752
  31. package/templates/skills/apex/steps/step-03a-layer0-domain.md +118 -0
  32. package/templates/skills/apex/steps/step-03b-layer1-seed.md +91 -0
  33. package/templates/skills/apex/steps/step-03c-layer2-backend.md +240 -0
  34. package/templates/skills/apex/steps/step-03d-layer3-frontend.md +300 -0
  35. package/templates/skills/apex/steps/step-03e-layer4-devdata.md +44 -0
  36. package/templates/skills/apex/steps/step-04-examine.md +70 -150
  37. package/templates/skills/application/references/frontend-i18n-and-output.md +2 -2
  38. package/templates/skills/application/references/frontend-route-naming.md +5 -1
  39. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +49 -198
  40. package/templates/skills/application/references/frontend-verification.md +11 -11
  41. package/templates/skills/application/steps/step-05-frontend.md +26 -15
  42. package/templates/skills/application/templates-frontend.md +4 -0
  43. package/templates/skills/cli-app-sync/SKILL.md +2 -2
  44. package/templates/skills/cli-app-sync/references/comparison-map.md +1 -1
  45. package/templates/skills/controller/references/controller-code-templates.md +70 -67
  46. package/templates/skills/controller/references/mcp-scaffold-workflow.md +5 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "4.32.0",
3
+ "version": "4.34.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -1,192 +1,118 @@
1
- using System;
2
- using System.Collections.Generic;
3
- using System.Threading;
4
- using System.Threading.Tasks;
5
- using Microsoft.AspNetCore.Authorization;
1
+ using MediatR;
6
2
  using Microsoft.AspNetCore.Mvc;
7
- using Microsoft.Extensions.Logging;
8
3
  using SmartStack.Api.Authorization;
9
4
  {{#if navRoute}}
10
5
  using SmartStack.Api.Routing;
11
6
  {{/if}}
12
- using SmartStack.Application.Common.Models;
7
+ using SmartStack.Application.Common.Authorization;
13
8
 
14
- namespace {{namespace}}.Controllers;
9
+ namespace {{namespace}};
15
10
 
16
11
  /// <summary>
17
- /// API controller for {{name}} operations
12
+ /// API controller for {{name}} operations.
18
13
  /// </summary>
19
14
  [ApiController]
20
15
  {{#if navRoute}}
21
- [NavRoute("{{navRoute}}")]
16
+ [NavRoute("{{navRoute}}"{{#if navRouteSuffix}}, Suffix = "{{navRouteSuffix}}"{{/if}}{{#if customSegment}}, CustomSegment = "{{customSegment}}"{{/if}})]
22
17
  {{else}}
23
18
  [Route("api/[controller]")]
24
19
  {{/if}}
25
- [Authorize]
20
+ [Microsoft.AspNetCore.Authorization.Authorize]
26
21
  [Produces("application/json")]
22
+ [Tags("{{namePlural}}")]
27
23
  public class {{name}}Controller : ControllerBase
28
24
  {
29
- private readonly ILogger<{{name}}Controller> _logger;
30
- // private readonly I{{name}}Service _{{nameCamel}}Service;
25
+ private readonly ISender _mediator;
31
26
 
32
- public {{name}}Controller(
33
- ILogger<{{name}}Controller> logger
34
- // I{{name}}Service {{nameCamel}}Service
35
- )
36
- {
37
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
38
- // _{{nameCamel}}Service = {{nameCamel}}Service ?? throw new ArgumentNullException(nameof({{nameCamel}}Service));
39
- }
27
+ public {{name}}Controller(ISender mediator) => _mediator = mediator;
40
28
 
41
29
  /// <summary>
42
30
  /// Get all {{namePlural}} with pagination
43
31
  /// </summary>
44
32
  [HttpGet]
45
33
  {{#if navRoute}}
46
- [RequirePermission("{{navRoute}}.read")]
34
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.View)]
47
35
  {{/if}}
48
- [ProducesResponseType(typeof(PaginatedResult<{{name}}Dto>), 200)]
49
- public async Task<ActionResult<PaginatedResult<{{name}}Dto>>> GetAll(
50
- [FromQuery] string? search = null,
51
- [FromQuery] int page = 1,
52
- [FromQuery] int pageSize = 20,
53
- CancellationToken cancellationToken = default)
36
+ [ProducesResponseType(typeof(List<{{name}}ListDto>), StatusCodes.Status200OK)]
37
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
38
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
39
+ public async Task<ActionResult<List<{{name}}ListDto>>> GetAll(CancellationToken ct)
54
40
  {
55
- _logger.LogInformation("Getting all {{namePlural}}");
56
-
57
- // TODO: Implement using service
58
- // var result = await _{{nameCamel}}Service.GetAllAsync(search, page, pageSize, cancellationToken);
59
- // return Ok(result);
60
-
61
- return Ok(PaginatedResult<{{name}}Dto>.Empty(page, pageSize));
41
+ var result = await _mediator.Send(new Get{{namePlural}}Query(), ct);
42
+ return Ok(result);
62
43
  }
63
44
 
64
45
  /// <summary>
65
46
  /// Get {{nameCamel}} by ID
66
47
  /// </summary>
67
- /// <param name="id">{{name}} ID</param>
68
- /// <param name="cancellationToken">Cancellation token</param>
69
- /// <returns>{{name}} details</returns>
70
48
  [HttpGet("{id:guid}")]
71
49
  {{#if navRoute}}
72
- [RequirePermission("{{navRoute}}.read")]
50
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.View)]
73
51
  {{/if}}
74
- [ProducesResponseType(typeof({{name}}Dto), 200)]
75
- [ProducesResponseType(404)]
76
- public async Task<ActionResult<{{name}}Dto>> GetById(
77
- Guid id,
78
- CancellationToken cancellationToken = default)
52
+ [ProducesResponseType(typeof({{name}}DetailDto), StatusCodes.Status200OK)]
53
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
54
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
55
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
56
+ public async Task<ActionResult<{{name}}DetailDto>> GetById(
57
+ Guid id, CancellationToken ct)
79
58
  {
80
- _logger.LogInformation("Getting {{nameCamel}} {Id}", id);
81
-
82
- // TODO: Implement using service
83
- // var result = await _{{nameCamel}}Service.GetByIdAsync(id, cancellationToken);
84
- // if (result == null) return NotFound();
85
- // return Ok(result);
86
-
87
- return NotFound();
59
+ var result = await _mediator.Send(new Get{{name}}ByIdQuery(id), ct);
60
+ if (result == null) return NotFound(new { message = "{{name}} not found" });
61
+ return Ok(result);
88
62
  }
89
63
 
90
64
  /// <summary>
91
65
  /// Create new {{nameCamel}}
92
66
  /// </summary>
93
- /// <param name="request">Create request</param>
94
- /// <param name="cancellationToken">Cancellation token</param>
95
- /// <returns>Created {{nameCamel}}</returns>
96
67
  [HttpPost]
97
68
  {{#if navRoute}}
98
- [RequirePermission("{{navRoute}}.create")]
69
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.Create)]
99
70
  {{/if}}
100
- [ProducesResponseType(typeof({{name}}Dto), 201)]
101
- [ProducesResponseType(400)]
102
- public async Task<ActionResult<{{name}}Dto>> Create(
103
- [FromBody] Create{{name}}Request request,
104
- CancellationToken cancellationToken = default)
71
+ [ProducesResponseType(typeof({{name}}DetailDto), StatusCodes.Status201Created)]
72
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
73
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
74
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
75
+ public async Task<ActionResult<{{name}}DetailDto>> Create(
76
+ [FromBody] Create{{name}}Request request, CancellationToken ct)
105
77
  {
106
- _logger.LogInformation("Creating new {{nameCamel}}");
107
-
108
- // TODO: Validate and create using service
109
- // var result = await _{{nameCamel}}Service.CreateAsync(request, cancellationToken);
110
- // return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
111
-
112
- var id = Guid.NewGuid();
113
- return CreatedAtAction(nameof(GetById), new { id }, null);
78
+ var result = await _mediator.Send(new Create{{name}}Command(request), ct);
79
+ return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
114
80
  }
115
81
 
116
82
  /// <summary>
117
83
  /// Update {{nameCamel}}
118
84
  /// </summary>
119
- /// <param name="id">{{name}} ID</param>
120
- /// <param name="request">Update request</param>
121
- /// <param name="cancellationToken">Cancellation token</param>
122
85
  [HttpPut("{id:guid}")]
123
86
  {{#if navRoute}}
124
- [RequirePermission("{{navRoute}}.update")]
87
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.Update)]
125
88
  {{/if}}
126
- [ProducesResponseType(204)]
127
- [ProducesResponseType(404)]
128
- [ProducesResponseType(400)]
129
- public async Task<ActionResult> Update(
130
- Guid id,
131
- [FromBody] Update{{name}}Request request,
132
- CancellationToken cancellationToken = default)
89
+ [ProducesResponseType(typeof({{name}}DetailDto), StatusCodes.Status200OK)]
90
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
91
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
92
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
93
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
94
+ public async Task<ActionResult<{{name}}DetailDto>> Update(
95
+ Guid id, [FromBody] Update{{name}}Request request, CancellationToken ct)
133
96
  {
134
- _logger.LogInformation("Updating {{nameCamel}} {Id}", id);
135
-
136
- // TODO: Implement using service
137
- // await _{{nameCamel}}Service.UpdateAsync(id, request, cancellationToken);
138
-
139
- return NoContent();
97
+ var result = await _mediator.Send(new Update{{name}}Command(id, request), ct);
98
+ if (result.IsNotFound) return NotFound(new { message = "{{name}} not found" });
99
+ return Ok(result.Data);
140
100
  }
141
101
 
142
102
  /// <summary>
143
103
  /// Delete {{nameCamel}}
144
104
  /// </summary>
145
- /// <param name="id">{{name}} ID</param>
146
- /// <param name="cancellationToken">Cancellation token</param>
147
105
  [HttpDelete("{id:guid}")]
148
106
  {{#if navRoute}}
149
- [RequirePermission("{{navRoute}}.delete")]
107
+ [RequirePermission(Permissions.{{module}}.{{namePlural}}.Delete)]
150
108
  {{/if}}
151
- [ProducesResponseType(204)]
152
- [ProducesResponseType(404)]
153
- public async Task<ActionResult> Delete(
154
- Guid id,
155
- CancellationToken cancellationToken = default)
109
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
110
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
111
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
112
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
113
+ public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
156
114
  {
157
- _logger.LogInformation("Deleting {{nameCamel}} {Id}", id);
158
-
159
- // TODO: Implement using service
160
- // await _{{nameCamel}}Service.DeleteAsync(id, cancellationToken);
161
-
162
- return NoContent();
115
+ var success = await _mediator.Send(new Delete{{name}}Command(id), ct);
116
+ return success ? NoContent() : NotFound(new { message = "{{name}} not found" });
163
117
  }
164
118
  }
165
-
166
- // ============================================================================
167
- // DTOs
168
- // ============================================================================
169
-
170
- /// <summary>
171
- /// {{name}} data transfer object
172
- /// </summary>
173
- public record {{name}}Dto(
174
- Guid Id,
175
- DateTime CreatedAt,
176
- DateTime? UpdatedAt
177
- // TODO: Add additional properties
178
- );
179
-
180
- /// <summary>
181
- /// Request to create a new {{nameCamel}}
182
- /// </summary>
183
- public record Create{{name}}Request(
184
- // TODO: Add creation properties
185
- );
186
-
187
- /// <summary>
188
- /// Request to update a {{nameCamel}}
189
- /// </summary>
190
- public record Update{{name}}Request(
191
- // TODO: Add update properties
192
- );
@@ -62,6 +62,25 @@ Migrations must be applied in order:
62
62
 
63
63
  This is handled automatically in `Program.cs.template`.
64
64
 
65
+ ## Frontend Setup (React)
66
+
67
+ ### Route Registration
68
+
69
+ SmartStack uses **PageRegistry + DynamicRouter** for routing. Register your pages:
70
+
71
+ ```tsx
72
+ import { PageRegistry } from '@atlashub/smartstack';
73
+ import { lazy } from 'react';
74
+
75
+ // Register your application pages
76
+ PageRegistry.register('rh.time.dashboard', lazy(() => import('./pages/TimeDashboard')));
77
+ PageRegistry.register('rh.time.list', lazy(() => import('./pages/TimeList')));
78
+ PageRegistry.register('rh.time.detail', lazy(() => import('./pages/TimeDetail')));
79
+ ```
80
+
81
+ Navigation entries (menu, permissions, routes) are managed in the database.
82
+ DynamicRouter resolves everything automatically — no manual route wiring needed.
83
+
65
84
  ## Usage
66
85
 
67
86
  ```bash
@@ -0,0 +1,315 @@
1
+ # {{ProjectName}}.Api - Memory
2
+
3
+ ## Purpose
4
+
5
+ HTTP entry point. REST API. Receives requests, delegates to Application layer, returns responses.
6
+
7
+ ## Dependencies
8
+
9
+ - **{{ProjectName}}.Application** (commands, queries, DTOs)
10
+ - **{{ProjectName}}.Infrastructure** (DI registration)
11
+ - **MediatR** (sending commands/queries)
12
+ - **Scalar.AspNetCore** (OpenAPI UI)
13
+
14
+ ---
15
+
16
+ ## OpenAPI / Swagger Configuration (CRITICAL)
17
+
18
+ ### Package Requirements
19
+
20
+ ```xml
21
+ <!-- {{ProjectName}}.Api.csproj -->
22
+ <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
23
+ <PackageReference Include="Scalar.AspNetCore" Version="2.0.0" />
24
+ ```
25
+
26
+ ### Program.cs Configuration
27
+
28
+ ```csharp
29
+ // Services
30
+ builder.Services.AddOpenApi(options =>
31
+ {
32
+ options.AddDocumentTransformer((document, context, ct) =>
33
+ {
34
+ document.Info = new OpenApiInfo
35
+ {
36
+ Title = "{{ProjectName}} API",
37
+ Version = "v1",
38
+ Description = "{{ProjectName}} REST API"
39
+ };
40
+ return Task.CompletedTask;
41
+ });
42
+ });
43
+
44
+ // Middleware (after app.Build())
45
+ if (app.Environment.IsDevelopment())
46
+ {
47
+ app.MapOpenApi(); // /openapi/v1.json
48
+ app.MapScalarApiReference(options => // /scalar/v1
49
+ {
50
+ options.WithTitle("{{ProjectName}} API")
51
+ .WithTheme(ScalarTheme.BluePlanet)
52
+ .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient);
53
+ });
54
+ }
55
+ ```
56
+
57
+ ### XML Documentation (MANDATORY for all Controllers)
58
+
59
+ ```xml
60
+ <!-- {{ProjectName}}.Api.csproj -->
61
+ <PropertyGroup>
62
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
63
+ <NoWarn>$(NoWarn);1591</NoWarn>
64
+ </PropertyGroup>
65
+ ```
66
+
67
+ ### Controller Documentation Pattern
68
+
69
+ ```csharp
70
+ /// <summary>
71
+ /// Manages orders
72
+ /// </summary>
73
+ [ApiController]
74
+ [Route("api/[controller]")]
75
+ [Produces("application/json")]
76
+ [Tags("Orders")]
77
+ public class OrdersController : ControllerBase
78
+ {
79
+ /// <summary>
80
+ /// Retrieves all orders with optional filtering
81
+ /// </summary>
82
+ [HttpGet]
83
+ [ProducesResponseType(typeof(IReadOnlyList<OrderDto>), StatusCodes.Status200OK)]
84
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
85
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status403Forbidden)]
86
+ public async Task<IActionResult> GetAll(CancellationToken ct)
87
+ {
88
+ var result = await _mediator.Send(new GetOrdersQuery(), ct);
89
+ return Ok(result);
90
+ }
91
+ }
92
+ ```
93
+
94
+ ### ProducesResponseType Cheat Sheet
95
+
96
+ | Scenario | Attributes |
97
+ |----------|------------|
98
+ | **GET (list)** | `[ProducesResponseType(typeof(List<Dto>), 200)]` |
99
+ | **GET (single)** | `[ProducesResponseType(typeof(Dto), 200)]`<br>`[ProducesResponseType(typeof(ProblemDetails), 404)]` |
100
+ | **POST (create)** | `[ProducesResponseType(typeof(Guid), 201)]`<br>`[ProducesResponseType(typeof(ValidationProblemDetails), 400)]` |
101
+ | **PUT (update)** | `[ProducesResponseType(204)]`<br>`[ProducesResponseType(typeof(ProblemDetails), 404)]` |
102
+ | **DELETE** | `[ProducesResponseType(204)]`<br>`[ProducesResponseType(typeof(ProblemDetails), 404)]` |
103
+ | **Auth required** | Add `[ProducesResponseType(typeof(ProblemDetails), 401)]` |
104
+ | **Permission required** | Add `[ProducesResponseType(typeof(ProblemDetails), 403)]` |
105
+ | **Conflict possible** | Add `[ProducesResponseType(typeof(ProblemDetails), 409)]` |
106
+
107
+ ### OpenAPI URLs
108
+
109
+ | Environment | URL | Purpose |
110
+ |-------------|-----|---------|
111
+ | Development | `/openapi/v1.json` | Raw OpenAPI spec |
112
+ | Development | `/scalar/v1` | Interactive UI (Scalar) |
113
+ | Production | Disabled | Security best practice |
114
+
115
+ ---
116
+
117
+ ## Structure
118
+
119
+ ```
120
+ {{ProjectName}}.Api/
121
+ ├── Controllers/ → Organized by Application/Module
122
+ ├── Authorization/ → RequirePermissionAttribute
123
+ ├── Middleware/
124
+ │ ├── GlobalExceptionHandlerMiddleware.cs
125
+ │ └── SessionValidationMiddleware.cs
126
+ ├── Extensions/
127
+ ├── RateLimiting/
128
+ ├── Program.cs
129
+ ├── appsettings.json
130
+ └── appsettings.Development.json
131
+ ```
132
+
133
+ ## Patterns
134
+
135
+ ### Controller Template
136
+
137
+ ```csharp
138
+ namespace {{ProjectName}}.Api.Controllers;
139
+
140
+ using MediatR;
141
+ using Microsoft.AspNetCore.Http;
142
+ using Microsoft.AspNetCore.Mvc;
143
+ using {{ProjectName}}.Api.Authorization;
144
+
145
+ [ApiController]
146
+ [Route("api/[controller]")]
147
+ [Produces("application/json")]
148
+ [Tags("Orders")]
149
+ public class OrdersController : ControllerBase
150
+ {
151
+ private readonly ISender _mediator;
152
+
153
+ public OrdersController(ISender mediator)
154
+ {
155
+ _mediator = mediator;
156
+ }
157
+
158
+ [HttpGet]
159
+ [ProducesResponseType(typeof(IReadOnlyList<OrderDto>), StatusCodes.Status200OK)]
160
+ public async Task<IActionResult> GetAll(CancellationToken ct)
161
+ {
162
+ var result = await _mediator.Send(new GetOrdersQuery(), ct);
163
+ return Ok(result);
164
+ }
165
+
166
+ [HttpGet("{id:guid}")]
167
+ [ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)]
168
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
169
+ public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
170
+ {
171
+ var result = await _mediator.Send(new GetOrderByIdQuery(id), ct);
172
+ return result is null ? NotFound() : Ok(result);
173
+ }
174
+
175
+ [HttpPost]
176
+ [ProducesResponseType(typeof(Guid), StatusCodes.Status201Created)]
177
+ [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
178
+ public async Task<IActionResult> Create([FromBody] CreateOrderCommand command, CancellationToken ct)
179
+ {
180
+ var id = await _mediator.Send(command, ct);
181
+ return CreatedAtAction(nameof(GetById), new { id }, id);
182
+ }
183
+
184
+ [HttpPut("{id:guid}")]
185
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
186
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
187
+ public async Task<IActionResult> Update(Guid id, [FromBody] UpdateOrderCommand command, CancellationToken ct)
188
+ {
189
+ if (id != command.Id) return BadRequest();
190
+ await _mediator.Send(command, ct);
191
+ return NoContent();
192
+ }
193
+
194
+ [HttpDelete("{id:guid}")]
195
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
196
+ [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
197
+ public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
198
+ {
199
+ await _mediator.Send(new DeleteOrderCommand(id), ct);
200
+ return NoContent();
201
+ }
202
+ }
203
+ ```
204
+
205
+ ### Exception Middleware
206
+
207
+ ```csharp
208
+ namespace {{ProjectName}}.Api.Middleware;
209
+
210
+ using System.Net;
211
+ using FluentValidation;
212
+ using Microsoft.AspNetCore.Mvc;
213
+ using {{ProjectName}}.Application.Common.Exceptions;
214
+
215
+ public class GlobalExceptionHandlerMiddleware
216
+ {
217
+ private readonly RequestDelegate _next;
218
+ private readonly ILogger<GlobalExceptionHandlerMiddleware> _logger;
219
+
220
+ public GlobalExceptionHandlerMiddleware(RequestDelegate next, ILogger<GlobalExceptionHandlerMiddleware> logger)
221
+ {
222
+ _next = next;
223
+ _logger = logger;
224
+ }
225
+
226
+ public async Task InvokeAsync(HttpContext context)
227
+ {
228
+ try
229
+ {
230
+ await _next(context);
231
+ }
232
+ catch (ValidationException ex)
233
+ {
234
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
235
+ context.Response.ContentType = "application/json";
236
+ var problemDetails = new ValidationProblemDetails(
237
+ ex.Errors.GroupBy(e => e.PropertyName)
238
+ .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray()))
239
+ {
240
+ Status = StatusCodes.Status400BadRequest,
241
+ Title = "Validation failed"
242
+ };
243
+ await context.Response.WriteAsJsonAsync(problemDetails);
244
+ }
245
+ catch (NotFoundException ex)
246
+ {
247
+ context.Response.StatusCode = StatusCodes.Status404NotFound;
248
+ context.Response.ContentType = "application/json";
249
+ var problemDetails = new ProblemDetails
250
+ {
251
+ Title = "Not Found",
252
+ Detail = ex.Message,
253
+ Status = StatusCodes.Status404NotFound
254
+ };
255
+ await context.Response.WriteAsJsonAsync(problemDetails);
256
+ }
257
+ catch (DomainException ex)
258
+ {
259
+ context.Response.StatusCode = StatusCodes.Status400BadRequest;
260
+ context.Response.ContentType = "application/json";
261
+ var problemDetails = new ProblemDetails
262
+ {
263
+ Title = "Business rule violation",
264
+ Detail = ex.Message,
265
+ Status = StatusCodes.Status400BadRequest
266
+ };
267
+ await context.Response.WriteAsJsonAsync(problemDetails);
268
+ }
269
+ catch (Exception ex)
270
+ {
271
+ _logger.LogError(ex, "Unhandled exception: {Message}", ex.Message);
272
+ context.Response.StatusCode = StatusCodes.Status500InternalServerError;
273
+ context.Response.ContentType = "application/json";
274
+ var problemDetails = new ProblemDetails
275
+ {
276
+ Title = "Internal Server Error",
277
+ Detail = "An unexpected error occurred",
278
+ Status = StatusCodes.Status500InternalServerError
279
+ };
280
+ await context.Response.WriteAsJsonAsync(problemDetails);
281
+ }
282
+ }
283
+ }
284
+ ```
285
+
286
+ ## API Response Conventions
287
+
288
+ | Action | Status Code | Response |
289
+ |--------|-------------|----------|
290
+ | GET (list) | 200 | `List<Dto>` |
291
+ | GET (single) | 200 / 404 | `Dto` / ProblemDetails |
292
+ | POST | 201 | Id + Location header |
293
+ | PUT | 204 | Empty |
294
+ | DELETE | 204 | Empty |
295
+ | Validation error | 400 | ValidationProblemDetails |
296
+ | Server error | 500 | ProblemDetails |
297
+
298
+ ## Rules
299
+
300
+ 1. **Controllers are thin** - delegate to MediatR immediately
301
+ 2. **NO business logic** in controllers
302
+ 3. **Use `CancellationToken`** in all async methods
303
+ 4. **Document with XML comments** for OpenAPI
304
+ 5. **Return `IActionResult`** for flexibility
305
+ 6. **ALWAYS add `[ProducesResponseType]`** for all possible responses
306
+ 7. **ALWAYS add `[Tags]`** to group endpoints in OpenAPI
307
+
308
+ ## When Adding New Endpoint
309
+
310
+ 1. Create controller in `Controllers/` (or add to existing)
311
+ 2. Inject `ISender` (MediatR)
312
+ 3. Create action method with proper HTTP verb
313
+ 4. Document with XML comments
314
+ 5. Add `[ProducesResponseType]` attributes
315
+ 6. Use command/query from Application layer