@ai-support-agent/cli 0.1.32-beta.1 → 0.1.33-beta.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/dist/commands/claude-code-args.d.ts +1 -0
- package/dist/commands/claude-code-args.d.ts.map +1 -1
- package/dist/commands/claude-code-args.js +3 -0
- package/dist/commands/claude-code-args.js.map +1 -1
- package/dist/commands/claude-code-runner.d.ts.map +1 -1
- package/dist/commands/claude-code-runner.js +2 -1
- package/dist/commands/claude-code-runner.js.map +1 -1
- package/dist/commands/plugin-dir.d.ts +20 -0
- package/dist/commands/plugin-dir.d.ts.map +1 -0
- package/dist/commands/plugin-dir.js +71 -0
- package/dist/commands/plugin-dir.js.map +1 -0
- package/dist/plugin/.claude-plugin/plugin.json +9 -0
- package/dist/plugin/LICENSE +26 -0
- package/dist/plugin/README.md +86 -0
- package/dist/plugin/SYNC.md +113 -0
- package/dist/plugin/agents/build-error-resolver.md +159 -0
- package/dist/plugin/agents/code-reviewer.md +277 -0
- package/dist/plugin/agents/django-reviewer.md +159 -0
- package/dist/plugin/agents/infra-reviewer.md +172 -0
- package/dist/plugin/agents/investigator.md +75 -0
- package/dist/plugin/agents/nextjs-reviewer.md +191 -0
- package/dist/plugin/agents/php-reviewer.md +167 -0
- package/dist/plugin/agents/planner.md +184 -0
- package/dist/plugin/agents/python-reviewer.md +161 -0
- package/dist/plugin/agents/react-reviewer.md +150 -0
- package/dist/plugin/agents/silent-failure-hunter.md +158 -0
- package/dist/plugin/agents/typescript-reviewer.md +179 -0
- package/dist/plugin/agents/ui-reviewer.md +203 -0
- package/dist/plugin/commands/add-feature.md +301 -0
- package/dist/plugin/commands/build-fix.md +47 -0
- package/dist/plugin/commands/code-review.md +228 -0
- package/dist/plugin/commands/fix-defect.md +393 -0
- package/dist/plugin/commands/learn-eval.md +94 -0
- package/dist/plugin/commands/learn.md +84 -0
- package/dist/plugin/commands/plan.md +211 -0
- package/dist/plugin/commands/test-coverage.md +64 -0
- package/dist/plugin/commands/update-docs.md +98 -0
- package/dist/plugin/hooks/hooks.json +59 -0
- package/dist/plugin/hooks/scripts/auto-format.sh +63 -0
- package/dist/plugin/hooks/scripts/check-secrets-before-commit.sh +50 -0
- package/dist/plugin/hooks/scripts/guard-dangerous-commands.sh +55 -0
- package/dist/plugin/hooks/scripts/on-command-resume.sh +112 -0
- package/dist/plugin/hooks/scripts/on-command-stop.sh +200 -0
- package/dist/plugin/hooks/scripts/protect-sensitive-files.sh +58 -0
- package/dist/plugin/rules/common/coding-guidelines.md +73 -0
- package/dist/plugin/rules/documentation/api-docs.md +46 -0
- package/dist/plugin/rules/documentation/docs-site.md +60 -0
- package/dist/plugin/rules/documentation/source-docs.md +89 -0
- package/dist/plugin/rules/documentation/test-docs.md +39 -0
- package/dist/plugin/rules/logging/logging-rules.md +83 -0
- package/dist/plugin/rules/php/coding-rules.md +40 -0
- package/dist/plugin/rules/python/coding-rules.md +40 -0
- package/dist/plugin/rules/typescript/coding-rules.md +45 -0
- package/dist/plugin/skills/api-design/SKILL.md +269 -0
- package/dist/plugin/skills/backend-patterns/SKILL.md +312 -0
- package/dist/plugin/skills/database-migrations/SKILL.md +323 -0
- package/dist/plugin/skills/docker-patterns/SKILL.md +308 -0
- package/dist/plugin/skills/docs-site/SKILL.md +341 -0
- package/dist/plugin/skills/e2e-testing/SKILL.md +334 -0
- package/dist/plugin/skills/frontend-patterns/SKILL.md +318 -0
- package/dist/plugin/skills/integration-testing/SKILL.md +273 -0
- package/package.json +2 -2
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-design
|
|
3
|
+
description: A pattern library for REST API design. Provides guidance on resource naming, choosing HTTP methods and status codes, error response formats, pagination, versioning, idempotency, authentication/authorization, and rate limiting. Use this when designing endpoints for a new API, reviewing the design of an existing API, or when you need decision criteria for a discussion about API conventions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# REST API Design Patterns
|
|
7
|
+
|
|
8
|
+
A collection of decision criteria and patterns for designing and implementing REST APIs. Reference this skill when adding new endpoints, revising an existing API, or doing a design review. Implementation examples favor NestJS, Django REST Framework, and Laravel.
|
|
9
|
+
|
|
10
|
+
This skill covers HTTP interface design — formats and conventions. For server-side implementation details of authorization, rate limiting, and error handling, see the backend-patterns skill.
|
|
11
|
+
|
|
12
|
+
## 1. Resource Design and Naming
|
|
13
|
+
|
|
14
|
+
### Basic Principles
|
|
15
|
+
|
|
16
|
+
- Represent resources as **plural nouns**. Don't use verbs.
|
|
17
|
+
- Good: `GET /users`, `POST /orders`, `GET /users/123`
|
|
18
|
+
- Bad: `GET /getUsers`, `POST /createOrder`
|
|
19
|
+
- Use lowercase kebab-case for paths (`/purchase-orders`). For query parameters, pick either snake_case or camelCase and stay consistent within the project.
|
|
20
|
+
- Represent IDs as path segments: `/users/{userId}`
|
|
21
|
+
|
|
22
|
+
### Nesting Depth
|
|
23
|
+
|
|
24
|
+
Keep nesting to **at most two levels** (two resources).
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
GET /users/123/orders # Acceptable: a user's orders
|
|
28
|
+
GET /users/123/orders/456/items/789 # Avoid: too deep
|
|
29
|
+
GET /order-items/789 # Preferred: reference directly if the ID is unique
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If a child resource can be uniquely identified without its parent, promote it to a top-level resource. Reserve nesting for expressing "belongs-to" relationships and narrowing list results.
|
|
33
|
+
|
|
34
|
+
### Expressing Relationships
|
|
35
|
+
|
|
36
|
+
- Express a relationship lookup either as a sub-resource (`/users/123/orders`) or as a filter (`/orders?user_id=123`). If you offer both, make sure their behavior matches.
|
|
37
|
+
- Express many-to-many association/disassociation as POST/DELETE on a sub-resource.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
POST /articles/10/tags # body: {"tag_id": 5} to attach a tag
|
|
41
|
+
DELETE /articles/10/tags/5 # remove a tag
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- Keep embedded related data in responses to the bare minimum (roughly an ID and a display name), or only expand it when explicitly requested (e.g. via `?include=`). Embedding deep relations unconditionally is a common source of N+1 queries and bloated payloads.
|
|
45
|
+
|
|
46
|
+
## 2. HTTP Methods and Status Codes
|
|
47
|
+
|
|
48
|
+
### Choosing a Method
|
|
49
|
+
|
|
50
|
+
| Method | Purpose | Idempotent |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| GET | Retrieval. Must not have side effects | Yes |
|
|
53
|
+
| POST | Create, or execute a non-idempotent action | No |
|
|
54
|
+
| PUT | Replace the entire resource | Yes |
|
|
55
|
+
| PATCH | Partial update | Not guaranteed (depends on design) |
|
|
56
|
+
| DELETE | Delete | Yes |
|
|
57
|
+
|
|
58
|
+
### When to Use Each Status Code
|
|
59
|
+
|
|
60
|
+
- **200 OK**: Successful retrieval or update. Response body present.
|
|
61
|
+
- **201 Created**: Successful creation. It's good practice to return the new resource's URL in the `Location` header.
|
|
62
|
+
- **204 No Content**: Success with no body. The standard response for a successful DELETE.
|
|
63
|
+
- **400 Bad Request**: The request is malformed syntactically (broken JSON, missing required parameters, etc.).
|
|
64
|
+
- **401 Unauthorized**: Not authenticated. Token missing, invalid, or expired.
|
|
65
|
+
- **403 Forbidden**: Authenticated, but not authorized for this action.
|
|
66
|
+
- **404 Not Found**: The resource doesn't exist.
|
|
67
|
+
- **409 Conflict**: A state conflict — unique constraint violation, optimistic lock failure, an operation that's already been processed, etc.
|
|
68
|
+
- **422 Unprocessable Entity**: Well-formed but semantically invalid (validation error). Decide once, per project, how you split this from 400.
|
|
69
|
+
- **500 Internal Server Error**: An unexpected server-side error. Log the details; never return them to the client.
|
|
70
|
+
|
|
71
|
+
### Choosing Between 404 and 403
|
|
72
|
+
|
|
73
|
+
Return 404 when you don't want an unauthorized user to even know the resource exists (e.g. another tenant's data, an unpublished draft). Return 403 when the resource's existence is public knowledge and only the action is forbidden (e.g. no edit permission on a published article). The deciding question is: "would revealing existence itself be a data leak?" In multi-tenant APIs, default to 404.
|
|
74
|
+
|
|
75
|
+
## 3. Error Responses
|
|
76
|
+
|
|
77
|
+
### Use One Consistent Shape
|
|
78
|
+
|
|
79
|
+
Return the same error structure from every endpoint. At minimum, include a code, a message, and details.
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"error": {
|
|
84
|
+
"code": "VALIDATION_ERROR",
|
|
85
|
+
"message": "There were errors in your input.",
|
|
86
|
+
"details": [
|
|
87
|
+
{ "field": "email", "code": "invalid_format", "message": "Email address format is invalid." },
|
|
88
|
+
{ "field": "age", "code": "out_of_range", "message": "Must be between 0 and 150." }
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- `code` should be a machine-readable constant string. Clients should branch on the code, not on the message text.
|
|
95
|
+
- Return validation errors **per field** in a `details` array, so forms can display errors next to each field.
|
|
96
|
+
- Aligning with RFC 9457 (Problem Details, `application/problem+json`) is also a solid choice. Either way, standardize on one shape.
|
|
97
|
+
|
|
98
|
+
### Don't Leak Internal Information
|
|
99
|
+
|
|
100
|
+
- Never include stack traces, raw SQL, file paths, or a library's raw exception message in a response.
|
|
101
|
+
- For 5xx errors, return only a generic message and a correlation ID (request ID); log the details server-side.
|
|
102
|
+
- On authentication failure, don't distinguish between "user doesn't exist" and "wrong password" in the response.
|
|
103
|
+
|
|
104
|
+
### Implementation Example (NestJS)
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// An exception filter that converts all errors into the common shape
|
|
108
|
+
@Catch()
|
|
109
|
+
export class AppExceptionFilter implements ExceptionFilter {
|
|
110
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
111
|
+
const res = host.switchToHttp().getResponse();
|
|
112
|
+
if (exception instanceof HttpException) {
|
|
113
|
+
res.status(exception.getStatus()).json({ error: this.toErrorBody(exception) });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Unexpected exception: log the details, return a generic message to the client
|
|
117
|
+
this.logger.error(exception);
|
|
118
|
+
res.status(500).json({
|
|
119
|
+
error: { code: 'INTERNAL_ERROR', message: 'An internal server error occurred.' },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
In Django REST Framework, point the `EXCEPTION_HANDLER` setting at a custom handler that repacks `ValidationError.detail` into the common shape. In Laravel, customize `render` via `withExceptions` in `bootstrap/app.php` (or the exception Handler).
|
|
126
|
+
|
|
127
|
+
## 4. Pagination
|
|
128
|
+
|
|
129
|
+
### Choosing a Strategy
|
|
130
|
+
|
|
131
|
+
- **Offset-based** (`?page=2&per_page=20` / `?offset=20&limit=20`)
|
|
132
|
+
- Well suited to page-number UIs. Simple to implement.
|
|
133
|
+
- Downsides: gets slow at large offsets. Inserts/deletes mid-list cause duplicates or gaps.
|
|
134
|
+
- **Cursor-based** (`?cursor=eyJpZCI6MTIzfQ&limit=20`)
|
|
135
|
+
- Well suited to infinite scroll, frequently-updated lists, and large datasets.
|
|
136
|
+
- The cursor encodes an opaque key that uniquely identifies the last row returned (e.g. `created_at` + `id`).
|
|
137
|
+
|
|
138
|
+
Decision rule: if you need a total-count display and arbitrary page jumps, use offset-based. If the dataset is large, updated frequently, or performance-sensitive, use cursor-based.
|
|
139
|
+
|
|
140
|
+
### Enforce a Limit Ceiling
|
|
141
|
+
|
|
142
|
+
The server must **always enforce an upper bound** on `limit`/`per_page` (e.g. default 20, max 100). Never use the client-supplied value as-is.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
# Django REST Framework
|
|
146
|
+
class StandardPagination(PageNumberPagination):
|
|
147
|
+
page_size = 20
|
|
148
|
+
page_size_query_param = "per_page"
|
|
149
|
+
max_page_size = 100 # requests above this are clamped to 100
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Include metadata in the response: for offset-based, `total` / `page` / `per_page`; for cursor-based, `next_cursor` (null when there's no next page).
|
|
153
|
+
|
|
154
|
+
## 5. Filtering, Sorting, and Search
|
|
155
|
+
|
|
156
|
+
- Express filters as query parameters: `GET /orders?status=shipped&user_id=123`
|
|
157
|
+
- Standardize a suffix convention for range conditions and stick to it: `?created_at_gte=2026-01-01&created_at_lt=2026-02-01` (or a `?created_at[gte]=...` style — pick one).
|
|
158
|
+
- For sorting, a comma-separated list with a `-` prefix for descending order is concise: `?sort=-created_at,name`.
|
|
159
|
+
- Route full-text/fuzzy search through `?q=keyword`, kept separate from structured filters.
|
|
160
|
+
- Enforce an **allowlist**. The server must explicitly enumerate which fields are filterable/sortable, and must never accept an arbitrary column name (this prevents SQL injection and unintended index scans).
|
|
161
|
+
|
|
162
|
+
```php
|
|
163
|
+
// Laravel: restrict sortable fields with an allowlist
|
|
164
|
+
$sortable = ['created_at', 'name', 'price'];
|
|
165
|
+
$sort = $request->query('sort', '-created_at');
|
|
166
|
+
$direction = str_starts_with($sort, '-') ? 'desc' : 'asc';
|
|
167
|
+
$column = ltrim($sort, '-');
|
|
168
|
+
abort_unless(in_array($column, $sortable, true), 400);
|
|
169
|
+
$query->orderBy($column, $direction);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 6. Versioning
|
|
173
|
+
|
|
174
|
+
### Policy
|
|
175
|
+
|
|
176
|
+
**Prefer URL-path versioning (`/v1/users`).** It's explicit, and it's easy to work with in routing, logging, caching, and documentation alike.
|
|
177
|
+
|
|
178
|
+
Options and decision criteria:
|
|
179
|
+
|
|
180
|
+
| Approach | Example | Characteristics |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| URL path | `/v1/users` | Most explicit. Recommended. Easy to inspect via browser or curl |
|
|
183
|
+
| Header | `Accept: application/vnd.api.v1+json` | Keeps the URL clean, but harder to debug and more costly to adopt |
|
|
184
|
+
| Query param | `/users?version=1` | Tends to complicate cache keys. Not recommended |
|
|
185
|
+
|
|
186
|
+
### Operating Rules
|
|
187
|
+
|
|
188
|
+
- Version only the major number (`v1`, `v2`). Don't bump the version for backward-compatible changes (added fields, new endpoints).
|
|
189
|
+
- Only cut a new version for breaking changes (removed fields, type changes, making a field required, changed behavior).
|
|
190
|
+
- Announce a deprecation date for old versions and signal it via `Deprecation` / `Sunset` headers.
|
|
191
|
+
- For internal-only APIs, it's reasonable to skip versioning and deploy the client and server together. For public APIs, versioning is mandatory.
|
|
192
|
+
|
|
193
|
+
## 7. Idempotency
|
|
194
|
+
|
|
195
|
+
- Implement **GET, PUT, and DELETE as idempotent**: sending the same request multiple times must leave the resource in the same state.
|
|
196
|
+
- A second DELETE may return 404 (the state — "doesn't exist" — is unchanged). If you'd rather keep the client logic simple, returning 204 again on the second call is also acceptable.
|
|
197
|
+
- **POST is not idempotent**. You need a defense against duplicate execution from network failures or retries (double charges, duplicate orders).
|
|
198
|
+
|
|
199
|
+
### Idempotency Keys
|
|
200
|
+
|
|
201
|
+
The client sends a unique-per-request key (a UUID) in an `Idempotency-Key` header, and the server stores the key alongside the processing result for some retention window. On a retry with the same key, **return the stored response as-is**.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// NestJS: sketch of duplicate-POST protection via an idempotency key
|
|
205
|
+
@Post('payments')
|
|
206
|
+
async create(@Headers('idempotency-key') key: string, @Body() dto: CreatePaymentDto) {
|
|
207
|
+
if (!key) throw new BadRequestException('Idempotency-Key header is required');
|
|
208
|
+
const cached = await this.idempotencyStore.find(key);
|
|
209
|
+
if (cached) return cached.response; // Retry: return the stored result
|
|
210
|
+
const result = await this.payments.create(dto);
|
|
211
|
+
await this.idempotencyStore.save(key, result, { ttl: '24h' });
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- Store the key under a unique constraint, and prevent concurrent-execution races with a transaction or lock.
|
|
217
|
+
- This is mandatory for any POST where duplicate execution causes real harm — payments, orders, notification sends.
|
|
218
|
+
|
|
219
|
+
## 8. Authentication and Authorization
|
|
220
|
+
|
|
221
|
+
### Token Authentication Basics
|
|
222
|
+
|
|
223
|
+
- Send tokens via the `Authorization: Bearer <token>` header. Never put a token in a query parameter (it leaks into logs and referrers).
|
|
224
|
+
- Keep access tokens short-lived (minutes to an hour) and refresh them with a refresh token.
|
|
225
|
+
- 401 means "authentication failed"; 403 means "authenticated, but not authorized." Don't conflate them.
|
|
226
|
+
|
|
227
|
+
### Per-Resource Authorization Checks
|
|
228
|
+
|
|
229
|
+
Keep authentication (who you are) separate from authorization (what you can do), and **always check ownership/permission against the specific resource being accessed**. Letting a request through on authentication alone, with direct object access by ID, is the textbook API vulnerability (IDOR / BOLA).
|
|
230
|
+
|
|
231
|
+
- Filter list endpoints at the query level. Filtering after fetching breaks pagination and result counts.
|
|
232
|
+
- When denying access to a single resource because the caller lacks permission, return 404 if you want to hide its existence (see section 2).
|
|
233
|
+
- For the authorization implementation mechanics and code examples (NestJS Guards, Laravel Policies, DRF `permission_classes`, etc.), see backend-patterns.
|
|
234
|
+
|
|
235
|
+
## 9. Rate Limiting
|
|
236
|
+
|
|
237
|
+
- Limit request counts per user (or token, or IP). Apply stricter limits to unauthenticated endpoints.
|
|
238
|
+
- On exceeding the limit, return **429 Too Many Requests** with a `Retry-After` header telling the client how many seconds until it can retry.
|
|
239
|
+
- Report the current limit state in every response's headers.
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
RateLimit-Limit: 100
|
|
243
|
+
RateLimit-Remaining: 42
|
|
244
|
+
RateLimit-Reset: 1718100000
|
|
245
|
+
Retry-After: 30 # only present on 429
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
(The `X-RateLimit-*` header family is also widely used. Pick one convention per project.)
|
|
249
|
+
|
|
250
|
+
- Apply a separate, stricter limit to authentication-related endpoints (login, password reset) than to the rest of the API.
|
|
251
|
+
- For implementation mechanics (each framework's throttling middleware, etc.), see backend-patterns.
|
|
252
|
+
|
|
253
|
+
## 10. Design Review Checklist
|
|
254
|
+
|
|
255
|
+
- [ ] Are paths plural nouns, with nesting no deeper than two levels?
|
|
256
|
+
- [ ] Does each endpoint's method and status codes (including 201/204/409/422) match its actual semantics?
|
|
257
|
+
- [ ] Do all endpoints return errors in the same shape? Are validation errors broken out per field?
|
|
258
|
+
- [ ] Do errors avoid leaking internal information (stack traces, SQL, file paths)?
|
|
259
|
+
- [ ] Does every list endpoint paginate, with the limit enforced server-side?
|
|
260
|
+
- [ ] Are filterable/sortable fields restricted to an allowlist?
|
|
261
|
+
- [ ] Is the versioning policy defined, including how breaking changes are handled?
|
|
262
|
+
- [ ] Do POSTs where duplicate execution causes real harm have idempotency-key protection?
|
|
263
|
+
- [ ] Is there object-level authorization on every resource (IDOR protection)? Is the 403/404 policy consistent?
|
|
264
|
+
- [ ] Is rate limiting configured, with 429 and RateLimit headers reported?
|
|
265
|
+
|
|
266
|
+
## Related
|
|
267
|
+
|
|
268
|
+
- Use the `/code-review` command for post-implementation code review.
|
|
269
|
+
- Delegate detailed API implementation review criteria (security, performance, convention compliance) to code-reviewer subagents.
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend-patterns
|
|
3
|
+
description: A collection of backend design and implementation patterns covering layering, input validation, N+1 queries and transactions, caching, error handling, authentication/authorization, rate limiting, structured logging, and background jobs, for NestJS / Django / Flask / Laravel / CakePHP. Use this when designing, implementing, or reviewing backend APIs, service layers, or jobs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# backend-patterns: Backend Design & Implementation Patterns
|
|
7
|
+
|
|
8
|
+
Target stacks: NestJS / Django / Flask / Laravel / CakePHP.
|
|
9
|
+
Code examples favor NestJS, Django, and Laravel; Flask and CakePHP equivalents are noted alongside them.
|
|
10
|
+
For interface conventions such as URL design and status codes, see the api-design skill.
|
|
11
|
+
|
|
12
|
+
## Layering
|
|
13
|
+
|
|
14
|
+
- Keep controllers (handlers) thin. Their only responsibilities are: accepting and transforming input, calling the service layer, and shaping the response.
|
|
15
|
+
- Put business logic in the service layer. Conditional branching, calculations, and cross-resource consistency all belong here.
|
|
16
|
+
- Keep data access isolated in a repository/ORM layer. Don't leak raw SQL or query-builder details into the service layer.
|
|
17
|
+
- Structuring things so the same logic can be called from both a controller and a job (CLI or queue worker) is a good self-check for whether your layering is actually sound.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// NestJS
|
|
21
|
+
// Bad: business logic and data access live directly in the controller
|
|
22
|
+
@Post()
|
|
23
|
+
async create(@Body() body: any) {
|
|
24
|
+
const user = await this.userRepo.findOne({ where: { email: body.email } });
|
|
25
|
+
if (user) throw new BadRequestException('exists');
|
|
26
|
+
if (body.plan === 'pro' && !body.cardToken) throw new BadRequestException();
|
|
27
|
+
return this.userRepo.save({ ...body });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Good: the controller only passes through; decisions live in the service layer
|
|
31
|
+
@Post()
|
|
32
|
+
async create(@Body() dto: CreateUserDto) {
|
|
33
|
+
return this.usersService.register(dto);
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
In Django, keep views thin and push logic into a service module (e.g. `services.py`) or model methods. In Laravel, delegate from controllers to service/action classes, and push complex Eloquent queries into query scopes or repositories. In CakePHP, preserve the Controller → Table (model layer) separation.
|
|
38
|
+
|
|
39
|
+
## Input Validation
|
|
40
|
+
|
|
41
|
+
Use the framework's built-in validation mechanism. Hand-rolled chains of `if` statements are easy to miss cases with and hard to review.
|
|
42
|
+
|
|
43
|
+
- NestJS: DTOs + class-validator + `ValidationPipe` (always enable `whitelist: true`)
|
|
44
|
+
- Django: Forms / DRF Serializers
|
|
45
|
+
- Flask: schema validation libraries such as marshmallow or pydantic
|
|
46
|
+
- Laravel: FormRequest
|
|
47
|
+
- CakePHP: Validator (the Table's `validationDefault`)
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// NestJS
|
|
51
|
+
// Good: declare types and constraints via a DTO, and strip unknown fields with whitelist
|
|
52
|
+
export class CreateUserDto {
|
|
53
|
+
@IsEmail()
|
|
54
|
+
email: string;
|
|
55
|
+
|
|
56
|
+
@IsString()
|
|
57
|
+
@Length(8, 72)
|
|
58
|
+
password: string;
|
|
59
|
+
}
|
|
60
|
+
// main.ts: app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# Django REST Framework
|
|
65
|
+
# Bad: passing request.data straight into the model (mass assignment)
|
|
66
|
+
User.objects.create(**request.data)
|
|
67
|
+
|
|
68
|
+
# Good: use a serializer to explicitly declare which fields are accepted
|
|
69
|
+
class UserSerializer(serializers.ModelSerializer):
|
|
70
|
+
class Meta:
|
|
71
|
+
model = User
|
|
72
|
+
fields = ["email", "password"] # avoid fields = "__all__"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```php
|
|
76
|
+
// Laravel
|
|
77
|
+
// Bad: mass-assigning $request->all() (is_admin etc. could be overwritten)
|
|
78
|
+
User::create($request->all());
|
|
79
|
+
|
|
80
|
+
// Good: use only FormRequest's validated() data, and keep $fillable minimal
|
|
81
|
+
public function store(StoreUserRequest $request)
|
|
82
|
+
{
|
|
83
|
+
return User::create($request->validated());
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The principle behind mass-assignment protection is the same everywhere: explicitly whitelist which fields may be accepted. Attributes tied to permissions or ownership — `is_admin`, `role`, `tenant_id`, and the like — should be set from server-side context (the authenticated identity), never from the request body.
|
|
88
|
+
|
|
89
|
+
## Database
|
|
90
|
+
|
|
91
|
+
### Detecting N+1 Queries and Batching Fetches
|
|
92
|
+
|
|
93
|
+
Always be suspicious of queries issued inside a loop. During development, check actual query counts with a query log (Django Debug Toolbar, Laravel Telescope / `DB::listen`, TypeORM's `logging: true`).
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
# Django
|
|
97
|
+
# Bad: fetches the author one row at a time per article (1 + N queries)
|
|
98
|
+
for article in Article.objects.all():
|
|
99
|
+
print(article.author.name)
|
|
100
|
+
|
|
101
|
+
# Good: preload via JOIN / batch fetch
|
|
102
|
+
for article in Article.objects.select_related("author"):
|
|
103
|
+
print(article.author.name)
|
|
104
|
+
# Use prefetch_related for many-to-many and reverse relations
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
```php
|
|
108
|
+
// Laravel
|
|
109
|
+
// Bad: accessing $article->author inside a loop triggers N lazy-load queries
|
|
110
|
+
$articles = Article::all();
|
|
111
|
+
|
|
112
|
+
// Good: eager loading fetches everything up front
|
|
113
|
+
$articles = Article::with('author')->get();
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
In NestJS + TypeORM, achieve the same with `relations` or `leftJoinAndSelect` on the QueryBuilder. With Prisma, use `include`.
|
|
117
|
+
|
|
118
|
+
### Transaction Boundaries
|
|
119
|
+
|
|
120
|
+
Identify the unit of work that must be "all succeed or all fail," and wrap exactly that unit in a transaction.
|
|
121
|
+
|
|
122
|
+
- Open transactions in the service layer. Opening them in a controller or in an individual repository call leaves the boundary ambiguous.
|
|
123
|
+
- Never put external API calls, email sending, or other long-running operations inside a transaction. Doing so extends how long locks are held, and if the external call fails and you roll back the DB, the external side effect can't be undone. Perform external effects after commit, or hand them off to a job queue.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
# Django
|
|
127
|
+
# Good: confirming an order and decrementing stock happen in one transaction
|
|
128
|
+
from django.db import transaction
|
|
129
|
+
|
|
130
|
+
def confirm_order(order_id: int) -> None:
|
|
131
|
+
with transaction.atomic():
|
|
132
|
+
order = Order.objects.select_for_update().get(pk=order_id)
|
|
133
|
+
order.confirm()
|
|
134
|
+
order.save()
|
|
135
|
+
Stock.objects.filter(product=order.product).update(
|
|
136
|
+
quantity=F("quantity") - order.quantity
|
|
137
|
+
)
|
|
138
|
+
# Good: send the email only after commit
|
|
139
|
+
transaction.on_commit(lambda: send_order_mail.delay(order_id))
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Laravel uses `DB::transaction(fn () => ...)`; NestJS + TypeORM uses `dataSource.transaction()` or a QueryRunner. In systems built around command/event sourcing with separate write and read models, a single command is typically the unit of consistency — don't try to force consistency across multiple aggregates into one transaction; instead, design the cross-aggregate effects as eventually-consistent follow-up processing.
|
|
143
|
+
|
|
144
|
+
## Caching
|
|
145
|
+
|
|
146
|
+
The default pattern is cache-aside: check the cache on read, and on a miss, fetch from the database and populate the cache.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// NestJS (cache-aside skeleton)
|
|
150
|
+
async getProduct(id: string): Promise<Product> {
|
|
151
|
+
const cached = await this.cache.get<Product>(`product:${id}`);
|
|
152
|
+
if (cached) return cached;
|
|
153
|
+
const product = await this.productRepo.findOneByOrFail({ id });
|
|
154
|
+
await this.cache.set(`product:${id}`, product, 300); // always set a TTL
|
|
155
|
+
return product;
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- Design assuming invalidation is hard. Combine explicit deletion on update with a TTL as a second line of defense, and decide per data type how much staleness is acceptable until the TTL expires. Standardize keys as `resource-name:id` so you can always identify what to invalidate.
|
|
160
|
+
- Safe to cache: master/reference data, public content, expensive aggregate results — anything that looks the same to everyone and can tolerate some staleness.
|
|
161
|
+
- Do not cache (or be extremely careful with): authorization decisions, entire responses containing personal information, or values that demand strict accuracy such as balances or inventory counts. Storing user-specific data under a key that doesn't include the user ID is a classic way to leak one user's data to another — avoid it absolutely.
|
|
162
|
+
|
|
163
|
+
## Error Handling
|
|
164
|
+
|
|
165
|
+
### Centralized Handlers
|
|
166
|
+
|
|
167
|
+
Don't scatter exception handling throughout the codebase; consolidate it in the framework's centralized mechanism.
|
|
168
|
+
|
|
169
|
+
- NestJS: Exception Filter (`@Catch()`)
|
|
170
|
+
- Django: middleware / DRF's `EXCEPTION_HANDLER`
|
|
171
|
+
- Flask: `@app.errorhandler`
|
|
172
|
+
- Laravel: exception handling configuration in `bootstrap/app.php` (formerly the Handler class)
|
|
173
|
+
- CakePHP: ErrorController / a custom ExceptionRenderer
|
|
174
|
+
|
|
175
|
+
A centralized handler has three jobs: logging (including the stack trace), producing a safe, well-formed response for the client, and mapping errors to status codes.
|
|
176
|
+
|
|
177
|
+
### Preventing Internal Information Leaks
|
|
178
|
+
|
|
179
|
+
Never include stack traces, SQL, file paths, or environment variables in production error responses. Always verify `DEBUG=False` (Django) and `APP_DEBUG=false` (Laravel) in production. Return only an error code and a correlation ID (request ID) to the client; keep the details in server-side logs.
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// NestJS
|
|
183
|
+
// Bad: returns the raw exception message (leaks internal details)
|
|
184
|
+
catch (e) { throw new InternalServerErrorException(e.message); }
|
|
185
|
+
|
|
186
|
+
// Good: log the details, return only a fixed code and correlation ID to the client
|
|
187
|
+
catch (e) {
|
|
188
|
+
this.logger.error({ requestId, err: e }, 'order confirmation failed');
|
|
189
|
+
throw new InternalServerErrorException({ code: 'INTERNAL_ERROR', requestId });
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Retries with Exponential Backoff (External API Calls)
|
|
194
|
+
|
|
195
|
+
Transient external API failures (timeouts, 429s, 5xx) are worth retrying. 4xx errors (validation failures, etc.) won't succeed on retry, so fail fast instead.
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
# Python (conceptual skeleton — in practice, use a library like tenacity)
|
|
199
|
+
# Good: exponential backoff + jitter, with a cap on retry attempts
|
|
200
|
+
import random, time
|
|
201
|
+
|
|
202
|
+
def call_with_retry(func, max_attempts=4, base=0.5):
|
|
203
|
+
for attempt in range(1, max_attempts + 1):
|
|
204
|
+
try:
|
|
205
|
+
return func()
|
|
206
|
+
except TransientError:
|
|
207
|
+
if attempt == max_attempts:
|
|
208
|
+
raise
|
|
209
|
+
time.sleep(base * (2 ** (attempt - 1)) + random.uniform(0, 0.1))
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Anything you retry must be idempotent. Don't casually retry a non-idempotent POST — only do so if you can attach an idempotency key. Use the silent-failure-hunter subagent to check for swallowed exceptions (`except: pass`, empty `catch` blocks).
|
|
213
|
+
|
|
214
|
+
## Authentication & Authorization
|
|
215
|
+
|
|
216
|
+
- Always enforce authorization server-side. Hiding a button or a screen on the frontend is a UX affordance, not a security control — design as if the API can be hit directly.
|
|
217
|
+
- Separate authentication (who you are) from authorization (what you're allowed to do). Even after authentication succeeds, check permission on the target resource for every request.
|
|
218
|
+
- Use each framework's declarative mechanism for role-based authorization: NestJS uses Guards + custom decorators, Django uses `permission_classes` / `@permission_required`, Laravel uses Policies/Gates, Flask uses decorators, CakePHP uses the Authorization plugin.
|
|
219
|
+
- Guard against IDOR (Insecure Direct Object Reference — being able to view another user's resource just by guessing its ID) by including "does the logged-in user own this" as part of the query condition, not as an afterthought check.
|
|
220
|
+
|
|
221
|
+
```php
|
|
222
|
+
// Laravel
|
|
223
|
+
// Bad: fetching by ID alone (exposes other users' orders)
|
|
224
|
+
$order = Order::findOrFail($id);
|
|
225
|
+
|
|
226
|
+
// Good: scope the query to the owner, then authorize via a Policy
|
|
227
|
+
$order = $request->user()->orders()->findOrFail($id);
|
|
228
|
+
$this->authorize('view', $order);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// NestJS
|
|
233
|
+
// Good: declare the required role via a decorator and enforce it centrally with a Guard
|
|
234
|
+
@Roles('admin')
|
|
235
|
+
@UseGuards(JwtAuthGuard, RolesGuard)
|
|
236
|
+
@Delete(':id')
|
|
237
|
+
remove(@Param('id') id: string) { ... }
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
In multi-tenant setups, never accept the tenant ID from the request body — derive it from the authenticated identity and enforce it on every query.
|
|
241
|
+
|
|
242
|
+
## Rate Limiting
|
|
243
|
+
|
|
244
|
+
Apply rate limiting to publicly exposed endpoints (login, sign-up, password reset, search, public APIs). This defends against both brute-force attacks and general overload.
|
|
245
|
+
|
|
246
|
+
- NestJS: `@nestjs/throttler`
|
|
247
|
+
- Django: DRF throttling (`AnonRateThrottle` / `ScopedRateThrottle`), django-ratelimit
|
|
248
|
+
- Flask: Flask-Limiter
|
|
249
|
+
- Laravel: the `throttle` middleware (RateLimiter)
|
|
250
|
+
- CakePHP: a rate-limiting plugin used as middleware
|
|
251
|
+
- Serverless setups: also apply throttling at the API Gateway / WAF layer
|
|
252
|
+
|
|
253
|
+
In multi-instance deployments, the rate-limit counter must live in a shared store such as Redis — otherwise per-instance counters make the limit effectively meaningless.
|
|
254
|
+
|
|
255
|
+
## Structured Logging
|
|
256
|
+
|
|
257
|
+
- Emit logs in a structured format (e.g. JSON) rather than embedding values into message strings, so they're searchable and aggregable as fields.
|
|
258
|
+
- Attach a request ID (correlation ID) to every log line so you can trace one request's full processing path. In NestJS, use AsyncLocalStorage (e.g. nestjs-cls); in Django/Flask, attach it via middleware.
|
|
259
|
+
- For the full house rules on log levels, debug-mode conventions (recording function entry/exit and inputs/outputs), and X-Ray/Sentry integration, consult your repository's own logging conventions/rules document.
|
|
260
|
+
- Include identifying fields (userId, orderId, etc.) as structured fields. Never log personal data or credentials — passwords, tokens, card numbers — under any circumstances.
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// NestJS (structured logger such as pino)
|
|
264
|
+
// Bad: no context, not searchable, doesn't say what failed
|
|
265
|
+
this.logger.log('failed');
|
|
266
|
+
|
|
267
|
+
// Good: correlation ID and target ID as structured fields
|
|
268
|
+
this.logger.warn(
|
|
269
|
+
{ requestId, orderId, reason: 'stock_shortage' },
|
|
270
|
+
'order confirmation rejected',
|
|
271
|
+
);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
How to choose a log level:
|
|
275
|
+
|
|
276
|
+
- `error`: a failure that needs a human to respond. Should trigger alerts.
|
|
277
|
+
- `warn`: something abnormal but self-recovered, or an expected-in-business failure (succeeded after retry, frequent validation rejections, etc.).
|
|
278
|
+
- `info`: business-event milestones (order confirmed, job completed).
|
|
279
|
+
- `debug`: development-time detail. Disabled by default in production.
|
|
280
|
+
|
|
281
|
+
Avoid letting your level scheme collapse — e.g. logging normal operation at `error`, or swallowing an exception while logging it at `info`.
|
|
282
|
+
|
|
283
|
+
## Background Jobs
|
|
284
|
+
|
|
285
|
+
- Use a durable queue backend: BullMQ (Redis), Celery (Redis / RabbitMQ), Laravel Queue (Redis / SQS / database), SQS, and similar. In-process queues — an in-memory array, chained `setTimeout` calls, or Laravel's `sync` driver in production — are not acceptable; jobs vanish on process restart or deploy.
|
|
286
|
+
- Design jobs to be idempotent. Queues generally guarantee at-least-once delivery, so the same job may run twice — make sure that doesn't corrupt results. Absorb duplicate execution with a "already processed" flag, a uniqueness constraint, or an idempotency key.
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
# Celery
|
|
290
|
+
# Bad: running this twice double-charges the customer
|
|
291
|
+
@shared_task
|
|
292
|
+
def charge(order_id):
|
|
293
|
+
api.charge(amount=get_order(order_id).total)
|
|
294
|
+
|
|
295
|
+
# Good: a processed-check plus an idempotency key make double execution harmless
|
|
296
|
+
@shared_task(bind=True, max_retries=5)
|
|
297
|
+
def charge(self, order_id):
|
|
298
|
+
order = get_order(order_id)
|
|
299
|
+
if order.charged:
|
|
300
|
+
return
|
|
301
|
+
api.charge(amount=order.total, idempotency_key=f"charge-{order_id}")
|
|
302
|
+
order.mark_charged()
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
- Always design for the failure path: cap the retry count, use exponential backoff, and on exhausting retries, route the job to a dead-letter queue (DLQ) or a failure table (Laravel's `failed_jobs`) with a notification. Never let a failure disappear silently.
|
|
306
|
+
- Pass an ID into the job, not the full entity, and re-fetch the latest state at execution time — this avoids acting on stale data that sat in the queue for a while.
|
|
307
|
+
|
|
308
|
+
## Related Commands & Subagents
|
|
309
|
+
|
|
310
|
+
- During the design phase, use `/plan` to nail down the implementation approach before starting.
|
|
311
|
+
- After implementation, request review from `/code-review` and code-reviewer subagents.
|
|
312
|
+
- Use the silent-failure-hunter subagent to check for swallowed exceptions and silently discarded errors.
|