@martinffx/atelier 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +94 -253
- package/package.json +1 -1
- package/skills/{spec-orchestrator → atelier-orchestrator}/SKILL.md +3 -3
- package/skills/atelier-setup/SKILL.md +80 -0
- package/skills/code-review/references/reviewers.md +3 -3
- package/skills/code-review/references/rq.md +6 -6
- package/skills/spec-brainstorm/SKILL.md +5 -5
- package/skills/spec-implement/SKILL.md +2 -3
- package/skills/spec-plan/SKILL.md +21 -29
- package/skills/spec-plan/references/plan_template.md +77 -0
- package/skills/python-architecture/SKILL.md +0 -148
- package/skills/python-architecture/references/data-modeling.md +0 -558
- package/skills/python-architecture/references/ddd.md +0 -569
- package/skills/python-architecture/references/functional-core.md +0 -337
- package/skills/python-build-tools/SKILL.md +0 -153
- package/skills/python-build-tools/references/basedpyright.md +0 -321
- package/skills/python-build-tools/references/ruff.md +0 -225
- package/skills/python-build-tools/references/uv.md +0 -257
- package/skills/python-fastapi/SKILL.md +0 -184
- package/skills/python-fastapi/references/api-design.md +0 -563
- package/skills/python-fastapi/references/dependencies.md +0 -479
- package/skills/python-fastapi/references/middleware.md +0 -475
- package/skills/python-fastapi/references/validation.md +0 -497
- package/skills/python-modern-python/SKILL.md +0 -201
- package/skills/python-modern-python/references/async.md +0 -76
- package/skills/python-modern-python/references/pattern-matching.md +0 -89
- package/skills/python-modern-python/references/typing.md +0 -96
- package/skills/python-monorepo/SKILL.md +0 -146
- package/skills/python-monorepo/references/docker.md +0 -515
- package/skills/python-monorepo/references/namespace-packages.md +0 -426
- package/skills/python-monorepo/references/workspace-config.md +0 -341
- package/skills/python-sqlalchemy/SKILL.md +0 -119
- package/skills/python-sqlalchemy/references/async.md +0 -51
- package/skills/python-sqlalchemy/references/models.md +0 -67
- package/skills/python-sqlalchemy/references/queries.md +0 -57
- package/skills/python-temporal/SKILL.md +0 -128
- package/skills/python-temporal/references/patterns.md +0 -87
- package/skills/python-temporal/references/testing.md +0 -54
- package/skills/python-testing/SKILL.md +0 -161
- package/skills/python-testing/references/boundaries.md +0 -449
- package/skills/python-testing/references/mocking.md +0 -449
- package/skills/python-testing/references/pytest.md +0 -524
- package/skills/typescript-api-design/SKILL.md +0 -396
- package/skills/typescript-api-design/references/error-responses.md +0 -469
- package/skills/typescript-build-tools/SKILL.md +0 -447
- package/skills/typescript-build-tools/references/biome.md +0 -436
- package/skills/typescript-build-tools/references/bun.md +0 -432
- package/skills/typescript-build-tools/references/turborepo.md +0 -426
- package/skills/typescript-build-tools/references/vitest.md +0 -518
- package/skills/typescript-drizzle-orm/SKILL.md +0 -398
- package/skills/typescript-drizzle-orm/references/cloudflare.md +0 -392
- package/skills/typescript-drizzle-orm/references/entity-pattern.md +0 -412
- package/skills/typescript-drizzle-orm/references/postgresql.md +0 -191
- package/skills/typescript-drizzle-orm/references/repository-pattern.md +0 -479
- package/skills/typescript-drizzle-orm/references/sqlite.md +0 -291
- package/skills/typescript-dynamodb-toolbox/SKILL.md +0 -370
- package/skills/typescript-dynamodb-toolbox/references/entity-layer.md +0 -574
- package/skills/typescript-dynamodb-toolbox/references/error-handling.md +0 -495
- package/skills/typescript-dynamodb-toolbox/references/modeling.md +0 -332
- package/skills/typescript-dynamodb-toolbox/references/repository-layer.md +0 -631
- package/skills/typescript-dynamodb-toolbox/references/testing.md +0 -718
- package/skills/typescript-dynamodb-toolbox/references/transactions.md +0 -423
- package/skills/typescript-effect-ts/SKILL.md +0 -341
- package/skills/typescript-effect-ts/references/core.md +0 -412
- package/skills/typescript-effect-ts/references/error-handling.md +0 -468
- package/skills/typescript-effect-ts/references/resources.md +0 -476
- package/skills/typescript-effect-ts/references/schema.md +0 -536
- package/skills/typescript-effect-ts/references/services.md +0 -461
- package/skills/typescript-fastify/SKILL.md +0 -405
- package/skills/typescript-fastify/references/plugins.md +0 -329
- package/skills/typescript-fastify/references/typeid.md +0 -315
- package/skills/typescript-functional-patterns/SKILL.md +0 -402
- package/skills/typescript-functional-patterns/references/adts.md +0 -497
- package/skills/typescript-functional-patterns/references/branded-types.md +0 -542
- package/skills/typescript-functional-patterns/references/migration-guide.md +0 -695
- package/skills/typescript-functional-patterns/references/option-result.md +0 -585
- package/skills/typescript-testing/SKILL.md +0 -353
- package/skills/typescript-testing/references/mocking.md +0 -675
- package/skills/typescript-testing/references/msw.md +0 -852
- package/skills/typescript-testing/references/snapshot-testing.md +0 -774
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: typescript-api-design
|
|
3
|
-
description: REST API design patterns. Use when designing endpoints, error responses, pagination, versioning, or API structure. Framework-agnostic principles for building consistent, maintainable APIs.
|
|
4
|
-
user-invocable: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# API Design Patterns
|
|
8
|
-
|
|
9
|
-
Best practices for designing REST APIs with consistent structure, error handling, and resource patterns.
|
|
10
|
-
|
|
11
|
-
## Additional References
|
|
12
|
-
|
|
13
|
-
- [references/error-responses.md](./references/error-responses.md) - Detailed error handling examples
|
|
14
|
-
|
|
15
|
-
## Resource Naming
|
|
16
|
-
|
|
17
|
-
Use consistent, predictable URL patterns:
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
# Collection resources (plural nouns)
|
|
21
|
-
GET /api/v1/users # List users
|
|
22
|
-
POST /api/v1/users # Create user
|
|
23
|
-
GET /api/v1/users/:id # Get user
|
|
24
|
-
PUT /api/v1/users/:id # Update user (full)
|
|
25
|
-
PATCH /api/v1/users/:id # Update user (partial)
|
|
26
|
-
DELETE /api/v1/users/:id # Delete user
|
|
27
|
-
|
|
28
|
-
# Nested resources
|
|
29
|
-
GET /api/v1/users/:userId/posts # List user's posts
|
|
30
|
-
POST /api/v1/users/:userId/posts # Create post for user
|
|
31
|
-
GET /api/v1/users/:userId/posts/:postId # Get specific post
|
|
32
|
-
|
|
33
|
-
# Actions (use verbs sparingly)
|
|
34
|
-
POST /api/v1/users/:id/activate # Activate user
|
|
35
|
-
POST /api/v1/posts/:id/publish # Publish post
|
|
36
|
-
POST /api/v1/invoices/:id/send # Send invoice
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### Guidelines
|
|
40
|
-
|
|
41
|
-
- Use plural nouns for collections (`/users`, not `/user`)
|
|
42
|
-
- Use lowercase with hyphens for multi-word resources (`/ledger-accounts`)
|
|
43
|
-
- Avoid deep nesting (max 2 levels: `/users/:id/posts/:id`)
|
|
44
|
-
- Use query parameters for filtering, sorting, pagination
|
|
45
|
-
- Use verbs only for actions that don't fit CRUD (activate, publish, send)
|
|
46
|
-
|
|
47
|
-
## API Versioning
|
|
48
|
-
|
|
49
|
-
Version APIs in the URL path:
|
|
50
|
-
|
|
51
|
-
```
|
|
52
|
-
/api/v1/users
|
|
53
|
-
/api/v2/users
|
|
54
|
-
|
|
55
|
-
# Not in headers (harder to test/debug)
|
|
56
|
-
# Not in query params (breaks caching)
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Version Strategy
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
// v1/routes.ts
|
|
63
|
-
export async function v1Routes(app: FastifyInstance) {
|
|
64
|
-
app.get('/users', getUsersV1)
|
|
65
|
-
app.post('/users', createUserV1)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// v2/routes.ts
|
|
69
|
-
export async function v2Routes(app: FastifyInstance) {
|
|
70
|
-
app.get('/users', getUsersV2) // Breaking change in response structure
|
|
71
|
-
app.post('/users', createUserV2)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// server.ts
|
|
75
|
-
app.register(v1Routes, { prefix: '/api/v1' })
|
|
76
|
-
app.register(v2Routes, { prefix: '/api/v2' })
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
## RFC 7807 Problem Details
|
|
80
|
-
|
|
81
|
-
Standardized error response format:
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
interface ProblemDetail {
|
|
85
|
-
type: string // Error type identifier
|
|
86
|
-
status: number // HTTP status code
|
|
87
|
-
title: string // Short, human-readable summary
|
|
88
|
-
detail: string // Specific explanation for this occurrence
|
|
89
|
-
instance: string // URI reference to specific occurrence
|
|
90
|
-
traceId: string // Request trace ID for debugging
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Example error response
|
|
94
|
-
{
|
|
95
|
-
"type": "NOT_FOUND",
|
|
96
|
-
"status": 404,
|
|
97
|
-
"title": "Not Found",
|
|
98
|
-
"detail": "User with ID usr_01h455vb4pex5vsknk084sn02q not found",
|
|
99
|
-
"instance": "/api/v1/users/usr_01h455vb4pex5vsknk084sn02q",
|
|
100
|
-
"traceId": "req_abc123xyz"
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Error Types
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
// Domain error base class
|
|
108
|
-
abstract class AppError extends Error {
|
|
109
|
-
abstract readonly status: number
|
|
110
|
-
abstract readonly type: string
|
|
111
|
-
|
|
112
|
-
constructor(message: string, public readonly context?: ErrorContext) {
|
|
113
|
-
super(message)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
toResponse(instance: string, traceId: string): ProblemDetail {
|
|
117
|
-
return {
|
|
118
|
-
type: this.type,
|
|
119
|
-
status: this.status,
|
|
120
|
-
title: this.name,
|
|
121
|
-
detail: this.message,
|
|
122
|
-
instance,
|
|
123
|
-
traceId,
|
|
124
|
-
...this.context,
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Specific error types
|
|
130
|
-
class NotFoundError extends AppError {
|
|
131
|
-
readonly status = 404
|
|
132
|
-
readonly type = 'NOT_FOUND'
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
class ConflictError extends AppError {
|
|
136
|
-
readonly status = 409
|
|
137
|
-
readonly type = 'CONFLICT'
|
|
138
|
-
|
|
139
|
-
constructor(
|
|
140
|
-
message: string,
|
|
141
|
-
public readonly retryable: boolean = false,
|
|
142
|
-
context?: ErrorContext
|
|
143
|
-
) {
|
|
144
|
-
super(message, context)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
class ServiceUnavailableError extends AppError {
|
|
149
|
-
readonly status = 503
|
|
150
|
-
readonly type = 'SERVICE_UNAVAILABLE'
|
|
151
|
-
|
|
152
|
-
constructor(
|
|
153
|
-
message: string,
|
|
154
|
-
public readonly retryable: boolean = true,
|
|
155
|
-
context?: ErrorContext
|
|
156
|
-
) {
|
|
157
|
-
super(message, context)
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
See [references/error-responses.md](./references/error-responses.md) for complete examples.
|
|
163
|
-
|
|
164
|
-
## Pagination (Cursor-Based)
|
|
165
|
-
|
|
166
|
-
Use cursor-based pagination for large datasets:
|
|
167
|
-
|
|
168
|
-
```typescript
|
|
169
|
-
// Request
|
|
170
|
-
GET /api/v1/posts?limit=20&cursor=pst_01h455vb4pex5vsknk084sn02q
|
|
171
|
-
|
|
172
|
-
// Response
|
|
173
|
-
{
|
|
174
|
-
"items": [
|
|
175
|
-
{ "id": "pst_01h455w3x8k5z9y7q1m0n2b3c4", ... },
|
|
176
|
-
{ "id": "pst_01h455x2y9l6a0z8r2n1o3c5d6", ... }
|
|
177
|
-
],
|
|
178
|
-
"nextCursor": "pst_01h455z1a0m7b8y9s3o2p4d6e7",
|
|
179
|
-
"hasMore": true
|
|
180
|
-
}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### Implementation
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
interface PaginatedRequest {
|
|
187
|
-
limit?: number // Max items to return (default 20, max 100)
|
|
188
|
-
cursor?: string // Cursor for next page (opaque to client)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
interface PaginatedResponse<T> {
|
|
192
|
-
items: T[]
|
|
193
|
-
nextCursor?: string
|
|
194
|
-
hasMore: boolean
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function listPosts(req: PaginatedRequest): Promise<PaginatedResponse<Post>> {
|
|
198
|
-
const limit = Math.min(req.limit ?? 20, 100)
|
|
199
|
-
const queryLimit = limit + 1 // Fetch one extra to check hasMore
|
|
200
|
-
|
|
201
|
-
const posts = await db.query.posts.findMany({
|
|
202
|
-
where: req.cursor ? gt(posts.id, req.cursor) : undefined,
|
|
203
|
-
orderBy: desc(posts.createdAt),
|
|
204
|
-
limit: queryLimit,
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
const hasMore = posts.length > limit
|
|
208
|
-
const items = posts.slice(0, limit)
|
|
209
|
-
const nextCursor = hasMore ? items[items.length - 1].id : undefined
|
|
210
|
-
|
|
211
|
-
return { items, nextCursor, hasMore }
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### Why Cursor Over Offset
|
|
216
|
-
|
|
217
|
-
```
|
|
218
|
-
❌ Offset-based (/posts?offset=40&limit=20)
|
|
219
|
-
- Unstable: Items can shift if new records inserted
|
|
220
|
-
- Performance: DB must scan all previous rows
|
|
221
|
-
- Inaccurate: Can miss or duplicate items
|
|
222
|
-
|
|
223
|
-
✅ Cursor-based (/posts?cursor=pst_xyz&limit=20)
|
|
224
|
-
- Stable: Cursor points to specific item
|
|
225
|
-
- Performant: DB uses index seek
|
|
226
|
-
- Accurate: No gaps or duplicates
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
## Filtering & Sorting
|
|
230
|
-
|
|
231
|
-
Use query parameters for filtering and sorting:
|
|
232
|
-
|
|
233
|
-
```
|
|
234
|
-
# Filtering
|
|
235
|
-
GET /api/v1/users?status=active&role=admin
|
|
236
|
-
GET /api/v1/posts?author=usr_abc&published=true
|
|
237
|
-
|
|
238
|
-
# Sorting
|
|
239
|
-
GET /api/v1/posts?sort=-createdAt # Descending (- prefix)
|
|
240
|
-
GET /api/v1/users?sort=name # Ascending
|
|
241
|
-
|
|
242
|
-
# Combined
|
|
243
|
-
GET /api/v1/posts?author=usr_abc&status=published&sort=-createdAt&limit=20
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### Implementation
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
interface ListPostsQuery {
|
|
250
|
-
author?: string
|
|
251
|
-
status?: 'draft' | 'published'
|
|
252
|
-
sort?: 'createdAt' | '-createdAt' | 'title' | '-title'
|
|
253
|
-
limit?: number
|
|
254
|
-
cursor?: string
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
async function listPosts(query: ListPostsQuery): Promise<PaginatedResponse<Post>> {
|
|
258
|
-
const conditions = []
|
|
259
|
-
|
|
260
|
-
if (query.author) {
|
|
261
|
-
conditions.push(eq(posts.authorId, query.author))
|
|
262
|
-
}
|
|
263
|
-
if (query.status) {
|
|
264
|
-
conditions.push(eq(posts.status, query.status))
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const orderByColumn = query.sort?.startsWith('-')
|
|
268
|
-
? query.sort.slice(1)
|
|
269
|
-
: query.sort ?? 'createdAt'
|
|
270
|
-
const orderByDirection = query.sort?.startsWith('-') ? desc : asc
|
|
271
|
-
|
|
272
|
-
return await db.query.posts.findMany({
|
|
273
|
-
where: conditions.length > 0 ? and(...conditions) : undefined,
|
|
274
|
-
orderBy: orderByDirection(posts[orderByColumn]),
|
|
275
|
-
limit: query.limit ?? 20,
|
|
276
|
-
})
|
|
277
|
-
}
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
## HTTP Status Codes
|
|
281
|
-
|
|
282
|
-
Use status codes consistently:
|
|
283
|
-
|
|
284
|
-
```
|
|
285
|
-
# Success
|
|
286
|
-
200 OK # Successful GET, PUT, PATCH
|
|
287
|
-
201 Created # Successful POST (include Location header)
|
|
288
|
-
204 No Content # Successful DELETE, PUT with no response body
|
|
289
|
-
|
|
290
|
-
# Client Errors
|
|
291
|
-
400 Bad Request # Invalid request body/parameters
|
|
292
|
-
401 Unauthorized # Missing or invalid authentication
|
|
293
|
-
403 Forbidden # Valid auth, but lacks permission
|
|
294
|
-
404 Not Found # Resource doesn't exist
|
|
295
|
-
409 Conflict # Resource already exists, optimistic lock failure
|
|
296
|
-
422 Unprocessable # Validation error (semantic)
|
|
297
|
-
429 Too Many Requests # Rate limit exceeded
|
|
298
|
-
|
|
299
|
-
# Server Errors
|
|
300
|
-
500 Internal Server Error # Unexpected error
|
|
301
|
-
503 Service Unavailable # Temporary unavailability, retry later
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
## Response Envelope (When to Use)
|
|
305
|
-
|
|
306
|
-
**Don't use envelopes for simple CRUD:**
|
|
307
|
-
|
|
308
|
-
```typescript
|
|
309
|
-
// ❌ Unnecessary wrapping
|
|
310
|
-
GET /api/v1/users/123
|
|
311
|
-
{
|
|
312
|
-
"success": true,
|
|
313
|
-
"data": { "id": "123", "name": "Alice" }
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// ✅ Return resource directly
|
|
317
|
-
GET /api/v1/users/123
|
|
318
|
-
{
|
|
319
|
-
"id": "123",
|
|
320
|
-
"name": "Alice"
|
|
321
|
-
}
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
**Use envelopes for pagination:**
|
|
325
|
-
|
|
326
|
-
```typescript
|
|
327
|
-
// ✅ Envelope needed for metadata
|
|
328
|
-
GET /api/v1/users?limit=20
|
|
329
|
-
{
|
|
330
|
-
"items": [...],
|
|
331
|
-
"nextCursor": "usr_xyz",
|
|
332
|
-
"hasMore": true
|
|
333
|
-
}
|
|
334
|
-
```
|
|
335
|
-
|
|
336
|
-
## Timestamps
|
|
337
|
-
|
|
338
|
-
Use ISO 8601 format for all timestamps:
|
|
339
|
-
|
|
340
|
-
```typescript
|
|
341
|
-
{
|
|
342
|
-
"createdAt": "2024-01-15T14:30:00.000Z", // ISO 8601 UTC
|
|
343
|
-
"updatedAt": "2024-01-16T09:15:30.123Z"
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// In entities
|
|
347
|
-
toResponse(): UserResponse {
|
|
348
|
-
return {
|
|
349
|
-
...
|
|
350
|
-
createdAt: this.createdAt.toISOString(), // Date → ISO string
|
|
351
|
-
updatedAt: this.updatedAt.toISOString(),
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
## Idempotency
|
|
357
|
-
|
|
358
|
-
Use idempotency keys for safe retries:
|
|
359
|
-
|
|
360
|
-
```typescript
|
|
361
|
-
// Request
|
|
362
|
-
POST /api/v1/transactions
|
|
363
|
-
Headers:
|
|
364
|
-
Idempotency-Key: txn_abc123xyz
|
|
365
|
-
Body:
|
|
366
|
-
{ "amount": 100, "from": "usr_123", "to": "usr_456" }
|
|
367
|
-
|
|
368
|
-
// Implementation
|
|
369
|
-
async function createTransaction(rq: CreateTransactionRequest, idempotencyKey: string) {
|
|
370
|
-
// Check if transaction with this key already exists
|
|
371
|
-
const existing = await db.query.transactions.findFirst({
|
|
372
|
-
where: eq(transactions.idempotencyKey, idempotencyKey),
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
if (existing) {
|
|
376
|
-
return TransactionEntity.fromRecord(existing) // Return existing
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Create new transaction
|
|
380
|
-
const transaction = TransactionEntity.fromRequest(rq, idempotencyKey)
|
|
381
|
-
return await transactionRepo.create(transaction)
|
|
382
|
-
}
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
## Guidelines
|
|
386
|
-
|
|
387
|
-
1. **Plural nouns** - Collections use plural resource names
|
|
388
|
-
2. **Lowercase with hyphens** - Multi-word resources like `ledger-accounts`
|
|
389
|
-
3. **Version in URL** - `/api/v1/`, `/api/v2/` for breaking changes
|
|
390
|
-
4. **RFC 7807 errors** - Standardized error response format
|
|
391
|
-
5. **Cursor pagination** - For large datasets (more stable than offset)
|
|
392
|
-
6. **Query params** - For filtering, sorting, pagination (not in path)
|
|
393
|
-
7. **HTTP status codes** - Use correct codes (200, 201, 204, 400, 404, 409, 500, 503)
|
|
394
|
-
8. **ISO 8601 timestamps** - Always use `.toISOString()` for dates
|
|
395
|
-
9. **Idempotency keys** - For non-idempotent operations (POST, PATCH)
|
|
396
|
-
10. **No unnecessary envelopes** - Return resources directly unless pagination needed
|