codecruise 0.1.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/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/codecruise.js +68 -0
- package/config/CLAUDE.md +107 -0
- package/config/agents/analyst.md +48 -0
- package/config/agents/architect-reviewer.md +161 -0
- package/config/agents/architect.md +119 -0
- package/config/agents/critic.md +63 -0
- package/config/agents/developer.md +96 -0
- package/config/agents/devops.md +81 -0
- package/config/agents/orchestrator.md +91 -0
- package/config/agents/planner.md +139 -0
- package/config/agents/retro.md +52 -0
- package/config/agents/reviewer.md +101 -0
- package/config/agents/security-reviewer.md +57 -0
- package/config/agents/stack/expo/AGENT.md +473 -0
- package/config/agents/stack/expo/rules/critical.md +427 -0
- package/config/agents/stack/expo/rules/native.md +455 -0
- package/config/agents/stack/expo/rules/navigation.md +445 -0
- package/config/agents/stack/expo/rules/performance.md +415 -0
- package/config/agents/stack/fastify/AGENT.md +397 -0
- package/config/agents/stack/fastify/rules/api-design.md +283 -0
- package/config/agents/stack/fastify/rules/critical.md +232 -0
- package/config/agents/stack/fastify/rules/queues.md +303 -0
- package/config/agents/stack/fastify/rules/security.md +384 -0
- package/config/agents/stack/index.yaml +48 -0
- package/config/agents/stack/nextjs/AGENT.md +421 -0
- package/config/agents/stack/nextjs/rules/components.md +413 -0
- package/config/agents/stack/nextjs/rules/critical.md +391 -0
- package/config/agents/stack/nextjs/rules/performance.md +403 -0
- package/config/agents/stack/nextjs/rules/styling.md +334 -0
- package/config/agents/stack/shared-ts/AGENT.md +384 -0
- package/config/agents/stack/shared-ts/rules/critical.md +315 -0
- package/config/agents/stack/shared-ts/rules/patterns.md +384 -0
- package/config/agents/stack/shared-ts/rules/zod.md +427 -0
- package/config/agents/tester.md +79 -0
- package/config/commands/architect-discuss.md +366 -0
- package/config/commands/architect-list.md +160 -0
- package/config/commands/architect-review.md +111 -0
- package/config/commands/architect.md +118 -0
- package/config/commands/compact.md +118 -0
- package/config/commands/companion.md +279 -0
- package/config/commands/dashboard.md +152 -0
- package/config/commands/doctor.md +227 -0
- package/config/commands/dogfood-report.md +101 -0
- package/config/commands/flags/run-autonomous.md +110 -0
- package/config/commands/flags/run-pause.md +80 -0
- package/config/commands/ingest.md +173 -0
- package/config/commands/init.md +128 -0
- package/config/commands/metrics.md +87 -0
- package/config/commands/parallel.md +320 -0
- package/config/commands/pause.md +55 -0
- package/config/commands/plan-review.md +130 -0
- package/config/commands/plan.md +216 -0
- package/config/commands/production-check.md +308 -0
- package/config/commands/refine.md +323 -0
- package/config/commands/resume.md +72 -0
- package/config/commands/retro.md +121 -0
- package/config/commands/retry.md +75 -0
- package/config/commands/role.md +310 -0
- package/config/commands/run.md +417 -0
- package/config/commands/scope.md +85 -0
- package/config/commands/setup-permissions.md +104 -0
- package/config/commands/skip.md +75 -0
- package/config/commands/spec-forge.md +213 -0
- package/config/commands/spec-help.md +194 -0
- package/config/commands/spec-patch.md +342 -0
- package/config/commands/spec-resolve.md +110 -0
- package/config/commands/spec-review.md +153 -0
- package/config/commands/status.md +114 -0
- package/config/commands/sync.md +131 -0
- package/config/commands/task.md +138 -0
- package/config/commands/verify.md +124 -0
- package/config/hooks/README.md +632 -0
- package/config/hooks/activity-log.sh +187 -0
- package/config/hooks/anti-rationalize.sh +52 -0
- package/config/hooks/capture-verification.sh +112 -0
- package/config/hooks/collect-metrics.sh +135 -0
- package/config/hooks/enforce-file-scope.sh +75 -0
- package/config/hooks/enforce-state-machine.sh +161 -0
- package/config/hooks/enforce-tdd.sh +180 -0
- package/config/hooks/format.sh +40 -0
- package/config/hooks/lib/activity-helpers.sh +162 -0
- package/config/hooks/lib/read-settings.sh +71 -0
- package/config/hooks/load-context-skills.sh +95 -0
- package/config/hooks/notify.sh +81 -0
- package/config/hooks/pre-commit.sample +35 -0
- package/config/hooks/protect-files.sh +63 -0
- package/config/hooks/track-agents.sh +41 -0
- package/config/hooks/track-commands.sh +37 -0
- package/config/hooks/track-enforcement.sh +44 -0
- package/config/hooks/track-ooda.sh +77 -0
- package/config/hooks/validate-commit-msg.sh +35 -0
- package/config/hooks/validate-plan.sh +213 -0
- package/config/hooks/verify-criteria.sh +46 -0
- package/config/hooks/verify-todo-completion.sh +140 -0
- package/config/rules/comments.md +25 -0
- package/config/rules/decision-rules.md +308 -0
- package/config/rules/hygiene.md +247 -0
- package/config/rules/pattern-detection.md +372 -0
- package/config/rules/profiles.md +193 -0
- package/config/rules/recovery.md +83 -0
- package/config/rules/scope-detection.md +213 -0
- package/config/rules/standards.md +127 -0
- package/config/rules/workflow.md +121 -0
- package/config/schemas.md +767 -0
- package/config/settings.json +195 -0
- package/config/skills/backend/SKILL.md +734 -0
- package/config/skills/database/SKILL.md +426 -0
- package/config/skills/frontend/SKILL.md +434 -0
- package/config/skills/git/SKILL.md +396 -0
- package/config/skills/index.yaml +36 -0
- package/config/skills/observability/SKILL.md +430 -0
- package/config/skills/package-dev/SKILL.md +498 -0
- package/config/skills/performance/SKILL.md +378 -0
- package/config/skills/resilience/SKILL.md +573 -0
- package/config/skills/testing/SKILL.md +398 -0
- package/config/skills/testing-patterns/SKILL.md +276 -0
- package/config/skills/typescript/SKILL.md +152 -0
- package/config/templates/CLAUDE.md +70 -0
- package/config/templates/README.md +117 -0
- package/config/templates/steering/adr-template.md +102 -0
- package/config/templates/steering/product.md +60 -0
- package/config/templates/steering/rfc-template.md +159 -0
- package/config/templates/steering/structure.md +146 -0
- package/config/templates/steering/tech.md +85 -0
- package/package.json +40 -0
- package/src/install.js +163 -0
- package/src/report.js +310 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend
|
|
3
|
+
description: API design, database, and security patterns
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
triggers:
|
|
6
|
+
- api
|
|
7
|
+
- endpoint
|
|
8
|
+
- database
|
|
9
|
+
- SQL
|
|
10
|
+
- auth
|
|
11
|
+
- server
|
|
12
|
+
- REST
|
|
13
|
+
- tRPC
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Backend Skill
|
|
17
|
+
|
|
18
|
+
Comprehensive API, database, and security patterns.
|
|
19
|
+
|
|
20
|
+
## Quick Reference
|
|
21
|
+
|
|
22
|
+
### API Design
|
|
23
|
+
|
|
24
|
+
| ID | Rule | Priority |
|
|
25
|
+
|----|------|----------|
|
|
26
|
+
| api-1 | Validate all inputs at boundaries with Zod | CRITICAL |
|
|
27
|
+
| api-2 | Use proper HTTP status codes | CRITICAL |
|
|
28
|
+
| api-3 | Return consistent error format | CRITICAL |
|
|
29
|
+
| api-4 | Never expose internal errors to clients | CRITICAL |
|
|
30
|
+
| api-5 | Use RESTful conventions for endpoints | HIGH |
|
|
31
|
+
| api-6 | Implement rate limiting on public endpoints | HIGH |
|
|
32
|
+
| api-7 | Version APIs when breaking changes needed | HIGH |
|
|
33
|
+
| api-8 | Use pagination for list endpoints | MEDIUM |
|
|
34
|
+
| api-9 | Document endpoints with OpenAPI | MEDIUM |
|
|
35
|
+
| api-10 | Use service layer for business logic | MEDIUM |
|
|
36
|
+
|
|
37
|
+
### Database (General)
|
|
38
|
+
|
|
39
|
+
| ID | Rule | Priority |
|
|
40
|
+
|----|------|----------|
|
|
41
|
+
| db-1 | Use parameterized queries, never interpolate | CRITICAL |
|
|
42
|
+
| db-2 | Always use transactions for related writes | CRITICAL |
|
|
43
|
+
| db-3 | Prevent N+1 queries with eager loading | HIGH |
|
|
44
|
+
| db-4 | Index columns used in WHERE and JOIN | HIGH |
|
|
45
|
+
| db-5 | Use connection pooling in production | HIGH |
|
|
46
|
+
| db-6 | Add created_at and updated_at to all tables | MEDIUM |
|
|
47
|
+
| db-7 | Use soft deletes for recoverable data | MEDIUM |
|
|
48
|
+
| db-8 | Validate foreign key constraints | MEDIUM |
|
|
49
|
+
| db-9 | Use decimal/string for money, never float | CRITICAL |
|
|
50
|
+
| db-10 | Implement optimistic locking for concurrent updates | MEDIUM |
|
|
51
|
+
|
|
52
|
+
### PostgreSQL
|
|
53
|
+
|
|
54
|
+
| ID | Rule | Priority |
|
|
55
|
+
|----|------|----------|
|
|
56
|
+
| pg-1 | Use JSONB for flexible schema fields | HIGH |
|
|
57
|
+
| pg-2 | Create partial indexes for filtered queries | HIGH |
|
|
58
|
+
| pg-3 | Use EXPLAIN ANALYZE for query optimization | HIGH |
|
|
59
|
+
| pg-4 | Use UUID v7 for sortable primary keys | MEDIUM |
|
|
60
|
+
| pg-5 | Use CTEs for complex queries | MEDIUM |
|
|
61
|
+
| pg-6 | Use pg_trgm for text search | MEDIUM |
|
|
62
|
+
| pg-7 | Use row-level security for multi-tenancy | HIGH |
|
|
63
|
+
| pg-8 | Set statement_timeout to prevent runaway queries | HIGH |
|
|
64
|
+
|
|
65
|
+
### MongoDB
|
|
66
|
+
|
|
67
|
+
| ID | Rule | Priority |
|
|
68
|
+
|----|------|----------|
|
|
69
|
+
| mongo-1 | Design schema for query patterns, not normalization | CRITICAL |
|
|
70
|
+
| mongo-2 | Embed frequently accessed related data | HIGH |
|
|
71
|
+
| mongo-3 | Use references for large/unbounded arrays | HIGH |
|
|
72
|
+
| mongo-4 | Create compound indexes matching query patterns | HIGH |
|
|
73
|
+
| mongo-5 | Use aggregation pipeline for complex queries | MEDIUM |
|
|
74
|
+
| mongo-6 | Set maxTimeMS to prevent slow queries | HIGH |
|
|
75
|
+
| mongo-7 | Use change streams for real-time updates | MEDIUM |
|
|
76
|
+
| mongo-8 | Avoid unbounded array growth | CRITICAL |
|
|
77
|
+
|
|
78
|
+
### Security
|
|
79
|
+
|
|
80
|
+
| ID | Rule | Priority |
|
|
81
|
+
|----|------|----------|
|
|
82
|
+
| sec-1 | Authenticate before authorize | CRITICAL |
|
|
83
|
+
| sec-2 | Check resource ownership on every request | CRITICAL |
|
|
84
|
+
| sec-3 | Never log passwords or tokens | CRITICAL |
|
|
85
|
+
| sec-4 | Use httpOnly cookies for session tokens | CRITICAL |
|
|
86
|
+
| sec-5 | Hash passwords with bcrypt/argon2 | CRITICAL |
|
|
87
|
+
| sec-6 | Implement CSRF protection for mutations | HIGH |
|
|
88
|
+
| sec-7 | Set secure headers (CSP, X-Frame-Options) | HIGH |
|
|
89
|
+
| sec-8 | Sanitize user input before storing | HIGH |
|
|
90
|
+
| sec-9 | Use secrets manager, not env files in production | HIGH |
|
|
91
|
+
| sec-10 | Audit log sensitive operations | MEDIUM |
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Critical Rules
|
|
96
|
+
|
|
97
|
+
### api-1: Input Validation
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { z } from 'zod';
|
|
101
|
+
|
|
102
|
+
const CreateUserSchema = z.object({
|
|
103
|
+
email: z.string().email(),
|
|
104
|
+
password: z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/),
|
|
105
|
+
name: z.string().min(2).max(100),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export async function POST(request: NextRequest) {
|
|
109
|
+
const body = await request.json();
|
|
110
|
+
const data = CreateUserSchema.parse(body); // Throws on invalid
|
|
111
|
+
// data is now validated and typed
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### api-2: HTTP Status Codes
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
200 OK - Success (GET, PUT, PATCH)
|
|
119
|
+
201 Created - Resource created (POST)
|
|
120
|
+
204 No Content - Success, no body (DELETE)
|
|
121
|
+
400 Bad Request - Validation error
|
|
122
|
+
401 Unauthorized - Authentication required
|
|
123
|
+
403 Forbidden - Insufficient permissions
|
|
124
|
+
404 Not Found - Resource not found
|
|
125
|
+
409 Conflict - Duplicate resource
|
|
126
|
+
422 Unprocessable - Valid syntax, invalid data
|
|
127
|
+
429 Too Many - Rate limited
|
|
128
|
+
500 Internal Error - Server error (hide details)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### api-3: Error Response Format
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// Consistent error format
|
|
135
|
+
interface ErrorResponse {
|
|
136
|
+
error: {
|
|
137
|
+
code: string; // Machine-readable: 'VALIDATION_ERROR'
|
|
138
|
+
message: string; // Human-readable: 'Invalid email format'
|
|
139
|
+
details?: { // Field-level errors
|
|
140
|
+
field: string;
|
|
141
|
+
message: string;
|
|
142
|
+
}[];
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Error handler middleware
|
|
147
|
+
function handleError(error: unknown): NextResponse<ErrorResponse> {
|
|
148
|
+
if (error instanceof z.ZodError) {
|
|
149
|
+
return NextResponse.json({
|
|
150
|
+
error: {
|
|
151
|
+
code: 'VALIDATION_ERROR',
|
|
152
|
+
message: 'Invalid input',
|
|
153
|
+
details: error.errors.map(e => ({
|
|
154
|
+
field: e.path.join('.'),
|
|
155
|
+
message: e.message,
|
|
156
|
+
})),
|
|
157
|
+
},
|
|
158
|
+
}, { status: 400 });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (error instanceof AppError) {
|
|
162
|
+
return NextResponse.json({
|
|
163
|
+
error: { code: error.code, message: error.message },
|
|
164
|
+
}, { status: error.statusCode });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Never expose internal errors
|
|
168
|
+
console.error('Unexpected error:', error);
|
|
169
|
+
return NextResponse.json({
|
|
170
|
+
error: { code: 'INTERNAL_ERROR', message: 'Something went wrong' },
|
|
171
|
+
}, { status: 500 });
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### db-1: Parameterized Queries
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// GOOD: Prisma handles escaping
|
|
179
|
+
await prisma.user.findMany({
|
|
180
|
+
where: { email: userInput }
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// GOOD: Raw query with parameters
|
|
184
|
+
await prisma.$queryRaw`
|
|
185
|
+
SELECT * FROM users WHERE email = ${userInput}
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
// BAD: String interpolation (SQL injection!)
|
|
189
|
+
// await prisma.$queryRawUnsafe(`SELECT * FROM users WHERE email = '${userInput}'`);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### db-2: Transactions
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// Related writes must be atomic
|
|
196
|
+
await prisma.$transaction(async (tx) => {
|
|
197
|
+
const user = await tx.user.create({ data: userData });
|
|
198
|
+
await tx.profile.create({ data: { userId: user.id, ...profileData } });
|
|
199
|
+
await tx.auditLog.create({ data: { action: 'USER_CREATED', userId: user.id } });
|
|
200
|
+
return user;
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### db-3: Prevent N+1
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// BAD: N+1 queries
|
|
208
|
+
const users = await prisma.user.findMany();
|
|
209
|
+
for (const user of users) {
|
|
210
|
+
const posts = await prisma.post.findMany({ where: { userId: user.id } });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// GOOD: Eager loading
|
|
214
|
+
const users = await prisma.user.findMany({
|
|
215
|
+
include: {
|
|
216
|
+
posts: {
|
|
217
|
+
take: 5,
|
|
218
|
+
orderBy: { createdAt: 'desc' },
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### db-9: Money as Decimal
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// BAD: Float precision issues
|
|
228
|
+
// 0.1 + 0.2 = 0.30000000000000004
|
|
229
|
+
|
|
230
|
+
// GOOD: Use Decimal or string in cents
|
|
231
|
+
model Transaction {
|
|
232
|
+
amount Decimal @db.Decimal(19, 4)
|
|
233
|
+
currency String @db.VarChar(3)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Or store cents as integer
|
|
237
|
+
model Transaction {
|
|
238
|
+
amountCents Int
|
|
239
|
+
currency String
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Display: formatMoney(amountCents / 100)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## PostgreSQL Best Practices
|
|
248
|
+
|
|
249
|
+
### pg-1: JSONB for Flexible Fields
|
|
250
|
+
|
|
251
|
+
```sql
|
|
252
|
+
-- Store flexible metadata without schema changes
|
|
253
|
+
CREATE TABLE products (
|
|
254
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
255
|
+
name TEXT NOT NULL,
|
|
256
|
+
metadata JSONB DEFAULT '{}'::jsonb
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
-- Query JSONB fields
|
|
260
|
+
SELECT * FROM products
|
|
261
|
+
WHERE metadata->>'category' = 'electronics'
|
|
262
|
+
AND (metadata->>'price')::numeric > 100;
|
|
263
|
+
|
|
264
|
+
-- Index JSONB for performance
|
|
265
|
+
CREATE INDEX idx_products_metadata ON products USING GIN (metadata);
|
|
266
|
+
|
|
267
|
+
-- Partial index for specific queries
|
|
268
|
+
CREATE INDEX idx_products_category ON products ((metadata->>'category'));
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### pg-2: Partial Indexes
|
|
272
|
+
|
|
273
|
+
```sql
|
|
274
|
+
-- Index only active users (smaller, faster)
|
|
275
|
+
CREATE INDEX idx_users_active_email ON users (email)
|
|
276
|
+
WHERE deleted_at IS NULL AND status = 'active';
|
|
277
|
+
|
|
278
|
+
-- Index only recent orders
|
|
279
|
+
CREATE INDEX idx_orders_recent ON orders (created_at DESC)
|
|
280
|
+
WHERE created_at > NOW() - INTERVAL '30 days';
|
|
281
|
+
|
|
282
|
+
-- Unique constraint with condition
|
|
283
|
+
CREATE UNIQUE INDEX idx_users_unique_email ON users (email)
|
|
284
|
+
WHERE deleted_at IS NULL;
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### pg-4: UUID v7 for Primary Keys
|
|
288
|
+
|
|
289
|
+
```sql
|
|
290
|
+
-- UUID v7: timestamp-sortable, better for indexes
|
|
291
|
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
292
|
+
|
|
293
|
+
-- Drizzle schema
|
|
294
|
+
export const users = pgTable('users', {
|
|
295
|
+
id: uuid('id').primaryKey().defaultRandom(),
|
|
296
|
+
// Or use uuid_generate_v7() with extension
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
-- Benefits over serial:
|
|
300
|
+
-- - No sequence contention
|
|
301
|
+
-- - Can be generated client-side
|
|
302
|
+
-- - Distributed-friendly
|
|
303
|
+
-- - No information leakage
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### pg-7: Row-Level Security for Multi-Tenancy
|
|
307
|
+
|
|
308
|
+
```sql
|
|
309
|
+
-- Enable RLS
|
|
310
|
+
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
|
|
311
|
+
|
|
312
|
+
-- Create policy: users see only their org's documents
|
|
313
|
+
CREATE POLICY documents_isolation ON documents
|
|
314
|
+
FOR ALL
|
|
315
|
+
USING (org_id = current_setting('app.current_org_id')::uuid);
|
|
316
|
+
|
|
317
|
+
-- Set org context before queries
|
|
318
|
+
SET app.current_org_id = 'org-uuid-here';
|
|
319
|
+
|
|
320
|
+
-- Now all queries are automatically filtered
|
|
321
|
+
SELECT * FROM documents; -- Only returns current org's docs
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### pg-8: Statement Timeout
|
|
325
|
+
|
|
326
|
+
```sql
|
|
327
|
+
-- Prevent runaway queries (set in connection)
|
|
328
|
+
SET statement_timeout = '30s';
|
|
329
|
+
|
|
330
|
+
-- Or per-transaction
|
|
331
|
+
BEGIN;
|
|
332
|
+
SET LOCAL statement_timeout = '5s';
|
|
333
|
+
SELECT * FROM expensive_query();
|
|
334
|
+
COMMIT;
|
|
335
|
+
|
|
336
|
+
-- In Prisma connection string
|
|
337
|
+
DATABASE_URL="postgresql://...?statement_timeout=30000"
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## MongoDB Best Practices
|
|
343
|
+
|
|
344
|
+
### mongo-1: Schema Design for Queries
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// Design schema based on how you query, not how data relates
|
|
348
|
+
|
|
349
|
+
// BAD: Normalized (requires multiple queries)
|
|
350
|
+
// users collection: { _id, name }
|
|
351
|
+
// orders collection: { _id, userId, items }
|
|
352
|
+
// items collection: { _id, orderId, productId }
|
|
353
|
+
|
|
354
|
+
// GOOD: Denormalized for common query
|
|
355
|
+
// orders collection:
|
|
356
|
+
{
|
|
357
|
+
_id: ObjectId,
|
|
358
|
+
user: { _id, name, email }, // Embedded (frequently accessed)
|
|
359
|
+
items: [
|
|
360
|
+
{ productId, name, price, quantity } // Embedded array
|
|
361
|
+
],
|
|
362
|
+
total: Decimal128,
|
|
363
|
+
status: 'pending'
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Query: Get order with all details in ONE query
|
|
367
|
+
const order = await db.orders.findOne({ _id: orderId });
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### mongo-2: Embed vs Reference
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
// EMBED when:
|
|
374
|
+
// - Data is accessed together
|
|
375
|
+
// - Data belongs to parent
|
|
376
|
+
// - Array is bounded (<100 items)
|
|
377
|
+
|
|
378
|
+
// User with addresses (bounded, always accessed together)
|
|
379
|
+
{
|
|
380
|
+
_id: ObjectId,
|
|
381
|
+
name: 'John',
|
|
382
|
+
addresses: [
|
|
383
|
+
{ type: 'home', street: '123 Main' },
|
|
384
|
+
{ type: 'work', street: '456 Office' }
|
|
385
|
+
]
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// REFERENCE when:
|
|
389
|
+
// - Data is large
|
|
390
|
+
// - Data is accessed independently
|
|
391
|
+
// - Array could grow unbounded
|
|
392
|
+
|
|
393
|
+
// Blog post with comments (unbounded)
|
|
394
|
+
// posts collection
|
|
395
|
+
{ _id: ObjectId, title: 'Post', authorId: ObjectId }
|
|
396
|
+
|
|
397
|
+
// comments collection (separate)
|
|
398
|
+
{ _id: ObjectId, postId: ObjectId, text: '...', authorId: ObjectId }
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### mongo-4: Compound Indexes
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// Index must match query pattern exactly
|
|
405
|
+
// Order matters: equality → sort → range
|
|
406
|
+
|
|
407
|
+
// Query: Find active users, sort by createdAt, filter by age
|
|
408
|
+
db.users.find({ status: 'active', age: { $gte: 18 } }).sort({ createdAt: -1 })
|
|
409
|
+
|
|
410
|
+
// Index: status (equality) → createdAt (sort) → age (range)
|
|
411
|
+
db.users.createIndex({ status: 1, createdAt: -1, age: 1 });
|
|
412
|
+
|
|
413
|
+
// Covered query (index-only, fastest)
|
|
414
|
+
db.users.find(
|
|
415
|
+
{ status: 'active' },
|
|
416
|
+
{ _id: 0, status: 1, createdAt: 1 } // Only indexed fields
|
|
417
|
+
).hint({ status: 1, createdAt: -1 });
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### mongo-5: Aggregation Pipeline
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// Complex queries with aggregation
|
|
424
|
+
const result = await db.orders.aggregate([
|
|
425
|
+
// Stage 1: Filter
|
|
426
|
+
{ $match: { status: 'completed', createdAt: { $gte: lastMonth } } },
|
|
427
|
+
|
|
428
|
+
// Stage 2: Group by user
|
|
429
|
+
{ $group: {
|
|
430
|
+
_id: '$userId',
|
|
431
|
+
totalSpent: { $sum: '$total' },
|
|
432
|
+
orderCount: { $sum: 1 }
|
|
433
|
+
}},
|
|
434
|
+
|
|
435
|
+
// Stage 3: Sort by spend
|
|
436
|
+
{ $sort: { totalSpent: -1 } },
|
|
437
|
+
|
|
438
|
+
// Stage 4: Top 10
|
|
439
|
+
{ $limit: 10 },
|
|
440
|
+
|
|
441
|
+
// Stage 5: Lookup user details
|
|
442
|
+
{ $lookup: {
|
|
443
|
+
from: 'users',
|
|
444
|
+
localField: '_id',
|
|
445
|
+
foreignField: '_id',
|
|
446
|
+
as: 'user'
|
|
447
|
+
}},
|
|
448
|
+
|
|
449
|
+
// Stage 6: Reshape
|
|
450
|
+
{ $project: {
|
|
451
|
+
userName: { $arrayElemAt: ['$user.name', 0] },
|
|
452
|
+
totalSpent: 1,
|
|
453
|
+
orderCount: 1
|
|
454
|
+
}}
|
|
455
|
+
]);
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### mongo-8: Avoid Unbounded Arrays
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
// BAD: Array grows forever
|
|
462
|
+
{
|
|
463
|
+
_id: 'popular-post',
|
|
464
|
+
likes: ['user1', 'user2', ... 'user1000000'] // Document too large!
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// GOOD: Bucket pattern (fixed array size)
|
|
468
|
+
{
|
|
469
|
+
_id: ObjectId,
|
|
470
|
+
postId: 'popular-post',
|
|
471
|
+
bucket: 1,
|
|
472
|
+
likes: ['user1', ..., 'user100'], // Max 100 per bucket
|
|
473
|
+
count: 100
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// GOOD: Separate collection
|
|
477
|
+
// likes collection
|
|
478
|
+
{ postId: 'popular-post', userId: 'user1', createdAt: Date }
|
|
479
|
+
|
|
480
|
+
// Count with aggregation or maintain counter
|
|
481
|
+
{ _id: 'popular-post', likeCount: 1000000 }
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### sec-2: Check Resource Ownership
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
export async function DELETE(
|
|
488
|
+
request: NextRequest,
|
|
489
|
+
{ params }: { params: { id: string } }
|
|
490
|
+
) {
|
|
491
|
+
const session = await getSession();
|
|
492
|
+
if (!session) {
|
|
493
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const resource = await prisma.resource.findUnique({
|
|
497
|
+
where: { id: params.id },
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
if (!resource) {
|
|
501
|
+
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// CRITICAL: Check ownership
|
|
505
|
+
if (resource.userId !== session.user.id && session.user.role !== 'ADMIN') {
|
|
506
|
+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
await prisma.resource.delete({ where: { id: params.id } });
|
|
510
|
+
return NextResponse.json({ success: true });
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### sec-4: Secure Session Cookies
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
const sessionOptions = {
|
|
518
|
+
password: process.env.SESSION_SECRET!, // 32+ chars
|
|
519
|
+
cookieName: 'session',
|
|
520
|
+
cookieOptions: {
|
|
521
|
+
httpOnly: true, // No JS access
|
|
522
|
+
secure: true, // HTTPS only
|
|
523
|
+
sameSite: 'lax', // CSRF protection
|
|
524
|
+
maxAge: 60 * 60 * 24, // 24 hours
|
|
525
|
+
path: '/',
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### sec-5: Password Hashing
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
import { hash, verify } from '@node-rs/argon2';
|
|
534
|
+
|
|
535
|
+
// Hash password before storing
|
|
536
|
+
const hashedPassword = await hash(plainPassword, {
|
|
537
|
+
memoryCost: 65536,
|
|
538
|
+
timeCost: 3,
|
|
539
|
+
parallelism: 4,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// Verify on login
|
|
543
|
+
const isValid = await verify(hashedPassword, inputPassword);
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## API Patterns
|
|
549
|
+
|
|
550
|
+
### RESTful Endpoints
|
|
551
|
+
|
|
552
|
+
```
|
|
553
|
+
GET /users # List users
|
|
554
|
+
GET /users/:id # Get user
|
|
555
|
+
POST /users # Create user
|
|
556
|
+
PATCH /users/:id # Update user
|
|
557
|
+
DELETE /users/:id # Delete user
|
|
558
|
+
|
|
559
|
+
GET /users/:id/posts # Nested resources
|
|
560
|
+
POST /users/:id/posts
|
|
561
|
+
|
|
562
|
+
# Query params for filtering
|
|
563
|
+
GET /users?role=admin&status=active&page=2&limit=20&sort=-createdAt
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Pagination
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
const PaginationSchema = z.object({
|
|
570
|
+
page: z.coerce.number().min(1).default(1),
|
|
571
|
+
limit: z.coerce.number().min(1).max(100).default(20),
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
export async function GET(request: NextRequest) {
|
|
575
|
+
const params = Object.fromEntries(request.nextUrl.searchParams);
|
|
576
|
+
const { page, limit } = PaginationSchema.parse(params);
|
|
577
|
+
|
|
578
|
+
const [data, total] = await Promise.all([
|
|
579
|
+
prisma.user.findMany({
|
|
580
|
+
skip: (page - 1) * limit,
|
|
581
|
+
take: limit,
|
|
582
|
+
}),
|
|
583
|
+
prisma.user.count(),
|
|
584
|
+
]);
|
|
585
|
+
|
|
586
|
+
return NextResponse.json({
|
|
587
|
+
data,
|
|
588
|
+
meta: {
|
|
589
|
+
page,
|
|
590
|
+
limit,
|
|
591
|
+
total,
|
|
592
|
+
totalPages: Math.ceil(total / limit),
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Service Layer
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
// services/user.service.ts
|
|
602
|
+
export class UserService {
|
|
603
|
+
async create(input: CreateUserInput): Promise<User> {
|
|
604
|
+
const existing = await prisma.user.findUnique({
|
|
605
|
+
where: { email: input.email },
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
if (existing) {
|
|
609
|
+
throw new ConflictError('Email already exists');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return prisma.user.create({
|
|
613
|
+
data: {
|
|
614
|
+
...input,
|
|
615
|
+
password: await hash(input.password),
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async findById(id: string): Promise<User> {
|
|
621
|
+
const user = await prisma.user.findUnique({ where: { id } });
|
|
622
|
+
if (!user) throw new NotFoundError('User');
|
|
623
|
+
return user;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
### Rate Limiting
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
import { Ratelimit } from '@upstash/ratelimit';
|
|
632
|
+
import { Redis } from '@upstash/redis';
|
|
633
|
+
|
|
634
|
+
const ratelimit = new Ratelimit({
|
|
635
|
+
redis: Redis.fromEnv(),
|
|
636
|
+
limiter: Ratelimit.slidingWindow(10, '10s'),
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
export async function POST(request: NextRequest) {
|
|
640
|
+
const ip = request.ip ?? '127.0.0.1';
|
|
641
|
+
const { success, remaining } = await ratelimit.limit(ip);
|
|
642
|
+
|
|
643
|
+
if (!success) {
|
|
644
|
+
return NextResponse.json(
|
|
645
|
+
{ error: { code: 'RATE_LIMITED', message: 'Too many requests' } },
|
|
646
|
+
{ status: 429, headers: { 'X-RateLimit-Remaining': String(remaining) } }
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Process request...
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## Error Classes
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
export class AppError extends Error {
|
|
660
|
+
constructor(
|
|
661
|
+
public code: string,
|
|
662
|
+
message: string,
|
|
663
|
+
public statusCode: number = 500
|
|
664
|
+
) {
|
|
665
|
+
super(message);
|
|
666
|
+
this.name = 'AppError';
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export class ValidationError extends AppError {
|
|
671
|
+
constructor(message: string) {
|
|
672
|
+
super('VALIDATION_ERROR', message, 400);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export class NotFoundError extends AppError {
|
|
677
|
+
constructor(resource: string) {
|
|
678
|
+
super('NOT_FOUND', `${resource} not found`, 404);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export class UnauthorizedError extends AppError {
|
|
683
|
+
constructor() {
|
|
684
|
+
super('UNAUTHORIZED', 'Authentication required', 401);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export class ForbiddenError extends AppError {
|
|
689
|
+
constructor() {
|
|
690
|
+
super('FORBIDDEN', 'Insufficient permissions', 403);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export class ConflictError extends AppError {
|
|
695
|
+
constructor(message: string) {
|
|
696
|
+
super('CONFLICT', message, 409);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
---
|
|
702
|
+
|
|
703
|
+
## Logging
|
|
704
|
+
|
|
705
|
+
```typescript
|
|
706
|
+
// Never log sensitive data
|
|
707
|
+
const logger = {
|
|
708
|
+
info: (message: string, meta?: object) => {
|
|
709
|
+
console.log(JSON.stringify({
|
|
710
|
+
level: 'info',
|
|
711
|
+
message,
|
|
712
|
+
...meta,
|
|
713
|
+
timestamp: new Date().toISOString(),
|
|
714
|
+
}));
|
|
715
|
+
},
|
|
716
|
+
|
|
717
|
+
error: (message: string, error?: Error) => {
|
|
718
|
+
console.error(JSON.stringify({
|
|
719
|
+
level: 'error',
|
|
720
|
+
message,
|
|
721
|
+
error: error ? {
|
|
722
|
+
name: error.name,
|
|
723
|
+
message: error.message,
|
|
724
|
+
stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
|
|
725
|
+
} : undefined,
|
|
726
|
+
timestamp: new Date().toISOString(),
|
|
727
|
+
}));
|
|
728
|
+
},
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
// Usage
|
|
732
|
+
logger.info('User created', { userId: user.id }); // Good
|
|
733
|
+
// logger.info('User created', { email, password }); // BAD!
|
|
734
|
+
```
|