clearctx 3.0.0 → 3.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/README.md +1 -0
- package/bin/setup.js +33 -1
- package/package.json +3 -2
- package/skills/api-design/SKILL.md +796 -0
- package/skills/devops/SKILL.md +1043 -0
- package/skills/index.json +53 -0
- package/skills/nodejs-backend/SKILL.md +853 -0
- package/skills/postgresql/SKILL.md +315 -0
- package/skills/react-frontend/SKILL.md +683 -0
- package/skills/security/SKILL.md +1000 -0
- package/skills/testing-qa/SKILL.md +842 -0
- package/skills/typescript/SKILL.md +932 -0
- package/src/mcp-server.js +126 -1
- package/src/prompts.js +47 -2
- package/src/skill-registry.js +182 -0
- package/src/stream-session.js +22 -2
- package/STRATEGY.md +0 -485
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nodejs-backend
|
|
3
|
+
description: Production-grade Node.js/Express.js backend patterns for APIs, middleware, error handling, and operational excellence
|
|
4
|
+
domain: backend
|
|
5
|
+
keywords: [nodejs, express, backend, api, middleware, error-handling, logging, validation]
|
|
6
|
+
version: 1.0.0
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Node.js Backend Expertise
|
|
10
|
+
|
|
11
|
+
Production-grade patterns for building robust Express.js APIs with operational excellence.
|
|
12
|
+
|
|
13
|
+
## Worker Context
|
|
14
|
+
|
|
15
|
+
### Express.js Project Structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
src/
|
|
19
|
+
├── controllers/ # HTTP request handling, response formatting
|
|
20
|
+
├── services/ # Business logic, data orchestration
|
|
21
|
+
├── repositories/ # Data access layer (DB queries)
|
|
22
|
+
├── middleware/ # Cross-cutting concerns
|
|
23
|
+
├── models/ # Data schemas (Zod, Sequelize, Prisma)
|
|
24
|
+
├── utils/ # Helpers, constants
|
|
25
|
+
└── app.js # Express app setup
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Middleware Execution Order (CRITICAL):**
|
|
29
|
+
```javascript
|
|
30
|
+
// ALWAYS follow this sequence:
|
|
31
|
+
app.use(cors()); // 1. CORS - first to set headers
|
|
32
|
+
app.use(helmet()); // 2. Security headers
|
|
33
|
+
app.use(rateLimit); // 3. Rate limiting
|
|
34
|
+
app.use(requestIdMiddleware); // 4. Request ID tracking
|
|
35
|
+
app.use(express.json()); // 5. Body parsing
|
|
36
|
+
app.use(authMiddleware); // 6. Authentication
|
|
37
|
+
// Routes with validation middleware
|
|
38
|
+
app.use('/api', routes); // 7. Application routes
|
|
39
|
+
app.use(notFoundHandler); // 8. 404 handler
|
|
40
|
+
app.use(errorHandler); // 9. Error handler (LAST)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Centralized Error Handling
|
|
44
|
+
|
|
45
|
+
**AppError Class (CRITICAL - create this first):**
|
|
46
|
+
```javascript
|
|
47
|
+
// utils/AppError.js
|
|
48
|
+
class AppError extends Error {
|
|
49
|
+
constructor(message, statusCode, code = 'INTERNAL_ERROR') {
|
|
50
|
+
super(message);
|
|
51
|
+
this.statusCode = statusCode;
|
|
52
|
+
this.code = code;
|
|
53
|
+
this.isOperational = true; // vs programmer errors
|
|
54
|
+
Error.captureStackTrace(this, this.constructor);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = AppError;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Error Handler Middleware:**
|
|
62
|
+
```javascript
|
|
63
|
+
// middleware/errorHandler.js
|
|
64
|
+
const { logger } = require('../utils/logger');
|
|
65
|
+
|
|
66
|
+
const errorHandler = (err, req, res, next) => {
|
|
67
|
+
// Log error with request context
|
|
68
|
+
logger.error({
|
|
69
|
+
err,
|
|
70
|
+
requestId: req.id,
|
|
71
|
+
method: req.method,
|
|
72
|
+
path: req.path,
|
|
73
|
+
userId: req.user?.id
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Operational errors (expected)
|
|
77
|
+
if (err.isOperational) {
|
|
78
|
+
return res.status(err.statusCode).json({
|
|
79
|
+
error: {
|
|
80
|
+
code: err.code,
|
|
81
|
+
message: err.message,
|
|
82
|
+
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Programmer errors (unexpected) - don't leak details
|
|
88
|
+
res.status(500).json({
|
|
89
|
+
error: {
|
|
90
|
+
code: 'INTERNAL_ERROR',
|
|
91
|
+
message: 'An unexpected error occurred'
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
module.exports = errorHandler;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**AsyncHandler Wrapper (CRITICAL - use for all async routes):**
|
|
100
|
+
```javascript
|
|
101
|
+
// utils/asyncHandler.js
|
|
102
|
+
const asyncHandler = (fn) => (req, res, next) => {
|
|
103
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
module.exports = asyncHandler;
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Middleware Patterns
|
|
110
|
+
|
|
111
|
+
**Authentication (Bearer Token):**
|
|
112
|
+
```javascript
|
|
113
|
+
// middleware/auth.js
|
|
114
|
+
const AppError = require('../utils/AppError');
|
|
115
|
+
const { verifyToken } = require('../utils/jwt');
|
|
116
|
+
|
|
117
|
+
const auth = asyncHandler(async (req, res, next) => {
|
|
118
|
+
const authHeader = req.headers.authorization;
|
|
119
|
+
|
|
120
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
121
|
+
throw new AppError('Missing or invalid authorization header', 401, 'UNAUTHORIZED');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const token = authHeader.substring(7);
|
|
125
|
+
const payload = verifyToken(token); // throws if invalid
|
|
126
|
+
|
|
127
|
+
req.user = payload; // Attach user to request
|
|
128
|
+
next();
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Request Validation (Zod):**
|
|
133
|
+
```javascript
|
|
134
|
+
// middleware/validate.js
|
|
135
|
+
const { z } = require('zod');
|
|
136
|
+
const AppError = require('../utils/AppError');
|
|
137
|
+
|
|
138
|
+
const validate = (schema) => (req, res, next) => {
|
|
139
|
+
try {
|
|
140
|
+
// Validate body, params, and query
|
|
141
|
+
const validated = schema.parse({
|
|
142
|
+
body: req.body,
|
|
143
|
+
params: req.params,
|
|
144
|
+
query: req.query
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Replace with validated/sanitized data
|
|
148
|
+
req.body = validated.body || req.body;
|
|
149
|
+
req.params = validated.params || req.params;
|
|
150
|
+
req.query = validated.query || req.query;
|
|
151
|
+
|
|
152
|
+
next();
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error instanceof z.ZodError) {
|
|
155
|
+
throw new AppError(
|
|
156
|
+
'Validation failed',
|
|
157
|
+
422,
|
|
158
|
+
'VALIDATION_ERROR',
|
|
159
|
+
{ details: error.errors }
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Usage in routes:
|
|
167
|
+
const createUserSchema = z.object({
|
|
168
|
+
body: z.object({
|
|
169
|
+
email: z.string().email(),
|
|
170
|
+
password: z.string().min(8),
|
|
171
|
+
name: z.string().min(1)
|
|
172
|
+
})
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
router.post('/users', validate(createUserSchema), createUser);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Rate Limiting:**
|
|
179
|
+
```javascript
|
|
180
|
+
// middleware/rateLimit.js
|
|
181
|
+
const rateLimit = require('express-rate-limit');
|
|
182
|
+
const RedisStore = require('rate-limit-redis');
|
|
183
|
+
const redis = require('../config/redis');
|
|
184
|
+
|
|
185
|
+
const limiter = rateLimit({
|
|
186
|
+
store: new RedisStore({ client: redis }),
|
|
187
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
188
|
+
max: 100, // limit each IP to 100 requests per windowMs
|
|
189
|
+
message: { error: { code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests' } },
|
|
190
|
+
standardHeaders: true,
|
|
191
|
+
legacyHeaders: false
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Request ID Tracking:**
|
|
196
|
+
```javascript
|
|
197
|
+
// middleware/requestId.js
|
|
198
|
+
const { v4: uuidv4 } = require('uuid');
|
|
199
|
+
|
|
200
|
+
const requestId = (req, res, next) => {
|
|
201
|
+
req.id = req.headers['x-request-id'] || uuidv4();
|
|
202
|
+
res.setHeader('X-Request-ID', req.id);
|
|
203
|
+
next();
|
|
204
|
+
};
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Request Validation Strategy
|
|
208
|
+
|
|
209
|
+
**IMPORTANT: Validate at route level, NOT in controllers**
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
// schemas/user.schema.js
|
|
213
|
+
const { z } = require('zod');
|
|
214
|
+
|
|
215
|
+
const createUserSchema = z.object({
|
|
216
|
+
body: z.object({
|
|
217
|
+
email: z.string().email().toLowerCase(), // Sanitize
|
|
218
|
+
password: z.string().min(8).max(128),
|
|
219
|
+
name: z.string().min(1).max(100).trim()
|
|
220
|
+
})
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const updateUserSchema = z.object({
|
|
224
|
+
params: z.object({
|
|
225
|
+
id: z.string().uuid()
|
|
226
|
+
}),
|
|
227
|
+
body: z.object({
|
|
228
|
+
name: z.string().min(1).max(100).trim().optional(),
|
|
229
|
+
bio: z.string().max(500).optional()
|
|
230
|
+
})
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
module.exports = { createUserSchema, updateUserSchema };
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Response Format Standards
|
|
237
|
+
|
|
238
|
+
**Success Responses:**
|
|
239
|
+
```javascript
|
|
240
|
+
// GOOD: Consistent success format
|
|
241
|
+
res.status(200).json({ data: users });
|
|
242
|
+
res.status(201).json({ data: newUser });
|
|
243
|
+
res.status(204).send(); // No content
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Error Responses:**
|
|
247
|
+
```javascript
|
|
248
|
+
// GOOD: Consistent error format
|
|
249
|
+
res.status(400).json({
|
|
250
|
+
error: {
|
|
251
|
+
code: 'INVALID_INPUT',
|
|
252
|
+
message: 'Email is required',
|
|
253
|
+
details: { field: 'email', reason: 'missing' } // Optional
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Status Code Reference Table:**
|
|
259
|
+
|
|
260
|
+
| Code | Use Case | Example |
|
|
261
|
+
|------|----------|---------|
|
|
262
|
+
| 200 | Success (read/update) | `GET /users/:id` |
|
|
263
|
+
| 201 | Created | `POST /users` |
|
|
264
|
+
| 204 | Success, no content | `DELETE /users/:id` |
|
|
265
|
+
| 400 | Bad request | Invalid JSON, malformed data |
|
|
266
|
+
| 401 | Unauthorized | Missing/invalid token |
|
|
267
|
+
| 403 | Forbidden | Valid token, insufficient permissions |
|
|
268
|
+
| 404 | Not found | Resource doesn't exist |
|
|
269
|
+
| 409 | Conflict | Duplicate email, version mismatch |
|
|
270
|
+
| 422 | Validation error | Zod validation failed |
|
|
271
|
+
| 429 | Rate limit exceeded | Too many requests |
|
|
272
|
+
| 500 | Server error | Unexpected error |
|
|
273
|
+
|
|
274
|
+
### Structured Logging (Pino)
|
|
275
|
+
|
|
276
|
+
**Setup:**
|
|
277
|
+
```javascript
|
|
278
|
+
// utils/logger.js
|
|
279
|
+
const pino = require('pino');
|
|
280
|
+
|
|
281
|
+
const logger = pino({
|
|
282
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
283
|
+
formatters: {
|
|
284
|
+
level: (label) => ({ level: label })
|
|
285
|
+
},
|
|
286
|
+
redact: {
|
|
287
|
+
paths: ['password', 'token', 'authorization', '*.password', '*.token'],
|
|
288
|
+
remove: true
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
module.exports = { logger };
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
**Usage:**
|
|
296
|
+
```javascript
|
|
297
|
+
// GOOD: Structured logging with context
|
|
298
|
+
logger.info({ userId, action: 'login' }, 'User logged in');
|
|
299
|
+
logger.error({ err, requestId: req.id }, 'Database query failed');
|
|
300
|
+
|
|
301
|
+
// BAD: String concatenation, no structure
|
|
302
|
+
logger.info('User ' + userId + ' logged in');
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**NEVER Log These (CRITICAL):**
|
|
306
|
+
- Passwords (plain or hashed)
|
|
307
|
+
- API keys, tokens, secrets
|
|
308
|
+
- Credit card numbers, SSN
|
|
309
|
+
- Full request/response bodies (may contain PII)
|
|
310
|
+
|
|
311
|
+
### Environment Configuration
|
|
312
|
+
|
|
313
|
+
**Fail-Fast Validation:**
|
|
314
|
+
```javascript
|
|
315
|
+
// config/env.js
|
|
316
|
+
const { z } = require('zod');
|
|
317
|
+
require('dotenv').config();
|
|
318
|
+
|
|
319
|
+
const envSchema = z.object({
|
|
320
|
+
NODE_ENV: z.enum(['development', 'production', 'test']),
|
|
321
|
+
PORT: z.string().transform(Number).pipe(z.number().min(1).max(65535)),
|
|
322
|
+
DATABASE_URL: z.string().url(),
|
|
323
|
+
JWT_SECRET: z.string().min(32),
|
|
324
|
+
REDIS_URL: z.string().url(),
|
|
325
|
+
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info')
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Validate on startup - crash immediately if invalid
|
|
329
|
+
const env = envSchema.parse(process.env);
|
|
330
|
+
|
|
331
|
+
module.exports = env;
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**NEVER:**
|
|
335
|
+
- Hardcode secrets in source code
|
|
336
|
+
- Commit .env files to git
|
|
337
|
+
- Use default secrets in production
|
|
338
|
+
- Access `process.env` directly (use validated config)
|
|
339
|
+
|
|
340
|
+
### Graceful Shutdown
|
|
341
|
+
|
|
342
|
+
**CRITICAL: Drain connections before exit**
|
|
343
|
+
```javascript
|
|
344
|
+
// server.js
|
|
345
|
+
const gracefulShutdown = async (signal) => {
|
|
346
|
+
logger.info({ signal }, 'Shutdown signal received');
|
|
347
|
+
|
|
348
|
+
// Stop accepting new connections
|
|
349
|
+
server.close(async () => {
|
|
350
|
+
logger.info('HTTP server closed');
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
// Close database connections
|
|
354
|
+
await db.close();
|
|
355
|
+
logger.info('Database connection closed');
|
|
356
|
+
|
|
357
|
+
// Close Redis connections
|
|
358
|
+
await redis.quit();
|
|
359
|
+
logger.info('Redis connection closed');
|
|
360
|
+
|
|
361
|
+
process.exit(0);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
logger.error({ err }, 'Error during shutdown');
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Force shutdown after 30s
|
|
369
|
+
setTimeout(() => {
|
|
370
|
+
logger.error('Forced shutdown after timeout');
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}, 30000);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
376
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
377
|
+
|
|
378
|
+
// CRITICAL: Handle unhandled rejections
|
|
379
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
380
|
+
logger.error({ reason, promise }, 'Unhandled Promise Rejection');
|
|
381
|
+
process.exit(1);
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Performance Patterns
|
|
386
|
+
|
|
387
|
+
**Connection Pooling (PostgreSQL):**
|
|
388
|
+
```javascript
|
|
389
|
+
// config/database.js
|
|
390
|
+
const { Pool } = require('pg');
|
|
391
|
+
|
|
392
|
+
const pool = new Pool({
|
|
393
|
+
connectionString: env.DATABASE_URL,
|
|
394
|
+
max: 20, // Max connections in pool
|
|
395
|
+
idleTimeoutMillis: 30000,
|
|
396
|
+
connectionTimeoutMillis: 2000
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// GOOD: Use pool, not new Client() for each query
|
|
400
|
+
const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Compression Middleware:**
|
|
404
|
+
```javascript
|
|
405
|
+
const compression = require('compression');
|
|
406
|
+
|
|
407
|
+
app.use(compression({
|
|
408
|
+
filter: (req, res) => {
|
|
409
|
+
if (req.headers['x-no-compression']) return false;
|
|
410
|
+
return compression.filter(req, res);
|
|
411
|
+
},
|
|
412
|
+
level: 6 // Balance between speed and ratio
|
|
413
|
+
}));
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Streaming Large Payloads:**
|
|
417
|
+
```javascript
|
|
418
|
+
// GOOD: Stream large responses
|
|
419
|
+
router.get('/export', asyncHandler(async (req, res) => {
|
|
420
|
+
res.setHeader('Content-Type', 'application/json');
|
|
421
|
+
res.setHeader('Content-Disposition', 'attachment; filename="export.json"');
|
|
422
|
+
|
|
423
|
+
const stream = await dataService.streamExport();
|
|
424
|
+
stream.pipe(res);
|
|
425
|
+
}));
|
|
426
|
+
|
|
427
|
+
// BAD: Load everything into memory
|
|
428
|
+
const allData = await dataService.getAll(); // OOM risk
|
|
429
|
+
res.json(allData);
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Controller-Service-Repository Pattern
|
|
433
|
+
|
|
434
|
+
**Controller (THIN - only HTTP concerns):**
|
|
435
|
+
```javascript
|
|
436
|
+
// controllers/user.controller.js
|
|
437
|
+
const userService = require('../services/user.service');
|
|
438
|
+
const asyncHandler = require('../utils/asyncHandler');
|
|
439
|
+
|
|
440
|
+
exports.createUser = asyncHandler(async (req, res) => {
|
|
441
|
+
const user = await userService.createUser(req.body);
|
|
442
|
+
res.status(201).json({ data: user });
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
exports.getUser = asyncHandler(async (req, res) => {
|
|
446
|
+
const user = await userService.getUser(req.params.id);
|
|
447
|
+
res.json({ data: user });
|
|
448
|
+
});
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
**Service (Business Logic):**
|
|
452
|
+
```javascript
|
|
453
|
+
// services/user.service.js
|
|
454
|
+
const userRepository = require('../repositories/user.repository');
|
|
455
|
+
const AppError = require('../utils/AppError');
|
|
456
|
+
const { hashPassword } = require('../utils/password');
|
|
457
|
+
|
|
458
|
+
exports.createUser = async (data) => {
|
|
459
|
+
const exists = await userRepository.findByEmail(data.email);
|
|
460
|
+
if (exists) {
|
|
461
|
+
throw new AppError('Email already in use', 409, 'EMAIL_CONFLICT');
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const hashedPassword = await hashPassword(data.password);
|
|
465
|
+
return userRepository.create({ ...data, password: hashedPassword });
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
exports.getUser = async (id) => {
|
|
469
|
+
const user = await userRepository.findById(id);
|
|
470
|
+
if (!user) {
|
|
471
|
+
throw new AppError('User not found', 404, 'USER_NOT_FOUND');
|
|
472
|
+
}
|
|
473
|
+
return user;
|
|
474
|
+
};
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
**Repository (Data Access):**
|
|
478
|
+
```javascript
|
|
479
|
+
// repositories/user.repository.js
|
|
480
|
+
const pool = require('../config/database');
|
|
481
|
+
|
|
482
|
+
exports.create = async (data) => {
|
|
483
|
+
const result = await pool.query(
|
|
484
|
+
'INSERT INTO users (email, password, name) VALUES ($1, $2, $3) RETURNING *',
|
|
485
|
+
[data.email, data.password, data.name]
|
|
486
|
+
);
|
|
487
|
+
return result.rows[0];
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
exports.findById = async (id) => {
|
|
491
|
+
const result = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
|
|
492
|
+
return result.rows[0];
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
exports.findByEmail = async (email) => {
|
|
496
|
+
const result = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
|
|
497
|
+
return result.rows[0];
|
|
498
|
+
};
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
## Conventions
|
|
502
|
+
|
|
503
|
+
### File Naming
|
|
504
|
+
- **Files:** `camelCase.js` (userController.js, authMiddleware.js)
|
|
505
|
+
- **Classes:** `PascalCase` (AppError, UserService)
|
|
506
|
+
- **Constants:** `UPPER_SNAKE_CASE` (MAX_LOGIN_ATTEMPTS)
|
|
507
|
+
|
|
508
|
+
### Database
|
|
509
|
+
- **Columns:** `snake_case` (user_id, created_at)
|
|
510
|
+
- **Tables:** `plural` (users, orders, audit_logs)
|
|
511
|
+
- **Foreign keys:** `{table}_id` (user_id, order_id)
|
|
512
|
+
|
|
513
|
+
### Variable Naming
|
|
514
|
+
- **JavaScript:** `camelCase` (userId, requestData)
|
|
515
|
+
- **SQL params:** `$1, $2` (PostgreSQL) or `?` (MySQL)
|
|
516
|
+
|
|
517
|
+
### Boolean Handling
|
|
518
|
+
- **Database:** `BOOLEAN` type (true/false)
|
|
519
|
+
- **JavaScript:** `true/false` (not 1/0)
|
|
520
|
+
- **Never:** String booleans ("true", "false")
|
|
521
|
+
|
|
522
|
+
### Date Handling
|
|
523
|
+
- **Storage:** UTC timestamps (`TIMESTAMP WITH TIME ZONE`)
|
|
524
|
+
- **API:** ISO 8601 strings (`2026-02-15T10:30:00Z`)
|
|
525
|
+
- **NEVER:** Unix timestamps in APIs (use ISO strings)
|
|
526
|
+
|
|
527
|
+
### Audit Logging
|
|
528
|
+
- **Action names:** lowercase verbs (`created`, `updated`, `deleted`)
|
|
529
|
+
- **Include:** userId, action, resourceType, resourceId, timestamp, changes (optional)
|
|
530
|
+
|
|
531
|
+
## Common Patterns
|
|
532
|
+
|
|
533
|
+
### 1. Paginated API Response
|
|
534
|
+
|
|
535
|
+
```javascript
|
|
536
|
+
// GOOD: Consistent pagination
|
|
537
|
+
router.get('/users', asyncHandler(async (req, res) => {
|
|
538
|
+
const page = parseInt(req.query.page) || 1;
|
|
539
|
+
const limit = parseInt(req.query.limit) || 20;
|
|
540
|
+
const offset = (page - 1) * limit;
|
|
541
|
+
|
|
542
|
+
const { rows: users, count } = await userRepository.findAll({ limit, offset });
|
|
543
|
+
|
|
544
|
+
res.json({
|
|
545
|
+
data: users,
|
|
546
|
+
pagination: {
|
|
547
|
+
page,
|
|
548
|
+
limit,
|
|
549
|
+
total: count,
|
|
550
|
+
totalPages: Math.ceil(count / limit)
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}));
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### 2. Transaction Wrapper
|
|
557
|
+
|
|
558
|
+
```javascript
|
|
559
|
+
// utils/transaction.js
|
|
560
|
+
const pool = require('../config/database');
|
|
561
|
+
|
|
562
|
+
const withTransaction = async (callback) => {
|
|
563
|
+
const client = await pool.connect();
|
|
564
|
+
try {
|
|
565
|
+
await client.query('BEGIN');
|
|
566
|
+
const result = await callback(client);
|
|
567
|
+
await client.query('COMMIT');
|
|
568
|
+
return result;
|
|
569
|
+
} catch (error) {
|
|
570
|
+
await client.query('ROLLBACK');
|
|
571
|
+
throw error;
|
|
572
|
+
} finally {
|
|
573
|
+
client.release();
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// Usage in service:
|
|
578
|
+
exports.transferFunds = async (fromId, toId, amount) => {
|
|
579
|
+
return withTransaction(async (client) => {
|
|
580
|
+
await client.query('UPDATE accounts SET balance = balance - $1 WHERE id = $2', [amount, fromId]);
|
|
581
|
+
await client.query('UPDATE accounts SET balance = balance + $1 WHERE id = $2', [amount, toId]);
|
|
582
|
+
return { success: true };
|
|
583
|
+
});
|
|
584
|
+
};
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### 3. Dependency Injection for Testing
|
|
588
|
+
|
|
589
|
+
```javascript
|
|
590
|
+
// GOOD: Inject dependencies
|
|
591
|
+
class UserService {
|
|
592
|
+
constructor(userRepository, emailService) {
|
|
593
|
+
this.userRepository = userRepository;
|
|
594
|
+
this.emailService = emailService;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async createUser(data) {
|
|
598
|
+
const user = await this.userRepository.create(data);
|
|
599
|
+
await this.emailService.sendWelcome(user.email);
|
|
600
|
+
return user;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Easy to mock in tests:
|
|
605
|
+
const mockRepo = { create: jest.fn() };
|
|
606
|
+
const mockEmail = { sendWelcome: jest.fn() };
|
|
607
|
+
const service = new UserService(mockRepo, mockEmail);
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### 4. Request Context Logger
|
|
611
|
+
|
|
612
|
+
```javascript
|
|
613
|
+
// middleware/requestLogger.js
|
|
614
|
+
const { logger } = require('../utils/logger');
|
|
615
|
+
|
|
616
|
+
const requestLogger = (req, res, next) => {
|
|
617
|
+
const start = Date.now();
|
|
618
|
+
|
|
619
|
+
// Create child logger with request context
|
|
620
|
+
req.log = logger.child({ requestId: req.id });
|
|
621
|
+
|
|
622
|
+
res.on('finish', () => {
|
|
623
|
+
req.log.info({
|
|
624
|
+
method: req.method,
|
|
625
|
+
path: req.path,
|
|
626
|
+
statusCode: res.statusCode,
|
|
627
|
+
duration: Date.now() - start,
|
|
628
|
+
userId: req.user?.id
|
|
629
|
+
}, 'Request completed');
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
next();
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
// Usage in controllers:
|
|
636
|
+
req.log.info({ orderId }, 'Processing order');
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
### 5. Feature Flags
|
|
640
|
+
|
|
641
|
+
```javascript
|
|
642
|
+
// utils/features.js
|
|
643
|
+
const features = {
|
|
644
|
+
newCheckout: process.env.FEATURE_NEW_CHECKOUT === 'true',
|
|
645
|
+
betaSearch: process.env.FEATURE_BETA_SEARCH === 'true'
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const isEnabled = (feature) => features[feature] || false;
|
|
649
|
+
|
|
650
|
+
// Usage:
|
|
651
|
+
if (isEnabled('newCheckout')) {
|
|
652
|
+
return newCheckoutService.process(order);
|
|
653
|
+
}
|
|
654
|
+
return legacyCheckoutService.process(order);
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
## Anti-Patterns
|
|
658
|
+
|
|
659
|
+
### 1. Callback Hell
|
|
660
|
+
|
|
661
|
+
**BAD:**
|
|
662
|
+
```javascript
|
|
663
|
+
getUserById(id, (err, user) => {
|
|
664
|
+
if (err) return handleError(err);
|
|
665
|
+
getOrders(user.id, (err, orders) => {
|
|
666
|
+
if (err) return handleError(err);
|
|
667
|
+
processOrders(orders, (err, result) => {
|
|
668
|
+
if (err) return handleError(err);
|
|
669
|
+
sendEmail(result, (err) => {
|
|
670
|
+
if (err) return handleError(err);
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
**GOOD:**
|
|
678
|
+
```javascript
|
|
679
|
+
const user = await userService.getById(id);
|
|
680
|
+
const orders = await orderService.getByUserId(user.id);
|
|
681
|
+
const result = await orderService.process(orders);
|
|
682
|
+
await emailService.send(result);
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### 2. Fat Controllers
|
|
686
|
+
|
|
687
|
+
**BAD:**
|
|
688
|
+
```javascript
|
|
689
|
+
// Controller doing business logic AND data access
|
|
690
|
+
exports.createUser = asyncHandler(async (req, res) => {
|
|
691
|
+
const exists = await pool.query('SELECT * FROM users WHERE email = $1', [req.body.email]);
|
|
692
|
+
if (exists.rows.length > 0) {
|
|
693
|
+
throw new AppError('Email exists', 409);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const hashed = await bcrypt.hash(req.body.password, 10);
|
|
697
|
+
const result = await pool.query(
|
|
698
|
+
'INSERT INTO users (email, password) VALUES ($1, $2) RETURNING *',
|
|
699
|
+
[req.body.email, hashed]
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
await sendEmail(result.rows[0].email, 'Welcome!');
|
|
703
|
+
res.status(201).json({ data: result.rows[0] });
|
|
704
|
+
});
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
**GOOD:**
|
|
708
|
+
```javascript
|
|
709
|
+
// Controller delegates to service
|
|
710
|
+
exports.createUser = asyncHandler(async (req, res) => {
|
|
711
|
+
const user = await userService.createUser(req.body);
|
|
712
|
+
res.status(201).json({ data: user });
|
|
713
|
+
});
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### 3. Ignoring Errors
|
|
717
|
+
|
|
718
|
+
**BAD:**
|
|
719
|
+
```javascript
|
|
720
|
+
// Swallowing errors
|
|
721
|
+
try {
|
|
722
|
+
await riskyOperation();
|
|
723
|
+
} catch (err) {
|
|
724
|
+
// Silent failure - user never knows something broke
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Or worse: empty catch
|
|
728
|
+
doSomething().catch(() => {});
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
**GOOD:**
|
|
732
|
+
```javascript
|
|
733
|
+
// Proper error handling
|
|
734
|
+
try {
|
|
735
|
+
await riskyOperation();
|
|
736
|
+
} catch (err) {
|
|
737
|
+
logger.error({ err }, 'Risky operation failed');
|
|
738
|
+
throw new AppError('Operation failed', 500, 'OPERATION_ERROR');
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
### 4. Blocking the Event Loop
|
|
743
|
+
|
|
744
|
+
**BAD:**
|
|
745
|
+
```javascript
|
|
746
|
+
// Synchronous crypto operations block the event loop
|
|
747
|
+
const bcrypt = require('bcrypt');
|
|
748
|
+
const hashed = bcrypt.hashSync(password, 10); // BLOCKS for ~100ms
|
|
749
|
+
|
|
750
|
+
// Heavy computation in request handler
|
|
751
|
+
router.get('/report', (req, res) => {
|
|
752
|
+
const data = processMillionsOfRecords(); // Blocks all other requests
|
|
753
|
+
res.json(data);
|
|
754
|
+
});
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
**GOOD:**
|
|
758
|
+
```javascript
|
|
759
|
+
// Use async versions
|
|
760
|
+
const hashed = await bcrypt.hash(password, 10);
|
|
761
|
+
|
|
762
|
+
// Offload heavy work to worker threads or queues
|
|
763
|
+
const { Worker } = require('worker_threads');
|
|
764
|
+
router.get('/report', asyncHandler(async (req, res) => {
|
|
765
|
+
const jobId = await jobQueue.add('generateReport', req.query);
|
|
766
|
+
res.status(202).json({ data: { jobId, status: 'processing' } });
|
|
767
|
+
}));
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
### 5. SQL Injection
|
|
771
|
+
|
|
772
|
+
**BAD:**
|
|
773
|
+
```javascript
|
|
774
|
+
// NEVER concatenate user input into SQL
|
|
775
|
+
const userId = req.params.id;
|
|
776
|
+
const query = `SELECT * FROM users WHERE id = '${userId}'`; // VULNERABLE
|
|
777
|
+
const result = await pool.query(query);
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
**GOOD:**
|
|
781
|
+
```javascript
|
|
782
|
+
// ALWAYS use parameterized queries
|
|
783
|
+
const userId = req.params.id;
|
|
784
|
+
const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
## Integration Notes
|
|
788
|
+
|
|
789
|
+
### Team Coordination
|
|
790
|
+
|
|
791
|
+
When working as part of a multi-session team:
|
|
792
|
+
|
|
793
|
+
1. **Startup:** Call `team_check_inbox()` FIRST to get messages/artifacts from other workers
|
|
794
|
+
2. **Consume Conventions:** Read `shared-conventions` artifact to align on response format, status codes, naming
|
|
795
|
+
3. **Consume Database Schema:** Read schema artifact from database worker to match table/column names exactly
|
|
796
|
+
4. **Publish API Contract:** After implementing routes, publish artifact with:
|
|
797
|
+
```json
|
|
798
|
+
{
|
|
799
|
+
"routes": [
|
|
800
|
+
{ "method": "POST", "path": "/api/users", "auth": true, "body": "CreateUserSchema" },
|
|
801
|
+
{ "method": "GET", "path": "/api/users/:id", "auth": true, "response": "UserSchema" }
|
|
802
|
+
],
|
|
803
|
+
"schemas": { "CreateUserSchema": {...}, "UserSchema": {...} },
|
|
804
|
+
"errorCodes": ["VALIDATION_ERROR", "USER_NOT_FOUND", "EMAIL_CONFLICT"]
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
5. **Coordinate with Frontend:** Frontend worker consumes your API contract artifact
|
|
808
|
+
6. **Broadcast Completion:** Use `team_broadcast()` when routes are ready for integration testing
|
|
809
|
+
7. **File Paths:** ALWAYS use relative paths (e.g., `src/controllers/user.js`), NEVER absolute paths
|
|
810
|
+
|
|
811
|
+
### Database Worker Coordination
|
|
812
|
+
|
|
813
|
+
**CRITICAL: Match database worker's schema exactly**
|
|
814
|
+
- Read their artifact for table names, column names, data types
|
|
815
|
+
- Use their naming conventions (e.g., `user_id` not `userId` in SQL)
|
|
816
|
+
- Follow their foreign key patterns
|
|
817
|
+
- Never create tables yourself - coordinate schema changes
|
|
818
|
+
|
|
819
|
+
### Frontend Worker Coordination
|
|
820
|
+
|
|
821
|
+
**Provide clear API contract:**
|
|
822
|
+
- Document all endpoints (method, path, auth requirement)
|
|
823
|
+
- Provide request/response schemas (use Zod schemas as source of truth)
|
|
824
|
+
- List all possible error codes
|
|
825
|
+
- Document pagination format
|
|
826
|
+
- Document authentication (Bearer token in `Authorization` header)
|
|
827
|
+
|
|
828
|
+
**Example artifact:**
|
|
829
|
+
```json
|
|
830
|
+
{
|
|
831
|
+
"baseUrl": "/api/v1",
|
|
832
|
+
"authentication": "Bearer token in Authorization header",
|
|
833
|
+
"responseFormat": {
|
|
834
|
+
"success": { "data": "<resource>" },
|
|
835
|
+
"error": { "error": { "code": "string", "message": "string" } }
|
|
836
|
+
},
|
|
837
|
+
"endpoints": [...]
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
### Testing Coordination
|
|
842
|
+
|
|
843
|
+
**For integration tests:**
|
|
844
|
+
- Seed database with test data (coordinate with database worker)
|
|
845
|
+
- Provide test fixtures (sample requests/responses)
|
|
846
|
+
- Document test user credentials
|
|
847
|
+
- Clear test data between runs
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
**Version:** 1.0.0
|
|
852
|
+
**Last Updated:** 2026-02-15
|
|
853
|
+
**Token Count:** ~1950 (under 2000 limit for Worker Context)
|