@polymorphism-tech/morph-spec 4.3.4 → 4.3.5
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/.morph/.morphversion +5 -0
- package/.morph/config/agents.json +948 -0
- package/.morph/config/config.json +9 -9
- package/.morph/project/context/README.md +17 -0
- package/.morph/project/context/detection-log.md +16 -0
- package/.morph/project/standards/inferred.md +59 -0
- package/.morph/standards/ai-agents/blazor-ui.md +364 -0
- package/.morph/standards/ai-agents/production.md +415 -0
- package/.morph/standards/ai-agents/setup.md +418 -0
- package/.morph/standards/ai-agents/team-orchestration.md +479 -0
- package/.morph/standards/ai-agents/workflows.md +354 -0
- package/.morph/standards/architecture/ddd/aggregates.md +120 -0
- package/.morph/standards/architecture/ddd/entities.md +99 -0
- package/.morph/standards/architecture/ddd/value-objects.md +124 -0
- package/.morph/standards/backend/api/minimal-api.md +494 -0
- package/.morph/standards/backend/api/rest.md +492 -0
- package/.morph/standards/backend/api/validation.md +88 -0
- package/.morph/standards/backend/authentication/passkeys.md +428 -0
- package/.morph/standards/backend/database/ef-core.md +199 -0
- package/.morph/standards/backend/database/migrations.md +393 -0
- package/.morph/standards/backend/database/postgresql/database.md +352 -0
- package/.morph/standards/backend/database/repository-patterns.md +528 -0
- package/.morph/standards/backend/database/vector-search-rag.md +541 -0
- package/.morph/standards/backend/dotnet/async.md +366 -0
- package/.morph/standards/backend/dotnet/core.md +117 -0
- package/.morph/standards/backend/dotnet/di.md +439 -0
- package/.morph/standards/backend/dotnet/program-cs-checklist.md +92 -0
- package/.morph/standards/backend/integrations/asaas/asaas-api.md +216 -0
- package/.morph/standards/backend/integrations/clerk/clerk-auth.md +290 -0
- package/.morph/standards/backend/integrations/hangfire/hangfire-jobs.md +350 -0
- package/.morph/standards/backend/integrations/resend/resend-email.md +385 -0
- package/.morph/standards/context/analytics.md +96 -0
- package/.morph/standards/context/bundles.md +110 -0
- package/.morph/standards/context/priming.md +78 -0
- package/.morph/standards/core/architecture.md +185 -0
- package/.morph/standards/core/coding.md +214 -0
- package/.morph/standards/core/git-branching-strategy.md +403 -0
- package/.morph/standards/core/git.md +185 -0
- package/.morph/standards/core/testing.md +295 -0
- package/.morph/standards/data/nosql/blob-storage.md +102 -0
- package/.morph/standards/data/nosql/cache/redis.md +97 -0
- package/.morph/standards/data/nosql/cosmos-db.md +118 -0
- package/.morph/standards/data/vector-search/azure-ai-search.md +121 -0
- package/.morph/standards/data/vector-search/rag-chunking.md +104 -0
- package/.morph/standards/frontend/blazor/design-checklist.md +222 -0
- package/.morph/standards/frontend/blazor/fluent-ui-setup.md +595 -0
- package/.morph/standards/frontend/blazor/fluent-ui.md +137 -0
- package/.morph/standards/frontend/blazor/html-conversion.md +184 -0
- package/.morph/standards/frontend/blazor/lifecycle.md +195 -0
- package/.morph/standards/frontend/blazor/pitfalls.md +198 -0
- package/.morph/standards/frontend/blazor/state.md +191 -0
- package/.morph/standards/frontend/design-system/animations.md +151 -0
- package/.morph/standards/frontend/design-system/naming.md +64 -0
- package/.morph/standards/frontend/nextjs/nextjs-patterns.md +198 -0
- package/.morph/standards/infrastructure/azure/azure.md +624 -0
- package/.morph/standards/infrastructure/azure/bicep/bicep-patterns.md +422 -0
- package/.morph/standards/infrastructure/azure/devops/azure-devops-setup.md +516 -0
- package/.morph/standards/infrastructure/azure/devops/local-development.md +520 -0
- package/.morph/standards/infrastructure/azure/services/functions.md +486 -0
- package/.morph/standards/infrastructure/azure/services/service-bus.md +459 -0
- package/.morph/standards/infrastructure/azure/services/storage.md +407 -0
- package/.morph/standards/infrastructure/docker/easypanel-deploy.md +196 -0
- package/.morph/standards/infrastructure/supabase/mcp-setup.md +252 -0
- package/.morph/standards/infrastructure/supabase/supabase-auth.md +176 -0
- package/.morph/standards/infrastructure/supabase/supabase-pgvector.md +169 -0
- package/.morph/standards/infrastructure/supabase/supabase-rls.md +184 -0
- package/.morph/standards/infrastructure/supabase/supabase-storage.md +153 -0
- package/.morph/standards/integration/api/graphql.md +91 -0
- package/.morph/standards/integration/api/grpc.md +114 -0
- package/.morph/standards/integration/api/rest-design.md +95 -0
- package/.morph/standards/integration/event-driven/cqrs.md +101 -0
- package/.morph/standards/integration/event-driven/event-sourcing.md +124 -0
- package/.morph/standards/integration/event-driven/service-bus.md +95 -0
- package/.morph/standards/observability/logging.md +131 -0
- package/.morph/standards/observability/metrics.md +121 -0
- package/.morph/standards/observability/monitoring.md +114 -0
- package/.morph/standards/observability/tracing.md +132 -0
- package/.morph/standards/workflows/parallel-execution.md +112 -0
- package/.morph/standards/workflows/thread-management.md +113 -0
- package/.morph/templates/.idea/morph-templates.xml +92 -0
- package/.morph/templates/.vscode/morph-templates.code-snippets +186 -0
- package/.morph/templates/IDE-SNIPPETS.md +266 -0
- package/.morph/templates/README.md +814 -0
- package/.morph/templates/REGISTRY.json +1677 -0
- package/.morph/templates/code/dotnet/backend/repository.cs +141 -0
- package/.morph/templates/code/dotnet/backend/service.cs +139 -0
- package/.morph/templates/code/dotnet/contracts/Commands.cs +74 -0
- package/.morph/templates/code/dotnet/contracts/Entities.cs +25 -0
- package/.morph/templates/code/dotnet/contracts/Queries.cs +74 -0
- package/.morph/templates/code/dotnet/contracts/README.md +74 -0
- package/.morph/templates/code/dotnet/contracts/api-contracts.cs +173 -0
- package/.morph/templates/code/dotnet/contracts/contracts.cs +217 -0
- package/.morph/templates/code/dotnet/database/migration.cs +83 -0
- package/.morph/templates/code/dotnet/frontend/component.razor +239 -0
- package/.morph/templates/code/dotnet/jobs/agent.cs +163 -0
- package/.morph/templates/code/dotnet/jobs/job.cs +171 -0
- package/.morph/templates/code/dotnet/test.cs +239 -0
- package/.morph/templates/code/sql/rls-policy.sql +57 -0
- package/.morph/templates/code/sql/supabase-migration.sql +100 -0
- package/.morph/templates/code/sql/supabase-migration.template.sql +113 -0
- package/.morph/templates/code/typescript/contracts.ts +168 -0
- package/.morph/templates/context/CONTEXT-FEATURE.md +276 -0
- package/.morph/templates/context/CONTEXT.md +181 -0
- package/.morph/templates/docs/proposal.md +182 -0
- package/.morph/templates/docs/spec.md +149 -0
- package/.morph/templates/examples/design-system-examples.md +357 -0
- package/.morph/templates/examples/spec-examples.md +90 -0
- package/.morph/templates/feature/decisions.md +187 -0
- package/.morph/templates/feature/recap.md +146 -0
- package/.morph/templates/feature/tasks.md +199 -0
- package/.morph/templates/infrastructure/azure/Dockerfile.example +82 -0
- package/.morph/templates/infrastructure/azure/README.md +286 -0
- package/.morph/templates/infrastructure/azure/app-insights.bicep +63 -0
- package/.morph/templates/infrastructure/azure/app-service.bicep +164 -0
- package/.morph/templates/infrastructure/azure/container-app-env.bicep +49 -0
- package/.morph/templates/infrastructure/azure/container-app.bicep +156 -0
- package/.morph/templates/infrastructure/azure/deploy-checklist.md +426 -0
- package/.morph/templates/infrastructure/azure/deploy.ps1 +229 -0
- package/.morph/templates/infrastructure/azure/deploy.sh +208 -0
- package/.morph/templates/infrastructure/azure/key-vault.bicep +91 -0
- package/.morph/templates/infrastructure/azure/main.bicep +189 -0
- package/.morph/templates/infrastructure/azure/parameters.dev.json +29 -0
- package/.morph/templates/infrastructure/azure/parameters.prod.json +29 -0
- package/.morph/templates/infrastructure/azure/parameters.staging.json +29 -0
- package/.morph/templates/infrastructure/azure/sql-database.bicep +103 -0
- package/.morph/templates/infrastructure/azure/storage.bicep +106 -0
- package/.morph/templates/infrastructure/docker/Dockerfile.template +58 -0
- package/.morph/templates/infrastructure/docker/docker-compose.template.yml +67 -0
- package/.morph/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -0
- package/.morph/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -0
- package/.morph/templates/infrastructure/docker/easypanel.template.json +54 -0
- package/.morph/templates/infrastructure/github/README.md +593 -0
- package/.morph/templates/infrastructure/github/actions/azure-auth/action.yml.hbs +22 -0
- package/.morph/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -0
- package/.morph/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -0
- package/.morph/templates/infrastructure/github/workflows/deploy-azure-app-service.yml.hbs +61 -0
- package/.morph/templates/infrastructure/github/workflows/deploy-easypanel.yml.hbs +31 -0
- package/.morph/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -0
- package/.morph/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -0
- package/.morph/templates/integrations/asaas-client.cs +387 -0
- package/.morph/templates/integrations/asaas-webhook.cs +351 -0
- package/.morph/templates/integrations/azure-identity-config.cs +288 -0
- package/.morph/templates/integrations/clerk-config.cs +258 -0
- package/.morph/templates/meta-prompts/fusion/fusion-agent.md +76 -0
- package/.morph/templates/meta-prompts/fusion/fusion-aggregator.md +100 -0
- package/.morph/templates/meta-prompts/hops/hop-retry.md +78 -0
- package/.morph/templates/meta-prompts/hops/hop-validation.md +97 -0
- package/.morph/templates/meta-prompts/hops/hop-wrapper.md +36 -0
- package/.morph/templates/meta-prompts/parallel-workers/parallel-coordinator.md +113 -0
- package/.morph/templates/meta-prompts/parallel-workers/parallel-worker.md +80 -0
- package/.morph/templates/meta-prompts/squad-leaders/backend-squad.md +90 -0
- package/.morph/templates/meta-prompts/squad-leaders/frontend-squad.md +126 -0
- package/.morph/templates/meta-prompts/squad-leaders/squad-leader.md +43 -0
- package/.morph/templates/meta-prompts/validators/checkpoint-validator.md +107 -0
- package/.morph/templates/meta-prompts/validators/pre-commit-validator.md +95 -0
- package/.morph/templates/saas/subscription.cs +347 -0
- package/.morph/templates/saas/tenant.cs +338 -0
- package/.morph/templates/state.template.json +17 -0
- package/.morph/templates/ui/FluentDesignTheme.cs +149 -0
- package/.morph/templates/ui/MudTheme.cs +281 -0
- package/.morph/templates/ui/design-system.css +226 -0
- package/bin/morph-spec.js +1 -1
- package/package.json +1 -1
- package/src/commands/project/update.js +100 -13
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# REST API Standards
|
|
2
|
+
|
|
3
|
+
> **Scope:** universal
|
|
4
|
+
> **Layer:** 0 (always load)
|
|
5
|
+
> **Keywords:** rest, api, endpoint, controller, http, restful
|
|
6
|
+
> **Load When:** always
|
|
7
|
+
|
|
8
|
+
RESTful API design conventions and best practices
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## HTTP Methods
|
|
13
|
+
|
|
14
|
+
| Method | Usage | Idempotent | Safe |
|
|
15
|
+
|--------|-------|------------|------|
|
|
16
|
+
| GET | Retrieve resource(s) | ✅ Yes | ✅ Yes |
|
|
17
|
+
| POST | Create new resource | ❌ No | ❌ No |
|
|
18
|
+
| PUT | Replace entire resource | ✅ Yes | ❌ No |
|
|
19
|
+
| PATCH | Partial update | ❌ No | ❌ No |
|
|
20
|
+
| DELETE | Remove resource | ✅ Yes | ❌ No |
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## URL Conventions
|
|
25
|
+
|
|
26
|
+
### Resource Naming
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
✅ GOOD
|
|
30
|
+
/api/users
|
|
31
|
+
/api/users/123
|
|
32
|
+
/api/users/123/orders
|
|
33
|
+
/api/orders/456/items
|
|
34
|
+
|
|
35
|
+
❌ BAD
|
|
36
|
+
/api/getUsers
|
|
37
|
+
/api/user
|
|
38
|
+
/api/User
|
|
39
|
+
/api/users/getAllOrders
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Rules
|
|
43
|
+
|
|
44
|
+
- Use **plural nouns** for collections (`/users`, not `/user`)
|
|
45
|
+
- Use **lowercase** (`/users`, not `/Users`)
|
|
46
|
+
- Use **hyphens** for multi-word resources (`/order-items`, not `/orderItems`)
|
|
47
|
+
- Use **forward slashes** for hierarchy (`/users/123/orders`)
|
|
48
|
+
- **No trailing slash** (`/users`, not `/users/`)
|
|
49
|
+
- **No verbs** in URLs (`/users`, not `/getUsers`)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Endpoint Patterns
|
|
54
|
+
|
|
55
|
+
### Collection Operations
|
|
56
|
+
|
|
57
|
+
```http
|
|
58
|
+
GET /api/users # List all users
|
|
59
|
+
POST /api/users # Create new user
|
|
60
|
+
GET /api/users/123 # Get user by ID
|
|
61
|
+
PUT /api/users/123 # Replace user
|
|
62
|
+
PATCH /api/users/123 # Update user
|
|
63
|
+
DELETE /api/users/123 # Delete user
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Nested Resources
|
|
67
|
+
|
|
68
|
+
```http
|
|
69
|
+
GET /api/users/123/orders # Get user's orders
|
|
70
|
+
POST /api/users/123/orders # Create order for user
|
|
71
|
+
GET /api/users/123/orders/456 # Get specific order
|
|
72
|
+
DELETE /api/users/123/orders/456 # Delete user's order
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Actions (Non-CRUD)
|
|
76
|
+
|
|
77
|
+
```http
|
|
78
|
+
POST /api/users/123/activate # Activate user
|
|
79
|
+
POST /api/orders/456/cancel # Cancel order
|
|
80
|
+
POST /api/orders/456/refund # Refund order
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## HTTP Status Codes
|
|
86
|
+
|
|
87
|
+
### Success (2xx)
|
|
88
|
+
|
|
89
|
+
| Code | Meaning | Usage |
|
|
90
|
+
|------|---------|-------|
|
|
91
|
+
| 200 OK | Success | GET, PUT, PATCH (with body) |
|
|
92
|
+
| 201 Created | Resource created | POST |
|
|
93
|
+
| 204 No Content | Success, no body | DELETE, PUT, PATCH (no body) |
|
|
94
|
+
|
|
95
|
+
### Client Error (4xx)
|
|
96
|
+
|
|
97
|
+
| Code | Meaning | Usage |
|
|
98
|
+
|------|---------|-------|
|
|
99
|
+
| 400 Bad Request | Invalid input | Validation errors |
|
|
100
|
+
| 401 Unauthorized | Not authenticated | Missing/invalid token |
|
|
101
|
+
| 403 Forbidden | Not authorized | Insufficient permissions |
|
|
102
|
+
| 404 Not Found | Resource not found | Invalid ID |
|
|
103
|
+
| 409 Conflict | Conflict | Duplicate email, version conflict |
|
|
104
|
+
| 422 Unprocessable Entity | Validation failed | Business rule violation |
|
|
105
|
+
|
|
106
|
+
### Server Error (5xx)
|
|
107
|
+
|
|
108
|
+
| Code | Meaning | Usage |
|
|
109
|
+
|------|---------|-------|
|
|
110
|
+
| 500 Internal Server Error | Unexpected error | Unhandled exceptions |
|
|
111
|
+
| 503 Service Unavailable | Service down | Maintenance, overload |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Request/Response Examples
|
|
116
|
+
|
|
117
|
+
### GET (List)
|
|
118
|
+
|
|
119
|
+
**Request:**
|
|
120
|
+
|
|
121
|
+
```http
|
|
122
|
+
GET /api/users?page=1&limit=20&sort=email&filter=active
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Response:**
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"data": [
|
|
130
|
+
{
|
|
131
|
+
"id": 1,
|
|
132
|
+
"email": "user@example.com",
|
|
133
|
+
"name": "John Doe",
|
|
134
|
+
"createdAt": "2026-02-16T10:00:00Z"
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
"meta": {
|
|
138
|
+
"page": 1,
|
|
139
|
+
"limit": 20,
|
|
140
|
+
"total": 45,
|
|
141
|
+
"totalPages": 3
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### GET (Single)
|
|
147
|
+
|
|
148
|
+
**Request:**
|
|
149
|
+
|
|
150
|
+
```http
|
|
151
|
+
GET /api/users/123
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Response:**
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"id": 123,
|
|
159
|
+
"email": "user@example.com",
|
|
160
|
+
"name": "John Doe",
|
|
161
|
+
"createdAt": "2026-02-16T10:00:00Z"
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### POST (Create)
|
|
166
|
+
|
|
167
|
+
**Request:**
|
|
168
|
+
|
|
169
|
+
```http
|
|
170
|
+
POST /api/users
|
|
171
|
+
Content-Type: application/json
|
|
172
|
+
|
|
173
|
+
{
|
|
174
|
+
"email": "newuser@example.com",
|
|
175
|
+
"name": "Jane Smith",
|
|
176
|
+
"password": "SecurePass123!"
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Response:**
|
|
181
|
+
|
|
182
|
+
```http
|
|
183
|
+
HTTP/1.1 201 Created
|
|
184
|
+
Location: /api/users/124
|
|
185
|
+
|
|
186
|
+
{
|
|
187
|
+
"id": 124,
|
|
188
|
+
"email": "newuser@example.com",
|
|
189
|
+
"name": "Jane Smith",
|
|
190
|
+
"createdAt": "2026-02-16T11:00:00Z"
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### PUT (Replace)
|
|
195
|
+
|
|
196
|
+
**Request:**
|
|
197
|
+
|
|
198
|
+
```http
|
|
199
|
+
PUT /api/users/123
|
|
200
|
+
Content-Type: application/json
|
|
201
|
+
|
|
202
|
+
{
|
|
203
|
+
"email": "updated@example.com",
|
|
204
|
+
"name": "John Updated"
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Response:**
|
|
209
|
+
|
|
210
|
+
```http
|
|
211
|
+
HTTP/1.1 200 OK
|
|
212
|
+
|
|
213
|
+
{
|
|
214
|
+
"id": 123,
|
|
215
|
+
"email": "updated@example.com",
|
|
216
|
+
"name": "John Updated",
|
|
217
|
+
"updatedAt": "2026-02-16T11:30:00Z"
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### DELETE
|
|
222
|
+
|
|
223
|
+
**Request:**
|
|
224
|
+
|
|
225
|
+
```http
|
|
226
|
+
DELETE /api/users/123
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Response:**
|
|
230
|
+
|
|
231
|
+
```http
|
|
232
|
+
HTTP/1.1 204 No Content
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Error Responses
|
|
238
|
+
|
|
239
|
+
### Standard Error Format
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"error": {
|
|
244
|
+
"code": "VALIDATION_ERROR",
|
|
245
|
+
"message": "Validation failed for one or more fields",
|
|
246
|
+
"details": [
|
|
247
|
+
{
|
|
248
|
+
"field": "email",
|
|
249
|
+
"message": "Email is required"
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"field": "password",
|
|
253
|
+
"message": "Password must be at least 8 characters"
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Error Codes
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
// 400 Bad Request
|
|
264
|
+
{
|
|
265
|
+
"error": {
|
|
266
|
+
"code": "INVALID_INPUT",
|
|
267
|
+
"message": "Invalid request body"
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 401 Unauthorized
|
|
272
|
+
{
|
|
273
|
+
"error": {
|
|
274
|
+
"code": "UNAUTHORIZED",
|
|
275
|
+
"message": "Authentication required"
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 403 Forbidden
|
|
280
|
+
{
|
|
281
|
+
"error": {
|
|
282
|
+
"code": "FORBIDDEN",
|
|
283
|
+
"message": "Insufficient permissions to access this resource"
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// 404 Not Found
|
|
288
|
+
{
|
|
289
|
+
"error": {
|
|
290
|
+
"code": "NOT_FOUND",
|
|
291
|
+
"message": "User with ID 123 not found"
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 409 Conflict
|
|
296
|
+
{
|
|
297
|
+
"error": {
|
|
298
|
+
"code": "CONFLICT",
|
|
299
|
+
"message": "Email already exists"
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 500 Internal Server Error
|
|
304
|
+
{
|
|
305
|
+
"error": {
|
|
306
|
+
"code": "INTERNAL_ERROR",
|
|
307
|
+
"message": "An unexpected error occurred"
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Pagination
|
|
315
|
+
|
|
316
|
+
### Query Parameters
|
|
317
|
+
|
|
318
|
+
```http
|
|
319
|
+
GET /api/users?page=2&limit=20
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Response Format
|
|
323
|
+
|
|
324
|
+
```json
|
|
325
|
+
{
|
|
326
|
+
"data": [...],
|
|
327
|
+
"meta": {
|
|
328
|
+
"page": 2,
|
|
329
|
+
"limit": 20,
|
|
330
|
+
"total": 150,
|
|
331
|
+
"totalPages": 8
|
|
332
|
+
},
|
|
333
|
+
"links": {
|
|
334
|
+
"first": "/api/users?page=1&limit=20",
|
|
335
|
+
"prev": "/api/users?page=1&limit=20",
|
|
336
|
+
"self": "/api/users?page=2&limit=20",
|
|
337
|
+
"next": "/api/users?page=3&limit=20",
|
|
338
|
+
"last": "/api/users?page=8&limit=20"
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Filtering and Sorting
|
|
346
|
+
|
|
347
|
+
### Query Parameters
|
|
348
|
+
|
|
349
|
+
```http
|
|
350
|
+
# Filter by status
|
|
351
|
+
GET /api/orders?status=pending
|
|
352
|
+
|
|
353
|
+
# Filter by date range
|
|
354
|
+
GET /api/orders?startDate=2026-01-01&endDate=2026-02-01
|
|
355
|
+
|
|
356
|
+
# Sort ascending
|
|
357
|
+
GET /api/users?sort=email
|
|
358
|
+
|
|
359
|
+
# Sort descending
|
|
360
|
+
GET /api/users?sort=-createdAt
|
|
361
|
+
|
|
362
|
+
# Multiple filters
|
|
363
|
+
GET /api/orders?status=pending&customer=123&sort=-createdAt
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Versioning
|
|
369
|
+
|
|
370
|
+
### URL Versioning (Recommended)
|
|
371
|
+
|
|
372
|
+
```http
|
|
373
|
+
GET /api/v1/users
|
|
374
|
+
GET /api/v2/users
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Header Versioning
|
|
378
|
+
|
|
379
|
+
```http
|
|
380
|
+
GET /api/users
|
|
381
|
+
Accept: application/vnd.api.v1+json
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Best Practices
|
|
387
|
+
|
|
388
|
+
### DO
|
|
389
|
+
|
|
390
|
+
✅ **Use plural nouns**
|
|
391
|
+
|
|
392
|
+
```http
|
|
393
|
+
✅ GET /api/users
|
|
394
|
+
❌ GET /api/user
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
✅ **Return appropriate status codes**
|
|
398
|
+
|
|
399
|
+
```csharp
|
|
400
|
+
// ✅ CORRECT
|
|
401
|
+
[HttpPost]
|
|
402
|
+
public async Task<IActionResult> CreateUser(CreateUserRequest request)
|
|
403
|
+
{
|
|
404
|
+
var user = await _userService.CreateAsync(request);
|
|
405
|
+
return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
✅ **Use DTOs (not entities)**
|
|
410
|
+
|
|
411
|
+
```csharp
|
|
412
|
+
// ✅ CORRECT
|
|
413
|
+
public record UserResponse(int Id, string Email, string Name);
|
|
414
|
+
|
|
415
|
+
[HttpGet("{id}")]
|
|
416
|
+
public async Task<ActionResult<UserResponse>> GetUser(int id)
|
|
417
|
+
{
|
|
418
|
+
var user = await _userService.GetByIdAsync(id);
|
|
419
|
+
return Ok(new UserResponse(user.Id, user.Email, user.Name));
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
✅ **Validate input**
|
|
424
|
+
|
|
425
|
+
```csharp
|
|
426
|
+
public record CreateUserRequest
|
|
427
|
+
{
|
|
428
|
+
[Required]
|
|
429
|
+
[EmailAddress]
|
|
430
|
+
public string Email { get; init; }
|
|
431
|
+
|
|
432
|
+
[Required]
|
|
433
|
+
[MinLength(8)]
|
|
434
|
+
public string Password { get; init; }
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### DON'T
|
|
439
|
+
|
|
440
|
+
❌ **Don't use verbs in URLs**
|
|
441
|
+
|
|
442
|
+
```http
|
|
443
|
+
❌ POST /api/createUser
|
|
444
|
+
✅ POST /api/users
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
❌ **Don't expose internal IDs**
|
|
448
|
+
|
|
449
|
+
```csharp
|
|
450
|
+
// ❌ WRONG (exposes database ID)
|
|
451
|
+
public class User
|
|
452
|
+
{
|
|
453
|
+
public int Id { get; set; } // Internal DB ID
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ✅ CORRECT (use public UUID)
|
|
457
|
+
public class UserResponse
|
|
458
|
+
{
|
|
459
|
+
public Guid PublicId { get; init; }
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
❌ **Don't return entities directly**
|
|
464
|
+
|
|
465
|
+
```csharp
|
|
466
|
+
// ❌ WRONG
|
|
467
|
+
[HttpGet]
|
|
468
|
+
public async Task<List<User>> GetUsers()
|
|
469
|
+
{
|
|
470
|
+
return await _context.Users.ToListAsync(); // Exposes all properties!
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ✅ CORRECT
|
|
474
|
+
[HttpGet]
|
|
475
|
+
public async Task<List<UserResponse>> GetUsers()
|
|
476
|
+
{
|
|
477
|
+
var users = await _context.Users.ToListAsync();
|
|
478
|
+
return users.Select(u => new UserResponse(u.Id, u.Email, u.Name)).ToList();
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## Related Standards
|
|
485
|
+
|
|
486
|
+
- [Error Handling](./error-handling.md)
|
|
487
|
+
- [Validation](./validation.md)
|
|
488
|
+
- [Minimal API](./ minimal-api.md)
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
*MORPH-SPEC by Polymorphism Tech*
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Status Validation Pattern
|
|
2
|
+
|
|
3
|
+
> **Scope:** universal
|
|
4
|
+
> **Layer:** 0 (always load)
|
|
5
|
+
> **Keywords:** validation, fluent, dto, modelstate, fluentvalidation
|
|
6
|
+
> **Load When:** always
|
|
7
|
+
|
|
8
|
+
**MORPH-SPEC Standard** — Validate what is INVALID, not what is valid.
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Validations assuming a single flow break when new flows are added.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
Original: Created → PendingPayment → Processing → Completed → EmailSent
|
|
16
|
+
Free: Created → Processing → Completed → EmailSent (skips payment)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```csharp
|
|
20
|
+
// ❌ Assumes single flow — breaks free flow
|
|
21
|
+
if (order.Status != OrderStatus.PendingPayment)
|
|
22
|
+
throw new InvalidOperationException("Must be pending payment");
|
|
23
|
+
|
|
24
|
+
// ✅ Validates invalid states — works for all flows
|
|
25
|
+
if (order.Status >= OrderStatus.Completed || order.Status == OrderStatus.Failed)
|
|
26
|
+
throw new InvalidOperationException("Order already completed or failed");
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Enum Design
|
|
30
|
+
|
|
31
|
+
```csharp
|
|
32
|
+
public enum OrderStatus
|
|
33
|
+
{
|
|
34
|
+
Created = 0, PendingPayment = 1, Processing = 2, Completed = 3, EmailSent = 4,
|
|
35
|
+
// Error states (separated range)
|
|
36
|
+
Failed = 100, Cancelled = 101, Refunded = 102
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Validation Patterns
|
|
41
|
+
|
|
42
|
+
| Pattern | When | Example |
|
|
43
|
+
|---------|------|---------|
|
|
44
|
+
| Final state check | Block after completion | `if (status >= OrderStatus.Completed)` |
|
|
45
|
+
| Error state list | Block specific errors | `if (new[] { Failed, Cancelled, Refunded }.Contains(status))` |
|
|
46
|
+
| Range comparison | Block finalized + errors | `if (status >= Completed \|\| status >= Failed)` |
|
|
47
|
+
|
|
48
|
+
## Anti-Patterns
|
|
49
|
+
|
|
50
|
+
| Anti-Pattern | Problem | Fix |
|
|
51
|
+
|-------------|---------|-----|
|
|
52
|
+
| `if (status != Expected)` | Assumes single flow | Validate invalid states instead |
|
|
53
|
+
| Exhaustive switch on valid states | Must modify for each new status | Validate only invalid states |
|
|
54
|
+
| Business logic in state validator | Mixed concerns | Separate state validation from business rules |
|
|
55
|
+
|
|
56
|
+
## Complete Example
|
|
57
|
+
|
|
58
|
+
```csharp
|
|
59
|
+
public async Task<Result> ProcessOrderAsync(Guid orderId, CancellationToken ct)
|
|
60
|
+
{
|
|
61
|
+
var order = await _repository.GetByIdAsync(orderId, ct);
|
|
62
|
+
if (order == null) return Result.Failure("Order not found");
|
|
63
|
+
|
|
64
|
+
// State validation (invalid states)
|
|
65
|
+
if (order.Status >= OrderStatus.Completed) return Result.Failure("Already completed");
|
|
66
|
+
if (order.Status == OrderStatus.Failed) return Result.Failure("Cannot process failed order");
|
|
67
|
+
if (order.Status == OrderStatus.Cancelled) return Result.Failure("Cannot process cancelled order");
|
|
68
|
+
|
|
69
|
+
// Business validation (separate)
|
|
70
|
+
if (order.RequiresPayment && !order.IsPaid) return Result.Failure("Payment required");
|
|
71
|
+
|
|
72
|
+
order.MarkAsProcessing();
|
|
73
|
+
await _repository.UpdateAsync(order, ct);
|
|
74
|
+
return Result.Success();
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Checklist
|
|
79
|
+
|
|
80
|
+
- [ ] Validates INVALID states (not valid)?
|
|
81
|
+
- [ ] Uses ordered enum comparison where possible?
|
|
82
|
+
- [ ] Error states treated separately (100+ range)?
|
|
83
|
+
- [ ] All flows documented in decisions.md?
|
|
84
|
+
- [ ] Tested with all possible flows?
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
*MORPH-SPEC by Polymorphism Tech*
|