@torus-engineering/tas-kit 1.11.1 → 1.13.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 (123) hide show
  1. package/.tas/README.md +334 -334
  2. package/{.claude → .tas/_platform/claude-code}/settings.json +0 -12
  3. package/{.claude → .tas/_platform}/hooks/code-quality.js +1 -1
  4. package/{.claude → .tas/_platform}/hooks/session-end.js +20 -25
  5. package/{.claude → .tas}/commands/ado-create.md +5 -4
  6. package/{.claude → .tas}/commands/ado-delete.md +5 -4
  7. package/{.claude → .tas}/commands/ado-update.md +5 -4
  8. package/{.claude → .tas}/commands/tas-adr.md +3 -3
  9. package/{.claude → .tas}/commands/tas-apitest-plan.md +2 -2
  10. package/{.claude → .tas}/commands/tas-apitest.md +4 -4
  11. package/{.claude → .tas}/commands/tas-bug.md +6 -6
  12. package/{.claude → .tas}/commands/tas-design.md +3 -3
  13. package/{.claude → .tas}/commands/tas-dev.md +11 -14
  14. package/{.claude → .tas}/commands/tas-epic.md +3 -3
  15. package/{.claude → .tas}/commands/tas-feature.md +4 -4
  16. package/{.claude → .tas}/commands/tas-fix.md +5 -5
  17. package/{.claude → .tas}/commands/tas-init.md +1 -1
  18. package/{.claude → .tas}/commands/tas-plan.md +198 -198
  19. package/{.claude → .tas}/commands/tas-prd.md +3 -3
  20. package/{.claude → .tas}/commands/tas-review.md +17 -15
  21. package/{.claude → .tas}/commands/tas-sad.md +3 -3
  22. package/{.claude → .tas}/commands/tas-security.md +4 -4
  23. package/{.claude → .tas}/commands/tas-story.md +3 -3
  24. package/.tas/platforms.json +5 -0
  25. package/.tas/project-status-example.yaml +17 -17
  26. package/{.claude/skills/ado-integration/SKILL.md → .tas/rules/ado-integration.md} +5 -15
  27. package/{.claude/skills/api-design/SKILL.md → .tas/rules/common/api-design.md} +517 -530
  28. package/{.claude → .tas}/rules/common/code-review.md +30 -6
  29. package/{.claude/rules/common/post-review-agent.md → .tas/rules/common/post-implementation-review.md} +51 -49
  30. package/{.claude → .tas}/rules/common/project-status.md +80 -80
  31. package/{.claude → .tas}/rules/common/stack-detection.md +29 -29
  32. package/.tas/{checklists → rules/common}/story-done.md +12 -5
  33. package/{.claude/skills/tas-tdd/SKILL.md → .tas/rules/common/tdd.md} +4 -38
  34. package/{.claude → .tas}/rules/common/testing.md +3 -8
  35. package/{.claude → .tas}/rules/common/token-logging.md +36 -27
  36. package/{.claude → .tas}/rules/csharp/api-testing.md +171 -171
  37. package/{.claude → .tas}/rules/csharp/coding-style.md +0 -2
  38. package/{.claude → .tas}/rules/csharp/security.md +10 -0
  39. package/{.claude → .tas}/rules/python/coding-style.md +0 -2
  40. package/{.claude → .tas}/rules/typescript/coding-style.md +0 -2
  41. package/.tas/rules/typescript/patterns.md +142 -0
  42. package/.tas/rules/typescript/security.md +88 -0
  43. package/{.claude → .tas}/rules/typescript/testing.md +0 -4
  44. package/{.claude → .tas}/rules/web/coding-style.md +0 -2
  45. package/.tas/tas-example.yaml +125 -126
  46. package/.tas/templates/ADR.md +47 -47
  47. package/.tas/templates/Bug.md +67 -67
  48. package/.tas/templates/Design-Spec.md +36 -36
  49. package/.tas/templates/Epic.md +46 -46
  50. package/.tas/templates/Feature.md +1 -1
  51. package/.tas/templates/Security-Report.md +27 -27
  52. package/.tas/tools/tas-ado-readme.md +169 -169
  53. package/.tas/tools/tas-ado.py +621 -621
  54. package/README.md +334 -334
  55. package/bin/cli.js +91 -73
  56. package/lib/adapters/antigravity.js +131 -0
  57. package/lib/adapters/claude-code.js +35 -0
  58. package/lib/adapters/codex.js +157 -0
  59. package/lib/adapters/cursor.js +80 -0
  60. package/lib/adapters/index.js +20 -0
  61. package/lib/adapters/utils.js +81 -0
  62. package/lib/deleted-files.json +99 -0
  63. package/lib/install.js +543 -327
  64. package/package.json +5 -4
  65. package/.claude/agents/code-reviewer.md +0 -41
  66. package/.claude/agents/e2e-runner.md +0 -61
  67. package/.claude/agents/planner.md +0 -82
  68. package/.claude/agents/tdd-guide.md +0 -84
  69. package/.claude/commands/tas-verify.md +0 -51
  70. package/.claude/rules/typescript/patterns.md +0 -62
  71. package/.claude/rules/typescript/security.md +0 -28
  72. package/.claude/settings.local.json +0 -38
  73. package/.claude/skills/ai-regression-testing/SKILL.md +0 -364
  74. package/.claude/skills/architecture-decision-records/SKILL.md +0 -184
  75. package/.claude/skills/benchmark/SKILL.md +0 -98
  76. package/.claude/skills/browser-qa/SKILL.md +0 -92
  77. package/.claude/skills/canary-watch/SKILL.md +0 -104
  78. package/.claude/skills/js-backend-patterns/SKILL.md +0 -603
  79. package/.claude/skills/tas-conventions/SKILL.md +0 -65
  80. package/.claude/skills/tas-implementation-complete/SKILL.md +0 -100
  81. package/.claude/skills/token-logger/SKILL.md +0 -19
  82. package/.tas/checklists/code-review.md +0 -29
  83. package/.tas/checklists/security.md +0 -21
  84. /package/{.claude → .tas}/agents/architect.md +0 -0
  85. /package/{.claude → .tas}/agents/aws-reviewer.md +0 -0
  86. /package/{.claude → .tas}/agents/build-resolver.md +0 -0
  87. /package/{.claude → .tas}/agents/code-explorer.md +0 -0
  88. /package/{.claude → .tas}/agents/csharp-reviewer.md +0 -0
  89. /package/{.claude → .tas}/agents/database-reviewer.md +0 -0
  90. /package/{.claude → .tas}/agents/doc-updater.md +0 -0
  91. /package/{.claude → .tas}/agents/python-reviewer.md +0 -0
  92. /package/{.claude → .tas}/agents/security-reviewer.md +0 -0
  93. /package/{.claude → .tas}/agents/typescript-reviewer.md +0 -0
  94. /package/{.claude → .tas}/commands/ado-get.md +0 -0
  95. /package/{.claude → .tas}/commands/ado-status.md +0 -0
  96. /package/{.claude → .tas}/commands/tas-brainstorm.md +0 -0
  97. /package/{.claude → .tas}/commands/tas-e2e-mobile.md +0 -0
  98. /package/{.claude → .tas}/commands/tas-e2e-web.md +0 -0
  99. /package/{.claude → .tas}/commands/tas-e2e.md +0 -0
  100. /package/{.claude → .tas}/commands/tas-functest-mobile.md +0 -0
  101. /package/{.claude → .tas}/commands/tas-functest-web.md +0 -0
  102. /package/{.claude → .tas}/commands/tas-functest.md +0 -0
  103. /package/{.claude → .tas}/commands/tas-spec.md +0 -0
  104. /package/{.claude → .tas}/commands/tas-status.md +0 -0
  105. /package/{.claude → .tas}/rules/.gitkeep +0 -0
  106. /package/{.claude → .tas}/rules/common/hooks.md +0 -0
  107. /package/{.claude → .tas}/rules/common/patterns.md +0 -0
  108. /package/{.claude → .tas}/rules/common/security.md +0 -0
  109. /package/{.claude → .tas}/rules/csharp/hooks.md +0 -0
  110. /package/{.claude → .tas}/rules/csharp/patterns.md +0 -0
  111. /package/{.claude → .tas}/rules/csharp/testing.md +0 -0
  112. /package/{.claude → .tas}/rules/python/hooks.md +0 -0
  113. /package/{.claude → .tas}/rules/python/patterns.md +0 -0
  114. /package/{.claude → .tas}/rules/python/security.md +0 -0
  115. /package/{.claude → .tas}/rules/python/testing.md +0 -0
  116. /package/{.claude → .tas}/rules/typescript/hooks.md +0 -0
  117. /package/{.claude → .tas}/rules/web/design-quality.md +0 -0
  118. /package/{.claude → .tas}/rules/web/hooks.md +0 -0
  119. /package/{.claude → .tas}/rules/web/patterns.md +0 -0
  120. /package/{.claude → .tas}/rules/web/performance.md +0 -0
  121. /package/{.claude → .tas}/rules/web/security.md +0 -0
  122. /package/{.claude → .tas}/rules/web/testing.md +0 -0
  123. /package/{CLAUDE-Example.md → .tas/templates/AGENTS.md} +0 -0
@@ -1,530 +1,517 @@
1
- ---
2
- name: api-design
3
- description: |
4
- Auto-invoke when designing new REST API endpoints, reviewing existing API contracts,
5
- planning pagination or filtering strategy, implementing error response formats,
6
- adding API versioning, or building public/partner-facing APIs.
7
- Used in /tas-plan whenever a Story involves backend API design (any stack) — produces
8
- API Spec (URL structure, HTTP methods, status codes, request/response shapes) before implementation.
9
- NOT for backend implementation layers (use js-backend-patterns for Node.js instead).
10
- origin: ECC
11
- allowed-tools: Read, Grep, Glob
12
- ---
13
-
14
- # API Design Patterns
15
-
16
- Conventions and best practices for designing consistent, developer-friendly REST APIs.
17
-
18
- ## When to Activate
19
-
20
- - Designing new API endpoints
21
- - Reviewing existing API contracts
22
- - Adding pagination, filtering, or sorting
23
- - Implementing error handling for APIs
24
- - Planning API versioning strategy
25
- - Building public or partner-facing APIs
26
-
27
- ## Resource Design
28
-
29
- ### URL Structure
30
-
31
- ```
32
- # Resources are nouns, plural, lowercase, kebab-case
33
- GET /api/v1/users
34
- GET /api/v1/users/:id
35
- POST /api/v1/users
36
- PUT /api/v1/users/:id
37
- PATCH /api/v1/users/:id
38
- DELETE /api/v1/users/:id
39
-
40
- # Sub-resources for relationships
41
- GET /api/v1/users/:id/orders
42
- POST /api/v1/users/:id/orders
43
-
44
- # Actions that don't map to CRUD (use verbs sparingly)
45
- POST /api/v1/orders/:id/cancel
46
- POST /api/v1/auth/login
47
- POST /api/v1/auth/refresh
48
- ```
49
-
50
- ### Naming Rules
51
-
52
- ```
53
- # GOOD
54
- /api/v1/team-members # kebab-case for multi-word resources
55
- /api/v1/orders?status=active # query params for filtering
56
- /api/v1/users/123/orders # nested resources for ownership
57
-
58
- # BAD
59
- /api/v1/getUsers # verb in URL
60
- /api/v1/user # singular (use plural)
61
- /api/v1/team_members # snake_case in URLs
62
- /api/v1/users/123/getOrders # verb in nested resource
63
- ```
64
-
65
- ## HTTP Methods and Status Codes
66
-
67
- ### Method Semantics
68
-
69
- | Method | Idempotent | Safe | Use For |
70
- |--------|-----------|------|---------|
71
- | GET | Yes | Yes | Retrieve resources |
72
- | POST | No | No | Create resources, trigger actions |
73
- | PUT | Yes | No | Full replacement of a resource |
74
- | PATCH | No* | No | Partial update of a resource |
75
- | DELETE | Yes | No | Remove a resource |
76
-
77
- *PATCH can be made idempotent with proper implementation
78
-
79
- ### Status Code Reference
80
-
81
- ```
82
- # Success
83
- 200 OK — GET, PUT, PATCH (with response body)
84
- 201 Created POST (include Location header)
85
- 204 No Content DELETE, PUT (no response body)
86
-
87
- # Client Errors
88
- 400 Bad Request — Validation failure, malformed JSON
89
- 401 Unauthorized — Missing or invalid authentication
90
- 403 Forbidden — Authenticated but not authorized
91
- 404 Not Found — Resource doesn't exist
92
- 409 Conflict — Duplicate entry, state conflict
93
- 422 Unprocessable Entity — Semantically invalid (valid JSON, bad data)
94
- 429 Too Many Requests — Rate limit exceeded
95
-
96
- # Server Errors
97
- 500 Internal Server Error Unexpected failure (never expose details)
98
- 502 Bad Gateway — Upstream service failed
99
- 503 Service Unavailable — Temporary overload, include Retry-After
100
- ```
101
-
102
- ### Common Mistakes
103
-
104
- ```
105
- # BAD: 200 for everything
106
- { "status": 200, "success": false, "error": "Not found" }
107
-
108
- # GOOD: Use HTTP status codes semantically
109
- HTTP/1.1 404 Not Found
110
- { "error": { "code": "not_found", "message": "User not found" } }
111
-
112
- # BAD: 500 for validation errors
113
- # GOOD: 400 or 422 with field-level details
114
-
115
- # BAD: 200 for created resources
116
- # GOOD: 201 with Location header
117
- HTTP/1.1 201 Created
118
- Location: /api/v1/users/abc-123
119
- ```
120
-
121
- ## Response Format
122
-
123
- ### Success Response
124
-
125
- ```json
126
- {
127
- "data": {
128
- "id": "abc-123",
129
- "email": "alice@example.com",
130
- "name": "Alice",
131
- "created_at": "2025-01-15T10:30:00Z"
132
- }
133
- }
134
- ```
135
-
136
- ### Collection Response (with Pagination)
137
-
138
- ```json
139
- {
140
- "data": [
141
- { "id": "abc-123", "name": "Alice" },
142
- { "id": "def-456", "name": "Bob" }
143
- ],
144
- "meta": {
145
- "total": 142,
146
- "page": 1,
147
- "per_page": 20,
148
- "total_pages": 8
149
- },
150
- "links": {
151
- "self": "/api/v1/users?page=1&per_page=20",
152
- "next": "/api/v1/users?page=2&per_page=20",
153
- "last": "/api/v1/users?page=8&per_page=20"
154
- }
155
- }
156
- ```
157
-
158
- ### Error Response
159
-
160
- ```json
161
- {
162
- "error": {
163
- "code": "validation_error",
164
- "message": "Request validation failed",
165
- "details": [
166
- {
167
- "field": "email",
168
- "message": "Must be a valid email address",
169
- "code": "invalid_format"
170
- },
171
- {
172
- "field": "age",
173
- "message": "Must be between 0 and 150",
174
- "code": "out_of_range"
175
- }
176
- ]
177
- }
178
- }
179
- ```
180
-
181
- ### Response Envelope Variants
182
-
183
- ```typescript
184
- // Option A: Envelope with data wrapper (recommended for public APIs)
185
- interface ApiResponse<T> {
186
- data: T;
187
- meta?: PaginationMeta;
188
- links?: PaginationLinks;
189
- }
190
-
191
- interface ApiError {
192
- error: {
193
- code: string;
194
- message: string;
195
- details?: FieldError[];
196
- };
197
- }
198
-
199
- // Option B: Flat response (simpler, common for internal APIs)
200
- // Success: just return the resource directly
201
- // Error: return error object
202
- // Distinguish by HTTP status code
203
- ```
204
-
205
- ## Pagination
206
-
207
- ### Offset-Based (Simple)
208
-
209
- ```
210
- GET /api/v1/users?page=2&per_page=20
211
-
212
- # Implementation
213
- SELECT * FROM users
214
- ORDER BY created_at DESC
215
- LIMIT 20 OFFSET 20;
216
- ```
217
-
218
- **Pros:** Easy to implement, supports "jump to page N"
219
- **Cons:** Slow on large offsets (OFFSET 100000), inconsistent with concurrent inserts
220
-
221
- ### Cursor-Based (Scalable)
222
-
223
- ```
224
- GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20
225
-
226
- # Implementation
227
- SELECT * FROM users
228
- WHERE id > :cursor_id
229
- ORDER BY id ASC
230
- LIMIT 21; -- fetch one extra to determine has_next
231
- ```
232
-
233
- ```json
234
- {
235
- "data": [...],
236
- "meta": {
237
- "has_next": true,
238
- "next_cursor": "eyJpZCI6MTQzfQ"
239
- }
240
- }
241
- ```
242
-
243
- **Pros:** Consistent performance regardless of position, stable with concurrent inserts
244
- **Cons:** Cannot jump to arbitrary page, cursor is opaque
245
-
246
- ### When to Use Which
247
-
248
- | Use Case | Pagination Type |
249
- |----------|----------------|
250
- | Admin dashboards, small datasets (<10K) | Offset |
251
- | Infinite scroll, feeds, large datasets | Cursor |
252
- | Public APIs | Cursor (default) with offset (optional) |
253
- | Search results | Offset (users expect page numbers) |
254
-
255
- ## Filtering, Sorting, and Search
256
-
257
- ### Filtering
258
-
259
- ```
260
- # Simple equality
261
- GET /api/v1/orders?status=active&customer_id=abc-123
262
-
263
- # Comparison operators (use bracket notation)
264
- GET /api/v1/products?price[gte]=10&price[lte]=100
265
- GET /api/v1/orders?created_at[after]=2025-01-01
266
-
267
- # Multiple values (comma-separated)
268
- GET /api/v1/products?category=electronics,clothing
269
-
270
- # Nested fields (dot notation)
271
- GET /api/v1/orders?customer.country=US
272
- ```
273
-
274
- ### Sorting
275
-
276
- ```
277
- # Single field (prefix - for descending)
278
- GET /api/v1/products?sort=-created_at
279
-
280
- # Multiple fields (comma-separated)
281
- GET /api/v1/products?sort=-featured,price,-created_at
282
- ```
283
-
284
- ### Full-Text Search
285
-
286
- ```
287
- # Search query parameter
288
- GET /api/v1/products?q=wireless+headphones
289
-
290
- # Field-specific search
291
- GET /api/v1/users?email=alice
292
- ```
293
-
294
- ### Sparse Fieldsets
295
-
296
- ```
297
- # Return only specified fields (reduces payload)
298
- GET /api/v1/users?fields=id,name,email
299
- GET /api/v1/orders?fields=id,total,status&include=customer.name
300
- ```
301
-
302
- ## Authentication and Authorization
303
-
304
- ### Token-Based Auth
305
-
306
- ```
307
- # Bearer token in Authorization header
308
- GET /api/v1/users
309
- Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
310
-
311
- # API key (for server-to-server)
312
- GET /api/v1/data
313
- X-API-Key: sk_live_abc123
314
- ```
315
-
316
- ### Authorization Patterns
317
-
318
- ```typescript
319
- // Resource-level: check ownership
320
- app.get("/api/v1/orders/:id", async (req, res) => {
321
- const order = await Order.findById(req.params.id);
322
- if (!order) return res.status(404).json({ error: { code: "not_found" } });
323
- if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } });
324
- return res.json({ data: order });
325
- });
326
-
327
- // Role-based: check permissions
328
- app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => {
329
- await User.delete(req.params.id);
330
- return res.status(204).send();
331
- });
332
- ```
333
-
334
- ## Rate Limiting
335
-
336
- ### Headers
337
-
338
- ```
339
- HTTP/1.1 200 OK
340
- X-RateLimit-Limit: 100
341
- X-RateLimit-Remaining: 95
342
- X-RateLimit-Reset: 1640000000
343
-
344
- # When exceeded
345
- HTTP/1.1 429 Too Many Requests
346
- Retry-After: 60
347
- {
348
- "error": {
349
- "code": "rate_limit_exceeded",
350
- "message": "Rate limit exceeded. Try again in 60 seconds."
351
- }
352
- }
353
- ```
354
-
355
- ### Rate Limit Tiers
356
-
357
- | Tier | Limit | Window | Use Case |
358
- |------|-------|--------|----------|
359
- | Anonymous | 30/min | Per IP | Public endpoints |
360
- | Authenticated | 100/min | Per user | Standard API access |
361
- | Premium | 1000/min | Per API key | Paid API plans |
362
- | Internal | 10000/min | Per service | Service-to-service |
363
-
364
- ## Versioning
365
-
366
- ### URL Path Versioning (Recommended)
367
-
368
- ```
369
- /api/v1/users
370
- /api/v2/users
371
- ```
372
-
373
- **Pros:** Explicit, easy to route, cacheable
374
- **Cons:** URL changes between versions
375
-
376
- ### Header Versioning
377
-
378
- ```
379
- GET /api/users
380
- Accept: application/vnd.myapp.v2+json
381
- ```
382
-
383
- **Pros:** Clean URLs
384
- **Cons:** Harder to test, easy to forget
385
-
386
- ### Versioning Strategy
387
-
388
- ```
389
- 1. Start with /api/v1/ — don't version until you need to
390
- 2. Maintain at most 2 active versions (current + previous)
391
- 3. Deprecation timeline:
392
- - Announce deprecation (6 months notice for public APIs)
393
- - Add Sunset header: Sunset: Sat, 01 Jan 2026 00:00:00 GMT
394
- - Return 410 Gone after sunset date
395
- 4. Non-breaking changes don't need a new version:
396
- - Adding new fields to responses
397
- - Adding new optional query parameters
398
- - Adding new endpoints
399
- 5. Breaking changes require a new version:
400
- - Removing or renaming fields
401
- - Changing field types
402
- - Changing URL structure
403
- - Changing authentication method
404
- ```
405
-
406
- ## Implementation Patterns
407
-
408
- ### TypeScript (Next.js API Route)
409
-
410
- ```typescript
411
- import { z } from "zod";
412
- import { NextRequest, NextResponse } from "next/server";
413
-
414
- const createUserSchema = z.object({
415
- email: z.string().email(),
416
- name: z.string().min(1).max(100),
417
- });
418
-
419
- export async function POST(req: NextRequest) {
420
- const body = await req.json();
421
- const parsed = createUserSchema.safeParse(body);
422
-
423
- if (!parsed.success) {
424
- return NextResponse.json({
425
- error: {
426
- code: "validation_error",
427
- message: "Request validation failed",
428
- details: parsed.error.issues.map(i => ({
429
- field: i.path.join("."),
430
- message: i.message,
431
- code: i.code,
432
- })),
433
- },
434
- }, { status: 422 });
435
- }
436
-
437
- const user = await createUser(parsed.data);
438
-
439
- return NextResponse.json(
440
- { data: user },
441
- {
442
- status: 201,
443
- headers: { Location: `/api/v1/users/${user.id}` },
444
- },
445
- );
446
- }
447
- ```
448
-
449
- ### Python (Django REST Framework)
450
-
451
- ```python
452
- from rest_framework import serializers, viewsets, status
453
- from rest_framework.response import Response
454
-
455
- class CreateUserSerializer(serializers.Serializer):
456
- email = serializers.EmailField()
457
- name = serializers.CharField(max_length=100)
458
-
459
- class UserSerializer(serializers.ModelSerializer):
460
- class Meta:
461
- model = User
462
- fields = ["id", "email", "name", "created_at"]
463
-
464
- class UserViewSet(viewsets.ModelViewSet):
465
- serializer_class = UserSerializer
466
- permission_classes = [IsAuthenticated]
467
-
468
- def get_serializer_class(self):
469
- if self.action == "create":
470
- return CreateUserSerializer
471
- return UserSerializer
472
-
473
- def create(self, request):
474
- serializer = CreateUserSerializer(data=request.data)
475
- serializer.is_valid(raise_exception=True)
476
- user = UserService.create(**serializer.validated_data)
477
- return Response(
478
- {"data": UserSerializer(user).data},
479
- status=status.HTTP_201_CREATED,
480
- headers={"Location": f"/api/v1/users/{user.id}"},
481
- )
482
- ```
483
-
484
- ### Go (net/http)
485
-
486
- ```go
487
- func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
488
- var req CreateUserRequest
489
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
490
- writeError(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
491
- return
492
- }
493
-
494
- if err := req.Validate(); err != nil {
495
- writeError(w, http.StatusUnprocessableEntity, "validation_error", err.Error())
496
- return
497
- }
498
-
499
- user, err := h.service.Create(r.Context(), req)
500
- if err != nil {
501
- switch {
502
- case errors.Is(err, domain.ErrEmailTaken):
503
- writeError(w, http.StatusConflict, "email_taken", "Email already registered")
504
- default:
505
- writeError(w, http.StatusInternalServerError, "internal_error", "Internal error")
506
- }
507
- return
508
- }
509
-
510
- w.Header().Set("Location", fmt.Sprintf("/api/v1/users/%s", user.ID))
511
- writeJSON(w, http.StatusCreated, map[string]any{"data": user})
512
- }
513
- ```
514
-
515
- ## API Design Checklist
516
-
517
- Before shipping a new endpoint:
518
-
519
- - [ ] Resource URL follows naming conventions (plural, kebab-case, no verbs)
520
- - [ ] Correct HTTP method used (GET for reads, POST for creates, etc.)
521
- - [ ] Appropriate status codes returned (not 200 for everything)
522
- - [ ] Input validated with schema (Zod, Pydantic, Bean Validation)
523
- - [ ] Error responses follow standard format with codes and messages
524
- - [ ] Pagination implemented for list endpoints (cursor or offset)
525
- - [ ] Authentication required (or explicitly marked as public)
526
- - [ ] Authorization checked (user can only access their own resources)
527
- - [ ] Rate limiting configured
528
- - [ ] Response does not leak internal details (stack traces, SQL errors)
529
- - [ ] Consistent naming with existing endpoints (camelCase vs snake_case)
530
- - [ ] Documented (OpenAPI/Swagger spec updated)
1
+ # API Design Patterns
2
+
3
+ Conventions and best practices for designing consistent, developer-friendly REST APIs.
4
+
5
+ ## When to Apply
6
+
7
+ - Designing new API endpoints
8
+ - Reviewing existing API contracts
9
+ - Adding pagination, filtering, or sorting
10
+ - Implementing error handling for APIs
11
+ - Planning API versioning strategy
12
+ - Building public or partner-facing APIs
13
+
14
+ ## Resource Design
15
+
16
+ ### URL Structure
17
+
18
+ ```
19
+ # Resources are nouns, plural, lowercase, kebab-case
20
+ GET /api/v1/users
21
+ GET /api/v1/users/:id
22
+ POST /api/v1/users
23
+ PUT /api/v1/users/:id
24
+ PATCH /api/v1/users/:id
25
+ DELETE /api/v1/users/:id
26
+
27
+ # Sub-resources for relationships
28
+ GET /api/v1/users/:id/orders
29
+ POST /api/v1/users/:id/orders
30
+
31
+ # Actions that don't map to CRUD (use verbs sparingly)
32
+ POST /api/v1/orders/:id/cancel
33
+ POST /api/v1/auth/login
34
+ POST /api/v1/auth/refresh
35
+ ```
36
+
37
+ ### Naming Rules
38
+
39
+ ```
40
+ # GOOD
41
+ /api/v1/team-members # kebab-case for multi-word resources
42
+ /api/v1/orders?status=active # query params for filtering
43
+ /api/v1/users/123/orders # nested resources for ownership
44
+
45
+ # BAD
46
+ /api/v1/getUsers # verb in URL
47
+ /api/v1/user # singular (use plural)
48
+ /api/v1/team_members # snake_case in URLs
49
+ /api/v1/users/123/getOrders # verb in nested resource
50
+ ```
51
+
52
+ ## HTTP Methods and Status Codes
53
+
54
+ ### Method Semantics
55
+
56
+ | Method | Idempotent | Safe | Use For |
57
+ |--------|-----------|------|---------|
58
+ | GET | Yes | Yes | Retrieve resources |
59
+ | POST | No | No | Create resources, trigger actions |
60
+ | PUT | Yes | No | Full replacement of a resource |
61
+ | PATCH | No* | No | Partial update of a resource |
62
+ | DELETE | Yes | No | Remove a resource |
63
+
64
+ *PATCH can be made idempotent with proper implementation
65
+
66
+ ### Status Code Reference
67
+
68
+ ```
69
+ # Success
70
+ 200 OK — GET, PUT, PATCH (with response body)
71
+ 201 Created — POST (include Location header)
72
+ 204 No Content — DELETE, PUT (no response body)
73
+
74
+ # Client Errors
75
+ 400 Bad Request — Validation failure, malformed JSON
76
+ 401 Unauthorized — Missing or invalid authentication
77
+ 403 Forbidden — Authenticated but not authorized
78
+ 404 Not Found — Resource doesn't exist
79
+ 409 Conflict — Duplicate entry, state conflict
80
+ 422 Unprocessable Entity — Semantically invalid (valid JSON, bad data)
81
+ 429 Too Many Requests — Rate limit exceeded
82
+
83
+ # Server Errors
84
+ 500 Internal Server Error Unexpected failure (never expose details)
85
+ 502 Bad Gateway Upstream service failed
86
+ 503 Service Unavailable — Temporary overload, include Retry-After
87
+ ```
88
+
89
+ ### Common Mistakes
90
+
91
+ ```
92
+ # BAD: 200 for everything
93
+ { "status": 200, "success": false, "error": "Not found" }
94
+
95
+ # GOOD: Use HTTP status codes semantically
96
+ HTTP/1.1 404 Not Found
97
+ { "error": { "code": "not_found", "message": "User not found" } }
98
+
99
+ # BAD: 500 for validation errors
100
+ # GOOD: 400 or 422 with field-level details
101
+
102
+ # BAD: 200 for created resources
103
+ # GOOD: 201 with Location header
104
+ HTTP/1.1 201 Created
105
+ Location: /api/v1/users/abc-123
106
+ ```
107
+
108
+ ## Response Format
109
+
110
+ ### Success Response
111
+
112
+ ```json
113
+ {
114
+ "data": {
115
+ "id": "abc-123",
116
+ "email": "alice@example.com",
117
+ "name": "Alice",
118
+ "created_at": "2025-01-15T10:30:00Z"
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### Collection Response (with Pagination)
124
+
125
+ ```json
126
+ {
127
+ "data": [
128
+ { "id": "abc-123", "name": "Alice" },
129
+ { "id": "def-456", "name": "Bob" }
130
+ ],
131
+ "meta": {
132
+ "total": 142,
133
+ "page": 1,
134
+ "per_page": 20,
135
+ "total_pages": 8
136
+ },
137
+ "links": {
138
+ "self": "/api/v1/users?page=1&per_page=20",
139
+ "next": "/api/v1/users?page=2&per_page=20",
140
+ "last": "/api/v1/users?page=8&per_page=20"
141
+ }
142
+ }
143
+ ```
144
+
145
+ ### Error Response
146
+
147
+ ```json
148
+ {
149
+ "error": {
150
+ "code": "validation_error",
151
+ "message": "Request validation failed",
152
+ "details": [
153
+ {
154
+ "field": "email",
155
+ "message": "Must be a valid email address",
156
+ "code": "invalid_format"
157
+ },
158
+ {
159
+ "field": "age",
160
+ "message": "Must be between 0 and 150",
161
+ "code": "out_of_range"
162
+ }
163
+ ]
164
+ }
165
+ }
166
+ ```
167
+
168
+ ### Response Envelope Variants
169
+
170
+ ```typescript
171
+ // Option A: Envelope with data wrapper (recommended for public APIs)
172
+ interface ApiResponse<T> {
173
+ data: T;
174
+ meta?: PaginationMeta;
175
+ links?: PaginationLinks;
176
+ }
177
+
178
+ interface ApiError {
179
+ error: {
180
+ code: string;
181
+ message: string;
182
+ details?: FieldError[];
183
+ };
184
+ }
185
+
186
+ // Option B: Flat response (simpler, common for internal APIs)
187
+ // Success: just return the resource directly
188
+ // Error: return error object
189
+ // Distinguish by HTTP status code
190
+ ```
191
+
192
+ ## Pagination
193
+
194
+ ### Offset-Based (Simple)
195
+
196
+ ```
197
+ GET /api/v1/users?page=2&per_page=20
198
+
199
+ # Implementation
200
+ SELECT * FROM users
201
+ ORDER BY created_at DESC
202
+ LIMIT 20 OFFSET 20;
203
+ ```
204
+
205
+ **Pros:** Easy to implement, supports "jump to page N"
206
+ **Cons:** Slow on large offsets (OFFSET 100000), inconsistent with concurrent inserts
207
+
208
+ ### Cursor-Based (Scalable)
209
+
210
+ ```
211
+ GET /api/v1/users?cursor=eyJpZCI6MTIzfQ&limit=20
212
+
213
+ # Implementation
214
+ SELECT * FROM users
215
+ WHERE id > :cursor_id
216
+ ORDER BY id ASC
217
+ LIMIT 21; -- fetch one extra to determine has_next
218
+ ```
219
+
220
+ ```json
221
+ {
222
+ "data": [...],
223
+ "meta": {
224
+ "has_next": true,
225
+ "next_cursor": "eyJpZCI6MTQzfQ"
226
+ }
227
+ }
228
+ ```
229
+
230
+ **Pros:** Consistent performance regardless of position, stable with concurrent inserts
231
+ **Cons:** Cannot jump to arbitrary page, cursor is opaque
232
+
233
+ ### When to Use Which
234
+
235
+ | Use Case | Pagination Type |
236
+ |----------|----------------|
237
+ | Admin dashboards, small datasets (<10K) | Offset |
238
+ | Infinite scroll, feeds, large datasets | Cursor |
239
+ | Public APIs | Cursor (default) with offset (optional) |
240
+ | Search results | Offset (users expect page numbers) |
241
+
242
+ ## Filtering, Sorting, and Search
243
+
244
+ ### Filtering
245
+
246
+ ```
247
+ # Simple equality
248
+ GET /api/v1/orders?status=active&customer_id=abc-123
249
+
250
+ # Comparison operators (use bracket notation)
251
+ GET /api/v1/products?price[gte]=10&price[lte]=100
252
+ GET /api/v1/orders?created_at[after]=2025-01-01
253
+
254
+ # Multiple values (comma-separated)
255
+ GET /api/v1/products?category=electronics,clothing
256
+
257
+ # Nested fields (dot notation)
258
+ GET /api/v1/orders?customer.country=US
259
+ ```
260
+
261
+ ### Sorting
262
+
263
+ ```
264
+ # Single field (prefix - for descending)
265
+ GET /api/v1/products?sort=-created_at
266
+
267
+ # Multiple fields (comma-separated)
268
+ GET /api/v1/products?sort=-featured,price,-created_at
269
+ ```
270
+
271
+ ### Full-Text Search
272
+
273
+ ```
274
+ # Search query parameter
275
+ GET /api/v1/products?q=wireless+headphones
276
+
277
+ # Field-specific search
278
+ GET /api/v1/users?email=alice
279
+ ```
280
+
281
+ ### Sparse Fieldsets
282
+
283
+ ```
284
+ # Return only specified fields (reduces payload)
285
+ GET /api/v1/users?fields=id,name,email
286
+ GET /api/v1/orders?fields=id,total,status&include=customer.name
287
+ ```
288
+
289
+ ## Authentication and Authorization
290
+
291
+ ### Token-Based Auth
292
+
293
+ ```
294
+ # Bearer token in Authorization header
295
+ GET /api/v1/users
296
+ Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
297
+
298
+ # API key (for server-to-server)
299
+ GET /api/v1/data
300
+ X-API-Key: sk_live_abc123
301
+ ```
302
+
303
+ ### Authorization Patterns
304
+
305
+ ```typescript
306
+ // Resource-level: check ownership
307
+ app.get("/api/v1/orders/:id", async (req, res) => {
308
+ const order = await Order.findById(req.params.id);
309
+ if (!order) return res.status(404).json({ error: { code: "not_found" } });
310
+ if (order.userId !== req.user.id) return res.status(403).json({ error: { code: "forbidden" } });
311
+ return res.json({ data: order });
312
+ });
313
+
314
+ // Role-based: check permissions
315
+ app.delete("/api/v1/users/:id", requireRole("admin"), async (req, res) => {
316
+ await User.delete(req.params.id);
317
+ return res.status(204).send();
318
+ });
319
+ ```
320
+
321
+ ## Rate Limiting
322
+
323
+ ### Headers
324
+
325
+ ```
326
+ HTTP/1.1 200 OK
327
+ X-RateLimit-Limit: 100
328
+ X-RateLimit-Remaining: 95
329
+ X-RateLimit-Reset: 1640000000
330
+
331
+ # When exceeded
332
+ HTTP/1.1 429 Too Many Requests
333
+ Retry-After: 60
334
+ {
335
+ "error": {
336
+ "code": "rate_limit_exceeded",
337
+ "message": "Rate limit exceeded. Try again in 60 seconds."
338
+ }
339
+ }
340
+ ```
341
+
342
+ ### Rate Limit Tiers
343
+
344
+ | Tier | Limit | Window | Use Case |
345
+ |------|-------|--------|----------|
346
+ | Anonymous | 30/min | Per IP | Public endpoints |
347
+ | Authenticated | 100/min | Per user | Standard API access |
348
+ | Premium | 1000/min | Per API key | Paid API plans |
349
+ | Internal | 10000/min | Per service | Service-to-service |
350
+
351
+ ## Versioning
352
+
353
+ ### URL Path Versioning (Recommended)
354
+
355
+ ```
356
+ /api/v1/users
357
+ /api/v2/users
358
+ ```
359
+
360
+ **Pros:** Explicit, easy to route, cacheable
361
+ **Cons:** URL changes between versions
362
+
363
+ ### Header Versioning
364
+
365
+ ```
366
+ GET /api/users
367
+ Accept: application/vnd.myapp.v2+json
368
+ ```
369
+
370
+ **Pros:** Clean URLs
371
+ **Cons:** Harder to test, easy to forget
372
+
373
+ ### Versioning Strategy
374
+
375
+ ```
376
+ 1. Start with /api/v1/ — don't version until you need to
377
+ 2. Maintain at most 2 active versions (current + previous)
378
+ 3. Deprecation timeline:
379
+ - Announce deprecation (6 months notice for public APIs)
380
+ - Add Sunset header: Sunset: Sat, 01 Jan 2026 00:00:00 GMT
381
+ - Return 410 Gone after sunset date
382
+ 4. Non-breaking changes don't need a new version:
383
+ - Adding new fields to responses
384
+ - Adding new optional query parameters
385
+ - Adding new endpoints
386
+ 5. Breaking changes require a new version:
387
+ - Removing or renaming fields
388
+ - Changing field types
389
+ - Changing URL structure
390
+ - Changing authentication method
391
+ ```
392
+
393
+ ## Implementation Patterns
394
+
395
+ ### TypeScript (Next.js API Route)
396
+
397
+ ```typescript
398
+ import { z } from "zod";
399
+ import { NextRequest, NextResponse } from "next/server";
400
+
401
+ const createUserSchema = z.object({
402
+ email: z.string().email(),
403
+ name: z.string().min(1).max(100),
404
+ });
405
+
406
+ export async function POST(req: NextRequest) {
407
+ const body = await req.json();
408
+ const parsed = createUserSchema.safeParse(body);
409
+
410
+ if (!parsed.success) {
411
+ return NextResponse.json({
412
+ error: {
413
+ code: "validation_error",
414
+ message: "Request validation failed",
415
+ details: parsed.error.issues.map(i => ({
416
+ field: i.path.join("."),
417
+ message: i.message,
418
+ code: i.code,
419
+ })),
420
+ },
421
+ }, { status: 422 });
422
+ }
423
+
424
+ const user = await createUser(parsed.data);
425
+
426
+ return NextResponse.json(
427
+ { data: user },
428
+ {
429
+ status: 201,
430
+ headers: { Location: `/api/v1/users/${user.id}` },
431
+ },
432
+ );
433
+ }
434
+ ```
435
+
436
+ ### Python (Django REST Framework)
437
+
438
+ ```python
439
+ from rest_framework import serializers, viewsets, status
440
+ from rest_framework.response import Response
441
+
442
+ class CreateUserSerializer(serializers.Serializer):
443
+ email = serializers.EmailField()
444
+ name = serializers.CharField(max_length=100)
445
+
446
+ class UserSerializer(serializers.ModelSerializer):
447
+ class Meta:
448
+ model = User
449
+ fields = ["id", "email", "name", "created_at"]
450
+
451
+ class UserViewSet(viewsets.ModelViewSet):
452
+ serializer_class = UserSerializer
453
+ permission_classes = [IsAuthenticated]
454
+
455
+ def get_serializer_class(self):
456
+ if self.action == "create":
457
+ return CreateUserSerializer
458
+ return UserSerializer
459
+
460
+ def create(self, request):
461
+ serializer = CreateUserSerializer(data=request.data)
462
+ serializer.is_valid(raise_exception=True)
463
+ user = UserService.create(**serializer.validated_data)
464
+ return Response(
465
+ {"data": UserSerializer(user).data},
466
+ status=status.HTTP_201_CREATED,
467
+ headers={"Location": f"/api/v1/users/{user.id}"},
468
+ )
469
+ ```
470
+
471
+ ### Go (net/http)
472
+
473
+ ```go
474
+ func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
475
+ var req CreateUserRequest
476
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
477
+ writeError(w, http.StatusBadRequest, "invalid_json", "Invalid request body")
478
+ return
479
+ }
480
+
481
+ if err := req.Validate(); err != nil {
482
+ writeError(w, http.StatusUnprocessableEntity, "validation_error", err.Error())
483
+ return
484
+ }
485
+
486
+ user, err := h.service.Create(r.Context(), req)
487
+ if err != nil {
488
+ switch {
489
+ case errors.Is(err, domain.ErrEmailTaken):
490
+ writeError(w, http.StatusConflict, "email_taken", "Email already registered")
491
+ default:
492
+ writeError(w, http.StatusInternalServerError, "internal_error", "Internal error")
493
+ }
494
+ return
495
+ }
496
+
497
+ w.Header().Set("Location", fmt.Sprintf("/api/v1/users/%s", user.ID))
498
+ writeJSON(w, http.StatusCreated, map[string]any{"data": user})
499
+ }
500
+ ```
501
+
502
+ ## API Design Checklist
503
+
504
+ Before shipping a new endpoint:
505
+
506
+ - [ ] Resource URL follows naming conventions (plural, kebab-case, no verbs)
507
+ - [ ] Correct HTTP method used (GET for reads, POST for creates, etc.)
508
+ - [ ] Appropriate status codes returned (not 200 for everything)
509
+ - [ ] Input validated with schema (Zod, Pydantic, Bean Validation)
510
+ - [ ] Error responses follow standard format with codes and messages
511
+ - [ ] Pagination implemented for list endpoints (cursor or offset)
512
+ - [ ] Authentication required (or explicitly marked as public)
513
+ - [ ] Authorization checked (user can only access their own resources)
514
+ - [ ] Rate limiting configured
515
+ - [ ] Response does not leak internal details (stack traces, SQL errors)
516
+ - [ ] Consistent naming with existing endpoints (camelCase vs snake_case)
517
+ - [ ] Documented (OpenAPI/Swagger spec updated)