omgkit 2.2.0 → 2.3.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/package.json +1 -1
- package/plugin/skills/databases/mongodb/SKILL.md +60 -776
- package/plugin/skills/databases/prisma/SKILL.md +53 -744
- package/plugin/skills/databases/redis/SKILL.md +53 -860
- package/plugin/skills/devops/aws/SKILL.md +68 -672
- package/plugin/skills/devops/github-actions/SKILL.md +54 -657
- package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
- package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
- package/plugin/skills/frameworks/django/SKILL.md +87 -853
- package/plugin/skills/frameworks/express/SKILL.md +95 -1301
- package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
- package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
- package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
- package/plugin/skills/frameworks/react/SKILL.md +94 -962
- package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
- package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
- package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
- package/plugin/skills/frontend/responsive/SKILL.md +76 -799
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
- package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
- package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
- package/plugin/skills/languages/javascript/SKILL.md +106 -849
- package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
- package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
- package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
- package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
- package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
- package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
- package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
- package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
- package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
- package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
- package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
- package/plugin/skills/security/better-auth/SKILL.md +46 -1034
- package/plugin/skills/security/oauth/SKILL.md +80 -934
- package/plugin/skills/security/owasp/SKILL.md +78 -862
- package/plugin/skills/testing/playwright/SKILL.md +77 -700
- package/plugin/skills/testing/pytest/SKILL.md +73 -811
- package/plugin/skills/testing/vitest/SKILL.md +60 -920
- package/plugin/skills/tools/document-processing/SKILL.md +111 -838
- package/plugin/skills/tools/image-processing/SKILL.md +126 -659
- package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
- package/plugin/skills/tools/media-processing/SKILL.md +118 -735
- package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
- package/plugin/skills/SKILL_STANDARDS.md +0 -743
|
@@ -1,1381 +1,175 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: express
|
|
3
|
-
description:
|
|
4
|
-
category: frameworks
|
|
5
|
-
triggers:
|
|
6
|
-
- express
|
|
7
|
-
- expressjs
|
|
8
|
-
- express.js
|
|
9
|
-
- node api
|
|
10
|
-
- node rest api
|
|
11
|
-
- express middleware
|
|
12
|
-
- express routing
|
|
13
|
-
- node backend
|
|
2
|
+
name: building-express-apis
|
|
3
|
+
description: Builds production Express.js APIs with TypeScript, middleware patterns, authentication, and error handling. Use when creating Node.js backends, REST APIs, or Express applications.
|
|
14
4
|
---
|
|
15
5
|
|
|
16
6
|
# Express.js
|
|
17
7
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
## Purpose
|
|
21
|
-
|
|
22
|
-
Build scalable Node.js APIs with confidence:
|
|
23
|
-
|
|
24
|
-
- Design clean API architectures with proper routing
|
|
25
|
-
- Implement robust middleware patterns
|
|
26
|
-
- Handle authentication and authorization securely
|
|
27
|
-
- Validate requests with comprehensive schemas
|
|
28
|
-
- Write comprehensive tests for reliability
|
|
29
|
-
- Deploy production-ready applications
|
|
30
|
-
- Optimize performance for high traffic
|
|
31
|
-
|
|
32
|
-
## Features
|
|
33
|
-
|
|
34
|
-
### 1. TypeScript Project Setup
|
|
8
|
+
## Quick Start
|
|
35
9
|
|
|
36
10
|
```typescript
|
|
37
|
-
|
|
38
|
-
import express, { Application, Request, Response, NextFunction } from 'express';
|
|
11
|
+
import express from 'express';
|
|
39
12
|
import cors from 'cors';
|
|
40
13
|
import helmet from 'helmet';
|
|
41
|
-
import compression from 'compression';
|
|
42
|
-
import rateLimit from 'express-rate-limit';
|
|
43
|
-
import { pinoHttp } from 'pino-http';
|
|
44
|
-
import { errorHandler } from './middleware/errorHandler';
|
|
45
|
-
import { notFoundHandler } from './middleware/notFoundHandler';
|
|
46
|
-
import { apiRouter } from './routes';
|
|
47
|
-
import { config } from './config';
|
|
48
|
-
import { logger } from './utils/logger';
|
|
49
|
-
|
|
50
|
-
export function createApp(): Application {
|
|
51
|
-
const app = express();
|
|
52
|
-
|
|
53
|
-
// Security middleware
|
|
54
|
-
app.use(helmet());
|
|
55
|
-
app.use(cors({
|
|
56
|
-
origin: config.cors.origins,
|
|
57
|
-
credentials: true,
|
|
58
|
-
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
59
|
-
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
60
|
-
}));
|
|
61
|
-
|
|
62
|
-
// Rate limiting
|
|
63
|
-
app.use(rateLimit({
|
|
64
|
-
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
65
|
-
max: 100,
|
|
66
|
-
standardHeaders: true,
|
|
67
|
-
legacyHeaders: false,
|
|
68
|
-
message: { error: 'Too many requests, please try again later.' },
|
|
69
|
-
}));
|
|
70
|
-
|
|
71
|
-
// Request parsing
|
|
72
|
-
app.use(express.json({ limit: '10mb' }));
|
|
73
|
-
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
74
|
-
app.use(compression());
|
|
75
|
-
|
|
76
|
-
// Logging
|
|
77
|
-
app.use(pinoHttp({
|
|
78
|
-
logger,
|
|
79
|
-
customLogLevel: (req, res, err) => {
|
|
80
|
-
if (res.statusCode >= 500 || err) return 'error';
|
|
81
|
-
if (res.statusCode >= 400) return 'warn';
|
|
82
|
-
return 'info';
|
|
83
|
-
},
|
|
84
|
-
}));
|
|
85
14
|
|
|
86
|
-
|
|
87
|
-
app.get('/health', (req, res) => {
|
|
88
|
-
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
|
|
89
|
-
});
|
|
15
|
+
const app = express();
|
|
90
16
|
|
|
91
|
-
|
|
92
|
-
|
|
17
|
+
app.use(helmet());
|
|
18
|
+
app.use(cors());
|
|
19
|
+
app.use(express.json());
|
|
93
20
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return app;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// src/server.ts
|
|
102
|
-
import { createApp } from './app';
|
|
103
|
-
import { config } from './config';
|
|
104
|
-
import { logger } from './utils/logger';
|
|
105
|
-
import { connectDatabase } from './database';
|
|
21
|
+
app.get('/api/health', (req, res) => {
|
|
22
|
+
res.json({ status: 'ok' });
|
|
23
|
+
});
|
|
106
24
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
await connectDatabase();
|
|
110
|
-
logger.info('Database connected');
|
|
25
|
+
app.listen(3000);
|
|
26
|
+
```
|
|
111
27
|
|
|
112
|
-
|
|
113
|
-
const server = app.listen(config.port, () => {
|
|
114
|
-
logger.info(`Server running on port ${config.port}`);
|
|
115
|
-
});
|
|
28
|
+
## Features
|
|
116
29
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
30
|
+
| Feature | Description | Guide |
|
|
31
|
+
|---------|-------------|-------|
|
|
32
|
+
| Project Setup | TypeScript config, middleware stack | [SETUP.md](SETUP.md) |
|
|
33
|
+
| Routing | Controllers, validation, async handlers | [ROUTING.md](ROUTING.md) |
|
|
34
|
+
| Middleware | Auth, validation, error handling | [MIDDLEWARE.md](MIDDLEWARE.md) |
|
|
35
|
+
| Database | Prisma/TypeORM integration | [DATABASE.md](DATABASE.md) |
|
|
36
|
+
| Testing | Jest, supertest patterns | [TESTING.md](TESTING.md) |
|
|
37
|
+
| Deployment | Docker, PM2, production config | [DEPLOYMENT.md](DEPLOYMENT.md) |
|
|
124
38
|
|
|
125
|
-
|
|
126
|
-
logger.error('Forced shutdown after timeout');
|
|
127
|
-
process.exit(1);
|
|
128
|
-
}, 10000);
|
|
129
|
-
};
|
|
39
|
+
## Common Patterns
|
|
130
40
|
|
|
131
|
-
|
|
132
|
-
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
133
|
-
} catch (error) {
|
|
134
|
-
logger.error('Failed to start server', error);
|
|
135
|
-
process.exit(1);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
bootstrap();
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
### 2. Middleware Patterns
|
|
41
|
+
### Controller Pattern
|
|
143
42
|
|
|
144
43
|
```typescript
|
|
145
|
-
//
|
|
44
|
+
// controllers/users.ts
|
|
146
45
|
import { Request, Response, NextFunction } from 'express';
|
|
147
|
-
import jwt from 'jsonwebtoken';
|
|
148
|
-
import { config } from '../config';
|
|
149
|
-
import { AppError } from '../errors/AppError';
|
|
150
46
|
import { UserService } from '../services/UserService';
|
|
151
47
|
|
|
152
|
-
export
|
|
153
|
-
|
|
154
|
-
id: string;
|
|
155
|
-
email: string;
|
|
156
|
-
role: string;
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function authenticate() {
|
|
161
|
-
return async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
162
|
-
try {
|
|
163
|
-
const authHeader = req.headers.authorization;
|
|
164
|
-
if (!authHeader?.startsWith('Bearer ')) {
|
|
165
|
-
throw new AppError('No token provided', 401, 'UNAUTHORIZED');
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const token = authHeader.substring(7);
|
|
169
|
-
const decoded = jwt.verify(token, config.jwt.secret) as {
|
|
170
|
-
userId: string;
|
|
171
|
-
email: string;
|
|
172
|
-
role: string;
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
// Optionally verify user still exists
|
|
176
|
-
const user = await UserService.findById(decoded.userId);
|
|
177
|
-
if (!user) {
|
|
178
|
-
throw new AppError('User not found', 401, 'UNAUTHORIZED');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
req.user = {
|
|
182
|
-
id: decoded.userId,
|
|
183
|
-
email: decoded.email,
|
|
184
|
-
role: decoded.role,
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
next();
|
|
188
|
-
} catch (error) {
|
|
189
|
-
if (error instanceof jwt.JsonWebTokenError) {
|
|
190
|
-
next(new AppError('Invalid token', 401, 'INVALID_TOKEN'));
|
|
191
|
-
} else if (error instanceof jwt.TokenExpiredError) {
|
|
192
|
-
next(new AppError('Token expired', 401, 'TOKEN_EXPIRED'));
|
|
193
|
-
} else {
|
|
194
|
-
next(error);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function authorize(...roles: string[]) {
|
|
201
|
-
return (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
202
|
-
if (!req.user) {
|
|
203
|
-
return next(new AppError('Authentication required', 401, 'UNAUTHORIZED'));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (!roles.includes(req.user.role)) {
|
|
207
|
-
return next(new AppError('Insufficient permissions', 403, 'FORBIDDEN'));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
next();
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// src/middleware/validate.ts
|
|
215
|
-
import { Request, Response, NextFunction } from 'express';
|
|
216
|
-
import { ZodSchema, ZodError } from 'zod';
|
|
217
|
-
import { AppError } from '../errors/AppError';
|
|
218
|
-
|
|
219
|
-
interface ValidationSchemas {
|
|
220
|
-
body?: ZodSchema;
|
|
221
|
-
query?: ZodSchema;
|
|
222
|
-
params?: ZodSchema;
|
|
223
|
-
}
|
|
48
|
+
export class UserController {
|
|
49
|
+
constructor(private userService: UserService) {}
|
|
224
50
|
|
|
225
|
-
|
|
226
|
-
return async (req: Request, res: Response, next: NextFunction) => {
|
|
51
|
+
getAll = async (req: Request, res: Response, next: NextFunction) => {
|
|
227
52
|
try {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
if (schemas.query) {
|
|
232
|
-
req.query = await schemas.query.parseAsync(req.query);
|
|
233
|
-
}
|
|
234
|
-
if (schemas.params) {
|
|
235
|
-
req.params = await schemas.params.parseAsync(req.params);
|
|
236
|
-
}
|
|
237
|
-
next();
|
|
53
|
+
const users = await this.userService.findAll();
|
|
54
|
+
res.json(users);
|
|
238
55
|
} catch (error) {
|
|
239
|
-
|
|
240
|
-
const details = error.errors.map(err => ({
|
|
241
|
-
path: err.path.join('.'),
|
|
242
|
-
message: err.message,
|
|
243
|
-
}));
|
|
244
|
-
next(new AppError('Validation failed', 400, 'VALIDATION_ERROR', details));
|
|
245
|
-
} else {
|
|
246
|
-
next(error);
|
|
247
|
-
}
|
|
56
|
+
next(error);
|
|
248
57
|
}
|
|
249
58
|
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// src/middleware/requestId.ts
|
|
253
|
-
import { Request, Response, NextFunction } from 'express';
|
|
254
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
255
|
-
|
|
256
|
-
export interface RequestWithId extends Request {
|
|
257
|
-
id: string;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export function requestId() {
|
|
261
|
-
return (req: RequestWithId, res: Response, next: NextFunction) => {
|
|
262
|
-
req.id = (req.headers['x-request-id'] as string) || uuidv4();
|
|
263
|
-
res.setHeader('X-Request-ID', req.id);
|
|
264
|
-
next();
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// src/middleware/cache.ts
|
|
269
|
-
import { Request, Response, NextFunction } from 'express';
|
|
270
|
-
import { redis } from '../database/redis';
|
|
271
|
-
|
|
272
|
-
interface CacheOptions {
|
|
273
|
-
ttl?: number;
|
|
274
|
-
keyGenerator?: (req: Request) => string;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export function cache(options: CacheOptions = {}) {
|
|
278
|
-
const { ttl = 300, keyGenerator } = options;
|
|
279
|
-
|
|
280
|
-
return async (req: Request, res: Response, next: NextFunction) => {
|
|
281
|
-
if (req.method !== 'GET') {
|
|
282
|
-
return next();
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const key = keyGenerator?.(req) || `cache:${req.originalUrl}`;
|
|
286
59
|
|
|
60
|
+
getById = async (req: Request, res: Response, next: NextFunction) => {
|
|
287
61
|
try {
|
|
288
|
-
const
|
|
289
|
-
if (
|
|
290
|
-
|
|
291
|
-
return res.json(JSON.parse(cached));
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Override res.json to cache the response
|
|
295
|
-
const originalJson = res.json.bind(res);
|
|
296
|
-
res.json = (body: unknown) => {
|
|
297
|
-
redis.setex(key, ttl, JSON.stringify(body));
|
|
298
|
-
res.setHeader('X-Cache', 'MISS');
|
|
299
|
-
return originalJson(body);
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
next();
|
|
62
|
+
const user = await this.userService.findById(req.params.id);
|
|
63
|
+
if (!user) return res.status(404).json({ error: 'Not found' });
|
|
64
|
+
res.json(user);
|
|
303
65
|
} catch (error) {
|
|
304
|
-
|
|
305
|
-
next();
|
|
66
|
+
next(error);
|
|
306
67
|
}
|
|
307
68
|
};
|
|
308
69
|
}
|
|
309
70
|
```
|
|
310
71
|
|
|
311
|
-
###
|
|
72
|
+
### Error Handler
|
|
312
73
|
|
|
313
74
|
```typescript
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
public readonly statusCode: number;
|
|
317
|
-
public readonly code: string;
|
|
318
|
-
public readonly isOperational: boolean;
|
|
319
|
-
public readonly details?: unknown;
|
|
75
|
+
// middleware/errorHandler.ts
|
|
76
|
+
import { Request, Response, NextFunction } from 'express';
|
|
320
77
|
|
|
78
|
+
export class AppError extends Error {
|
|
321
79
|
constructor(
|
|
80
|
+
public statusCode: number,
|
|
322
81
|
message: string,
|
|
323
|
-
|
|
324
|
-
code: string = 'INTERNAL_ERROR',
|
|
325
|
-
details?: unknown,
|
|
326
|
-
isOperational: boolean = true
|
|
82
|
+
public isOperational = true
|
|
327
83
|
) {
|
|
328
84
|
super(message);
|
|
329
|
-
this.statusCode = statusCode;
|
|
330
|
-
this.code = code;
|
|
331
|
-
this.isOperational = isOperational;
|
|
332
|
-
this.details = details;
|
|
333
|
-
|
|
334
|
-
Error.captureStackTrace(this, this.constructor);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export class NotFoundError extends AppError {
|
|
339
|
-
constructor(resource: string, id?: string) {
|
|
340
|
-
super(
|
|
341
|
-
id ? `${resource} with id ${id} not found` : `${resource} not found`,
|
|
342
|
-
404,
|
|
343
|
-
'NOT_FOUND'
|
|
344
|
-
);
|
|
345
85
|
}
|
|
346
86
|
}
|
|
347
87
|
|
|
348
|
-
export class ValidationError extends AppError {
|
|
349
|
-
constructor(message: string, details?: unknown) {
|
|
350
|
-
super(message, 400, 'VALIDATION_ERROR', details);
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
export class UnauthorizedError extends AppError {
|
|
355
|
-
constructor(message: string = 'Unauthorized') {
|
|
356
|
-
super(message, 401, 'UNAUTHORIZED');
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
export class ForbiddenError extends AppError {
|
|
361
|
-
constructor(message: string = 'Forbidden') {
|
|
362
|
-
super(message, 403, 'FORBIDDEN');
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
export class ConflictError extends AppError {
|
|
367
|
-
constructor(message: string) {
|
|
368
|
-
super(message, 409, 'CONFLICT');
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// src/middleware/errorHandler.ts
|
|
373
|
-
import { Request, Response, NextFunction } from 'express';
|
|
374
|
-
import { AppError } from '../errors/AppError';
|
|
375
|
-
import { logger } from '../utils/logger';
|
|
376
|
-
import { config } from '../config';
|
|
377
|
-
|
|
378
88
|
export function errorHandler(
|
|
379
89
|
err: Error,
|
|
380
90
|
req: Request,
|
|
381
91
|
res: Response,
|
|
382
92
|
next: NextFunction
|
|
383
93
|
) {
|
|
384
|
-
// Log error
|
|
385
|
-
logger.error({
|
|
386
|
-
error: err.message,
|
|
387
|
-
stack: err.stack,
|
|
388
|
-
path: req.path,
|
|
389
|
-
method: req.method,
|
|
390
|
-
body: req.body,
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
// Handle known operational errors
|
|
394
94
|
if (err instanceof AppError) {
|
|
395
|
-
return res.status(err.statusCode).json({
|
|
396
|
-
error: {
|
|
397
|
-
code: err.code,
|
|
398
|
-
message: err.message,
|
|
399
|
-
details: err.details,
|
|
400
|
-
},
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Handle Prisma errors
|
|
405
|
-
if (err.constructor.name === 'PrismaClientKnownRequestError') {
|
|
406
|
-
const prismaError = err as { code: string; meta?: { target?: string[] } };
|
|
407
|
-
if (prismaError.code === 'P2002') {
|
|
408
|
-
const field = prismaError.meta?.target?.[0] || 'field';
|
|
409
|
-
return res.status(409).json({
|
|
410
|
-
error: {
|
|
411
|
-
code: 'CONFLICT',
|
|
412
|
-
message: `A record with this ${field} already exists`,
|
|
413
|
-
},
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
if (prismaError.code === 'P2025') {
|
|
417
|
-
return res.status(404).json({
|
|
418
|
-
error: {
|
|
419
|
-
code: 'NOT_FOUND',
|
|
420
|
-
message: 'Record not found',
|
|
421
|
-
},
|
|
422
|
-
});
|
|
423
|
-
}
|
|
95
|
+
return res.status(err.statusCode).json({ error: err.message });
|
|
424
96
|
}
|
|
425
97
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
? 'An unexpected error occurred'
|
|
429
|
-
: err.message;
|
|
430
|
-
|
|
431
|
-
res.status(500).json({
|
|
432
|
-
error: {
|
|
433
|
-
code: 'INTERNAL_ERROR',
|
|
434
|
-
message,
|
|
435
|
-
...(config.isDevelopment && { stack: err.stack }),
|
|
436
|
-
},
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// src/middleware/notFoundHandler.ts
|
|
441
|
-
import { Request, Response } from 'express';
|
|
442
|
-
|
|
443
|
-
export function notFoundHandler(req: Request, res: Response) {
|
|
444
|
-
res.status(404).json({
|
|
445
|
-
error: {
|
|
446
|
-
code: 'NOT_FOUND',
|
|
447
|
-
message: `Route ${req.method} ${req.path} not found`,
|
|
448
|
-
},
|
|
449
|
-
});
|
|
98
|
+
console.error(err);
|
|
99
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
450
100
|
}
|
|
451
101
|
```
|
|
452
102
|
|
|
453
|
-
###
|
|
103
|
+
### Validation Middleware
|
|
454
104
|
|
|
455
105
|
```typescript
|
|
456
|
-
//
|
|
457
|
-
import {
|
|
458
|
-
import {
|
|
459
|
-
import { authRouter } from './auth';
|
|
460
|
-
import { projectRouter } from './projects';
|
|
461
|
-
import { organizationRouter } from './organizations';
|
|
462
|
-
|
|
463
|
-
export const apiRouter = Router();
|
|
464
|
-
|
|
465
|
-
apiRouter.use('/auth', authRouter);
|
|
466
|
-
apiRouter.use('/users', userRouter);
|
|
467
|
-
apiRouter.use('/organizations', organizationRouter);
|
|
468
|
-
apiRouter.use('/projects', projectRouter);
|
|
469
|
-
|
|
470
|
-
// src/routes/users.ts
|
|
471
|
-
import { Router } from 'express';
|
|
472
|
-
import { UserController } from '../controllers/UserController';
|
|
473
|
-
import { authenticate, authorize } from '../middleware/auth';
|
|
474
|
-
import { validate } from '../middleware/validate';
|
|
475
|
-
import { userSchemas } from '../schemas/userSchemas';
|
|
476
|
-
|
|
477
|
-
export const userRouter = Router();
|
|
478
|
-
const controller = new UserController();
|
|
479
|
-
|
|
480
|
-
userRouter.get(
|
|
481
|
-
'/',
|
|
482
|
-
authenticate(),
|
|
483
|
-
authorize('admin'),
|
|
484
|
-
validate({ query: userSchemas.list }),
|
|
485
|
-
controller.list
|
|
486
|
-
);
|
|
487
|
-
|
|
488
|
-
userRouter.get(
|
|
489
|
-
'/me',
|
|
490
|
-
authenticate(),
|
|
491
|
-
controller.getCurrentUser
|
|
492
|
-
);
|
|
493
|
-
|
|
494
|
-
userRouter.patch(
|
|
495
|
-
'/me',
|
|
496
|
-
authenticate(),
|
|
497
|
-
validate({ body: userSchemas.updateProfile }),
|
|
498
|
-
controller.updateProfile
|
|
499
|
-
);
|
|
500
|
-
|
|
501
|
-
userRouter.get(
|
|
502
|
-
'/:id',
|
|
503
|
-
authenticate(),
|
|
504
|
-
validate({ params: userSchemas.params }),
|
|
505
|
-
controller.getById
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
userRouter.post(
|
|
509
|
-
'/',
|
|
510
|
-
authenticate(),
|
|
511
|
-
authorize('admin'),
|
|
512
|
-
validate({ body: userSchemas.create }),
|
|
513
|
-
controller.create
|
|
514
|
-
);
|
|
515
|
-
|
|
516
|
-
userRouter.patch(
|
|
517
|
-
'/:id',
|
|
518
|
-
authenticate(),
|
|
519
|
-
authorize('admin'),
|
|
520
|
-
validate({ params: userSchemas.params, body: userSchemas.update }),
|
|
521
|
-
controller.update
|
|
522
|
-
);
|
|
523
|
-
|
|
524
|
-
userRouter.delete(
|
|
525
|
-
'/:id',
|
|
526
|
-
authenticate(),
|
|
527
|
-
authorize('admin'),
|
|
528
|
-
validate({ params: userSchemas.params }),
|
|
529
|
-
controller.delete
|
|
530
|
-
);
|
|
531
|
-
|
|
532
|
-
// src/routes/projects.ts
|
|
533
|
-
import { Router } from 'express';
|
|
534
|
-
import { ProjectController } from '../controllers/ProjectController';
|
|
535
|
-
import { authenticate } from '../middleware/auth';
|
|
536
|
-
import { validate } from '../middleware/validate';
|
|
537
|
-
import { projectSchemas } from '../schemas/projectSchemas';
|
|
538
|
-
import { cache } from '../middleware/cache';
|
|
539
|
-
|
|
540
|
-
export const projectRouter = Router();
|
|
541
|
-
const controller = new ProjectController();
|
|
542
|
-
|
|
543
|
-
projectRouter.use(authenticate());
|
|
544
|
-
|
|
545
|
-
projectRouter.get(
|
|
546
|
-
'/',
|
|
547
|
-
validate({ query: projectSchemas.list }),
|
|
548
|
-
cache({ ttl: 60 }),
|
|
549
|
-
controller.list
|
|
550
|
-
);
|
|
551
|
-
|
|
552
|
-
projectRouter.get(
|
|
553
|
-
'/:id',
|
|
554
|
-
validate({ params: projectSchemas.params }),
|
|
555
|
-
cache({ ttl: 300 }),
|
|
556
|
-
controller.getById
|
|
557
|
-
);
|
|
558
|
-
|
|
559
|
-
projectRouter.post(
|
|
560
|
-
'/',
|
|
561
|
-
validate({ body: projectSchemas.create }),
|
|
562
|
-
controller.create
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
projectRouter.patch(
|
|
566
|
-
'/:id',
|
|
567
|
-
validate({ params: projectSchemas.params, body: projectSchemas.update }),
|
|
568
|
-
controller.update
|
|
569
|
-
);
|
|
570
|
-
|
|
571
|
-
projectRouter.delete(
|
|
572
|
-
'/:id',
|
|
573
|
-
validate({ params: projectSchemas.params }),
|
|
574
|
-
controller.delete
|
|
575
|
-
);
|
|
576
|
-
|
|
577
|
-
// Nested routes for project tasks
|
|
578
|
-
projectRouter.get(
|
|
579
|
-
'/:id/tasks',
|
|
580
|
-
validate({ params: projectSchemas.params, query: projectSchemas.taskList }),
|
|
581
|
-
controller.getTasks
|
|
582
|
-
);
|
|
583
|
-
|
|
584
|
-
projectRouter.post(
|
|
585
|
-
'/:id/tasks',
|
|
586
|
-
validate({ params: projectSchemas.params, body: projectSchemas.createTask }),
|
|
587
|
-
controller.createTask
|
|
588
|
-
);
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
### 5. Controllers and Services
|
|
592
|
-
|
|
593
|
-
```typescript
|
|
594
|
-
// src/controllers/UserController.ts
|
|
595
|
-
import { Response, NextFunction } from 'express';
|
|
596
|
-
import { AuthRequest } from '../middleware/auth';
|
|
597
|
-
import { UserService } from '../services/UserService';
|
|
598
|
-
import { NotFoundError } from '../errors/AppError';
|
|
599
|
-
|
|
600
|
-
export class UserController {
|
|
601
|
-
private userService = new UserService();
|
|
602
|
-
|
|
603
|
-
list = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
604
|
-
try {
|
|
605
|
-
const { page = 1, limit = 20, search, role } = req.query as {
|
|
606
|
-
page?: number;
|
|
607
|
-
limit?: number;
|
|
608
|
-
search?: string;
|
|
609
|
-
role?: string;
|
|
610
|
-
};
|
|
611
|
-
|
|
612
|
-
const result = await this.userService.findAll({
|
|
613
|
-
page: Number(page),
|
|
614
|
-
limit: Math.min(Number(limit), 100),
|
|
615
|
-
search,
|
|
616
|
-
role,
|
|
617
|
-
});
|
|
618
|
-
|
|
619
|
-
res.json(result);
|
|
620
|
-
} catch (error) {
|
|
621
|
-
next(error);
|
|
622
|
-
}
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
getCurrentUser = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
626
|
-
try {
|
|
627
|
-
const user = await this.userService.findById(req.user!.id);
|
|
628
|
-
if (!user) {
|
|
629
|
-
throw new NotFoundError('User');
|
|
630
|
-
}
|
|
631
|
-
res.json(user);
|
|
632
|
-
} catch (error) {
|
|
633
|
-
next(error);
|
|
634
|
-
}
|
|
635
|
-
};
|
|
636
|
-
|
|
637
|
-
getById = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
638
|
-
try {
|
|
639
|
-
const user = await this.userService.findById(req.params.id);
|
|
640
|
-
if (!user) {
|
|
641
|
-
throw new NotFoundError('User', req.params.id);
|
|
642
|
-
}
|
|
643
|
-
res.json(user);
|
|
644
|
-
} catch (error) {
|
|
645
|
-
next(error);
|
|
646
|
-
}
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
create = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
650
|
-
try {
|
|
651
|
-
const user = await this.userService.create(req.body);
|
|
652
|
-
res.status(201).json(user);
|
|
653
|
-
} catch (error) {
|
|
654
|
-
next(error);
|
|
655
|
-
}
|
|
656
|
-
};
|
|
657
|
-
|
|
658
|
-
updateProfile = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
659
|
-
try {
|
|
660
|
-
const user = await this.userService.update(req.user!.id, req.body);
|
|
661
|
-
res.json(user);
|
|
662
|
-
} catch (error) {
|
|
663
|
-
next(error);
|
|
664
|
-
}
|
|
665
|
-
};
|
|
666
|
-
|
|
667
|
-
update = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
668
|
-
try {
|
|
669
|
-
const user = await this.userService.update(req.params.id, req.body);
|
|
670
|
-
res.json(user);
|
|
671
|
-
} catch (error) {
|
|
672
|
-
next(error);
|
|
673
|
-
}
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
delete = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
|
677
|
-
try {
|
|
678
|
-
await this.userService.delete(req.params.id);
|
|
679
|
-
res.status(204).send();
|
|
680
|
-
} catch (error) {
|
|
681
|
-
next(error);
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// src/services/UserService.ts
|
|
687
|
-
import { Prisma } from '@prisma/client';
|
|
688
|
-
import { prisma } from '../database/prisma';
|
|
689
|
-
import { ConflictError, NotFoundError } from '../errors/AppError';
|
|
690
|
-
import { hashPassword, verifyPassword } from '../utils/password';
|
|
691
|
-
|
|
692
|
-
interface FindAllOptions {
|
|
693
|
-
page: number;
|
|
694
|
-
limit: number;
|
|
695
|
-
search?: string;
|
|
696
|
-
role?: string;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
interface CreateUserData {
|
|
700
|
-
email: string;
|
|
701
|
-
password: string;
|
|
702
|
-
name: string;
|
|
703
|
-
role?: string;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
interface UpdateUserData {
|
|
707
|
-
email?: string;
|
|
708
|
-
name?: string;
|
|
709
|
-
role?: string;
|
|
710
|
-
password?: string;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
export class UserService {
|
|
714
|
-
async findAll(options: FindAllOptions) {
|
|
715
|
-
const { page, limit, search, role } = options;
|
|
716
|
-
const skip = (page - 1) * limit;
|
|
717
|
-
|
|
718
|
-
const where: Prisma.UserWhereInput = {
|
|
719
|
-
deletedAt: null,
|
|
720
|
-
...(search && {
|
|
721
|
-
OR: [
|
|
722
|
-
{ name: { contains: search, mode: 'insensitive' } },
|
|
723
|
-
{ email: { contains: search, mode: 'insensitive' } },
|
|
724
|
-
],
|
|
725
|
-
}),
|
|
726
|
-
...(role && { role }),
|
|
727
|
-
};
|
|
728
|
-
|
|
729
|
-
const [users, total] = await Promise.all([
|
|
730
|
-
prisma.user.findMany({
|
|
731
|
-
where,
|
|
732
|
-
skip,
|
|
733
|
-
take: limit,
|
|
734
|
-
select: {
|
|
735
|
-
id: true,
|
|
736
|
-
email: true,
|
|
737
|
-
name: true,
|
|
738
|
-
role: true,
|
|
739
|
-
createdAt: true,
|
|
740
|
-
},
|
|
741
|
-
orderBy: { createdAt: 'desc' },
|
|
742
|
-
}),
|
|
743
|
-
prisma.user.count({ where }),
|
|
744
|
-
]);
|
|
745
|
-
|
|
746
|
-
return {
|
|
747
|
-
data: users,
|
|
748
|
-
pagination: {
|
|
749
|
-
page,
|
|
750
|
-
limit,
|
|
751
|
-
total,
|
|
752
|
-
totalPages: Math.ceil(total / limit),
|
|
753
|
-
hasMore: skip + users.length < total,
|
|
754
|
-
},
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
async findById(id: string) {
|
|
759
|
-
return prisma.user.findFirst({
|
|
760
|
-
where: { id, deletedAt: null },
|
|
761
|
-
select: {
|
|
762
|
-
id: true,
|
|
763
|
-
email: true,
|
|
764
|
-
name: true,
|
|
765
|
-
role: true,
|
|
766
|
-
createdAt: true,
|
|
767
|
-
updatedAt: true,
|
|
768
|
-
organizations: {
|
|
769
|
-
select: {
|
|
770
|
-
organization: {
|
|
771
|
-
select: { id: true, name: true, slug: true },
|
|
772
|
-
},
|
|
773
|
-
role: true,
|
|
774
|
-
},
|
|
775
|
-
},
|
|
776
|
-
},
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
async findByEmail(email: string) {
|
|
781
|
-
return prisma.user.findFirst({
|
|
782
|
-
where: { email, deletedAt: null },
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
async create(data: CreateUserData) {
|
|
787
|
-
const existing = await this.findByEmail(data.email);
|
|
788
|
-
if (existing) {
|
|
789
|
-
throw new ConflictError('Email already in use');
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
const hashedPassword = await hashPassword(data.password);
|
|
793
|
-
|
|
794
|
-
return prisma.user.create({
|
|
795
|
-
data: {
|
|
796
|
-
...data,
|
|
797
|
-
password: hashedPassword,
|
|
798
|
-
},
|
|
799
|
-
select: {
|
|
800
|
-
id: true,
|
|
801
|
-
email: true,
|
|
802
|
-
name: true,
|
|
803
|
-
role: true,
|
|
804
|
-
createdAt: true,
|
|
805
|
-
},
|
|
806
|
-
});
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
async update(id: string, data: UpdateUserData) {
|
|
810
|
-
const user = await this.findById(id);
|
|
811
|
-
if (!user) {
|
|
812
|
-
throw new NotFoundError('User', id);
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
if (data.email && data.email !== user.email) {
|
|
816
|
-
const existing = await this.findByEmail(data.email);
|
|
817
|
-
if (existing) {
|
|
818
|
-
throw new ConflictError('Email already in use');
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
const updateData: Prisma.UserUpdateInput = { ...data };
|
|
823
|
-
if (data.password) {
|
|
824
|
-
updateData.password = await hashPassword(data.password);
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
return prisma.user.update({
|
|
828
|
-
where: { id },
|
|
829
|
-
data: updateData,
|
|
830
|
-
select: {
|
|
831
|
-
id: true,
|
|
832
|
-
email: true,
|
|
833
|
-
name: true,
|
|
834
|
-
role: true,
|
|
835
|
-
updatedAt: true,
|
|
836
|
-
},
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
async delete(id: string) {
|
|
841
|
-
const user = await this.findById(id);
|
|
842
|
-
if (!user) {
|
|
843
|
-
throw new NotFoundError('User', id);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
// Soft delete
|
|
847
|
-
await prisma.user.update({
|
|
848
|
-
where: { id },
|
|
849
|
-
data: { deletedAt: new Date() },
|
|
850
|
-
});
|
|
851
|
-
}
|
|
106
|
+
// middleware/validate.ts
|
|
107
|
+
import { Request, Response, NextFunction } from 'express';
|
|
108
|
+
import { ZodSchema } from 'zod';
|
|
852
109
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
110
|
+
export function validate(schema: ZodSchema) {
|
|
111
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
112
|
+
const result = schema.safeParse({
|
|
113
|
+
body: req.body,
|
|
114
|
+
query: req.query,
|
|
115
|
+
params: req.params,
|
|
856
116
|
});
|
|
857
117
|
|
|
858
|
-
if (!
|
|
859
|
-
return
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
const isValid = await verifyPassword(password, user.password);
|
|
863
|
-
if (!isValid) {
|
|
864
|
-
return null;
|
|
118
|
+
if (!result.success) {
|
|
119
|
+
return res.status(400).json({ errors: result.error.issues });
|
|
865
120
|
}
|
|
866
121
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
email: user.email,
|
|
870
|
-
name: user.name,
|
|
871
|
-
role: user.role,
|
|
872
|
-
};
|
|
873
|
-
}
|
|
122
|
+
next();
|
|
123
|
+
};
|
|
874
124
|
}
|
|
875
125
|
```
|
|
876
126
|
|
|
877
|
-
|
|
127
|
+
## Workflows
|
|
878
128
|
|
|
879
|
-
|
|
880
|
-
// src/schemas/userSchemas.ts
|
|
881
|
-
import { z } from 'zod';
|
|
129
|
+
### API Development Workflow
|
|
882
130
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
131
|
+
1. Define routes in `routes/index.ts`
|
|
132
|
+
2. Create controller with business logic
|
|
133
|
+
3. Add validation schemas with Zod
|
|
134
|
+
4. Write tests with supertest
|
|
135
|
+
5. Document with OpenAPI/Swagger
|
|
887
136
|
|
|
888
|
-
|
|
889
|
-
page: z.coerce.number().int().positive().default(1),
|
|
890
|
-
limit: z.coerce.number().int().positive().max(100).default(20),
|
|
891
|
-
search: z.string().optional(),
|
|
892
|
-
role: z.enum(['admin', 'user', 'guest']).optional(),
|
|
893
|
-
}),
|
|
137
|
+
### Middleware Order
|
|
894
138
|
|
|
895
|
-
create: z.object({
|
|
896
|
-
email: z.string().email('Invalid email address'),
|
|
897
|
-
password: z
|
|
898
|
-
.string()
|
|
899
|
-
.min(8, 'Password must be at least 8 characters')
|
|
900
|
-
.regex(/[A-Z]/, 'Password must contain an uppercase letter')
|
|
901
|
-
.regex(/[0-9]/, 'Password must contain a number'),
|
|
902
|
-
name: z.string().min(2).max(100),
|
|
903
|
-
role: z.enum(['admin', 'user', 'guest']).default('user'),
|
|
904
|
-
}),
|
|
905
|
-
|
|
906
|
-
update: z.object({
|
|
907
|
-
email: z.string().email('Invalid email address').optional(),
|
|
908
|
-
name: z.string().min(2).max(100).optional(),
|
|
909
|
-
role: z.enum(['admin', 'user', 'guest']).optional(),
|
|
910
|
-
password: z
|
|
911
|
-
.string()
|
|
912
|
-
.min(8, 'Password must be at least 8 characters')
|
|
913
|
-
.regex(/[A-Z]/, 'Password must contain an uppercase letter')
|
|
914
|
-
.regex(/[0-9]/, 'Password must contain a number')
|
|
915
|
-
.optional(),
|
|
916
|
-
}).refine(data => Object.keys(data).length > 0, {
|
|
917
|
-
message: 'At least one field must be provided',
|
|
918
|
-
}),
|
|
919
|
-
|
|
920
|
-
updateProfile: z.object({
|
|
921
|
-
name: z.string().min(2).max(100).optional(),
|
|
922
|
-
avatar: z.string().url().optional(),
|
|
923
|
-
}),
|
|
924
|
-
};
|
|
925
|
-
|
|
926
|
-
// src/schemas/projectSchemas.ts
|
|
927
|
-
import { z } from 'zod';
|
|
928
|
-
|
|
929
|
-
export const projectSchemas = {
|
|
930
|
-
params: z.object({
|
|
931
|
-
id: z.string().uuid('Invalid project ID'),
|
|
932
|
-
}),
|
|
933
|
-
|
|
934
|
-
list: z.object({
|
|
935
|
-
page: z.coerce.number().int().positive().default(1),
|
|
936
|
-
limit: z.coerce.number().int().positive().max(100).default(20),
|
|
937
|
-
status: z.enum(['draft', 'active', 'completed', 'archived']).optional(),
|
|
938
|
-
search: z.string().optional(),
|
|
939
|
-
sortBy: z.enum(['createdAt', 'name', 'updatedAt']).default('createdAt'),
|
|
940
|
-
sortOrder: z.enum(['asc', 'desc']).default('desc'),
|
|
941
|
-
}),
|
|
942
|
-
|
|
943
|
-
create: z.object({
|
|
944
|
-
name: z.string().min(1).max(255),
|
|
945
|
-
description: z.string().max(5000).optional(),
|
|
946
|
-
organizationId: z.string().uuid(),
|
|
947
|
-
}),
|
|
948
|
-
|
|
949
|
-
update: z.object({
|
|
950
|
-
name: z.string().min(1).max(255).optional(),
|
|
951
|
-
description: z.string().max(5000).optional(),
|
|
952
|
-
status: z.enum(['draft', 'active', 'completed', 'archived']).optional(),
|
|
953
|
-
}),
|
|
954
|
-
|
|
955
|
-
taskList: z.object({
|
|
956
|
-
page: z.coerce.number().int().positive().default(1),
|
|
957
|
-
limit: z.coerce.number().int().positive().max(100).default(20),
|
|
958
|
-
status: z.enum(['todo', 'in_progress', 'done']).optional(),
|
|
959
|
-
}),
|
|
960
|
-
|
|
961
|
-
createTask: z.object({
|
|
962
|
-
title: z.string().min(1).max(255),
|
|
963
|
-
description: z.string().max(5000).optional(),
|
|
964
|
-
priority: z.enum(['low', 'medium', 'high']).default('medium'),
|
|
965
|
-
assigneeId: z.string().uuid().optional(),
|
|
966
|
-
dueDate: z.coerce.date().optional(),
|
|
967
|
-
}),
|
|
968
|
-
};
|
|
969
139
|
```
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
export const prismaMock = mockDeep<PrismaClient>();
|
|
979
|
-
|
|
980
|
-
jest.mock('../src/database/prisma', () => ({
|
|
981
|
-
prisma: prismaMock,
|
|
982
|
-
}));
|
|
983
|
-
|
|
984
|
-
beforeEach(() => {
|
|
985
|
-
jest.clearAllMocks();
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
// tests/integration/users.test.ts
|
|
989
|
-
import request from 'supertest';
|
|
990
|
-
import { createApp } from '../../src/app';
|
|
991
|
-
import { prisma } from '../../src/database/prisma';
|
|
992
|
-
import { generateToken } from '../../src/utils/jwt';
|
|
993
|
-
|
|
994
|
-
const app = createApp();
|
|
995
|
-
|
|
996
|
-
describe('Users API', () => {
|
|
997
|
-
let adminToken: string;
|
|
998
|
-
let userToken: string;
|
|
999
|
-
|
|
1000
|
-
beforeAll(async () => {
|
|
1001
|
-
// Create test users
|
|
1002
|
-
const admin = await prisma.user.create({
|
|
1003
|
-
data: {
|
|
1004
|
-
email: 'admin@test.com',
|
|
1005
|
-
password: await hashPassword('Admin123!'),
|
|
1006
|
-
name: 'Admin User',
|
|
1007
|
-
role: 'admin',
|
|
1008
|
-
},
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
const user = await prisma.user.create({
|
|
1012
|
-
data: {
|
|
1013
|
-
email: 'user@test.com',
|
|
1014
|
-
password: await hashPassword('User123!'),
|
|
1015
|
-
name: 'Regular User',
|
|
1016
|
-
role: 'user',
|
|
1017
|
-
},
|
|
1018
|
-
});
|
|
1019
|
-
|
|
1020
|
-
adminToken = generateToken(admin);
|
|
1021
|
-
userToken = generateToken(user);
|
|
1022
|
-
});
|
|
1023
|
-
|
|
1024
|
-
afterAll(async () => {
|
|
1025
|
-
await prisma.user.deleteMany();
|
|
1026
|
-
});
|
|
1027
|
-
|
|
1028
|
-
describe('GET /api/v1/users', () => {
|
|
1029
|
-
it('should return paginated users for admin', async () => {
|
|
1030
|
-
const response = await request(app)
|
|
1031
|
-
.get('/api/v1/users')
|
|
1032
|
-
.set('Authorization', `Bearer ${adminToken}`)
|
|
1033
|
-
.expect(200);
|
|
1034
|
-
|
|
1035
|
-
expect(response.body).toHaveProperty('data');
|
|
1036
|
-
expect(response.body).toHaveProperty('pagination');
|
|
1037
|
-
expect(Array.isArray(response.body.data)).toBe(true);
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1040
|
-
it('should return 403 for non-admin users', async () => {
|
|
1041
|
-
await request(app)
|
|
1042
|
-
.get('/api/v1/users')
|
|
1043
|
-
.set('Authorization', `Bearer ${userToken}`)
|
|
1044
|
-
.expect(403);
|
|
1045
|
-
});
|
|
1046
|
-
|
|
1047
|
-
it('should return 401 without token', async () => {
|
|
1048
|
-
await request(app)
|
|
1049
|
-
.get('/api/v1/users')
|
|
1050
|
-
.expect(401);
|
|
1051
|
-
});
|
|
1052
|
-
|
|
1053
|
-
it('should filter users by search term', async () => {
|
|
1054
|
-
const response = await request(app)
|
|
1055
|
-
.get('/api/v1/users?search=admin')
|
|
1056
|
-
.set('Authorization', `Bearer ${adminToken}`)
|
|
1057
|
-
.expect(200);
|
|
1058
|
-
|
|
1059
|
-
expect(response.body.data.every((u: any) =>
|
|
1060
|
-
u.name.toLowerCase().includes('admin') ||
|
|
1061
|
-
u.email.toLowerCase().includes('admin')
|
|
1062
|
-
)).toBe(true);
|
|
1063
|
-
});
|
|
1064
|
-
});
|
|
1065
|
-
|
|
1066
|
-
describe('GET /api/v1/users/me', () => {
|
|
1067
|
-
it('should return current user profile', async () => {
|
|
1068
|
-
const response = await request(app)
|
|
1069
|
-
.get('/api/v1/users/me')
|
|
1070
|
-
.set('Authorization', `Bearer ${userToken}`)
|
|
1071
|
-
.expect(200);
|
|
1072
|
-
|
|
1073
|
-
expect(response.body.email).toBe('user@test.com');
|
|
1074
|
-
expect(response.body).not.toHaveProperty('password');
|
|
1075
|
-
});
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
describe('POST /api/v1/users', () => {
|
|
1079
|
-
it('should create a new user', async () => {
|
|
1080
|
-
const newUser = {
|
|
1081
|
-
email: 'new@test.com',
|
|
1082
|
-
password: 'NewUser123!',
|
|
1083
|
-
name: 'New User',
|
|
1084
|
-
role: 'user',
|
|
1085
|
-
};
|
|
1086
|
-
|
|
1087
|
-
const response = await request(app)
|
|
1088
|
-
.post('/api/v1/users')
|
|
1089
|
-
.set('Authorization', `Bearer ${adminToken}`)
|
|
1090
|
-
.send(newUser)
|
|
1091
|
-
.expect(201);
|
|
1092
|
-
|
|
1093
|
-
expect(response.body.email).toBe(newUser.email);
|
|
1094
|
-
expect(response.body).toHaveProperty('id');
|
|
1095
|
-
expect(response.body).not.toHaveProperty('password');
|
|
1096
|
-
});
|
|
1097
|
-
|
|
1098
|
-
it('should return 400 for invalid email', async () => {
|
|
1099
|
-
const response = await request(app)
|
|
1100
|
-
.post('/api/v1/users')
|
|
1101
|
-
.set('Authorization', `Bearer ${adminToken}`)
|
|
1102
|
-
.send({
|
|
1103
|
-
email: 'invalid-email',
|
|
1104
|
-
password: 'Valid123!',
|
|
1105
|
-
name: 'Test User',
|
|
1106
|
-
})
|
|
1107
|
-
.expect(400);
|
|
1108
|
-
|
|
1109
|
-
expect(response.body.error.code).toBe('VALIDATION_ERROR');
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
it('should return 409 for duplicate email', async () => {
|
|
1113
|
-
await request(app)
|
|
1114
|
-
.post('/api/v1/users')
|
|
1115
|
-
.set('Authorization', `Bearer ${adminToken}`)
|
|
1116
|
-
.send({
|
|
1117
|
-
email: 'user@test.com',
|
|
1118
|
-
password: 'Duplicate123!',
|
|
1119
|
-
name: 'Duplicate User',
|
|
1120
|
-
})
|
|
1121
|
-
.expect(409);
|
|
1122
|
-
});
|
|
1123
|
-
});
|
|
1124
|
-
});
|
|
1125
|
-
|
|
1126
|
-
// tests/unit/UserService.test.ts
|
|
1127
|
-
import { UserService } from '../../src/services/UserService';
|
|
1128
|
-
import { prismaMock } from '../setup';
|
|
1129
|
-
|
|
1130
|
-
describe('UserService', () => {
|
|
1131
|
-
const service = new UserService();
|
|
1132
|
-
|
|
1133
|
-
describe('findAll', () => {
|
|
1134
|
-
it('should return paginated users', async () => {
|
|
1135
|
-
const mockUsers = [
|
|
1136
|
-
{ id: '1', email: 'user1@test.com', name: 'User 1', role: 'user', createdAt: new Date() },
|
|
1137
|
-
{ id: '2', email: 'user2@test.com', name: 'User 2', role: 'user', createdAt: new Date() },
|
|
1138
|
-
];
|
|
1139
|
-
|
|
1140
|
-
prismaMock.user.findMany.mockResolvedValue(mockUsers);
|
|
1141
|
-
prismaMock.user.count.mockResolvedValue(2);
|
|
1142
|
-
|
|
1143
|
-
const result = await service.findAll({ page: 1, limit: 10 });
|
|
1144
|
-
|
|
1145
|
-
expect(result.data).toHaveLength(2);
|
|
1146
|
-
expect(result.pagination.total).toBe(2);
|
|
1147
|
-
expect(prismaMock.user.findMany).toHaveBeenCalledWith(
|
|
1148
|
-
expect.objectContaining({
|
|
1149
|
-
skip: 0,
|
|
1150
|
-
take: 10,
|
|
1151
|
-
})
|
|
1152
|
-
);
|
|
1153
|
-
});
|
|
1154
|
-
|
|
1155
|
-
it('should apply search filter', async () => {
|
|
1156
|
-
prismaMock.user.findMany.mockResolvedValue([]);
|
|
1157
|
-
prismaMock.user.count.mockResolvedValue(0);
|
|
1158
|
-
|
|
1159
|
-
await service.findAll({ page: 1, limit: 10, search: 'john' });
|
|
1160
|
-
|
|
1161
|
-
expect(prismaMock.user.findMany).toHaveBeenCalledWith(
|
|
1162
|
-
expect.objectContaining({
|
|
1163
|
-
where: expect.objectContaining({
|
|
1164
|
-
OR: expect.arrayContaining([
|
|
1165
|
-
{ name: { contains: 'john', mode: 'insensitive' } },
|
|
1166
|
-
]),
|
|
1167
|
-
}),
|
|
1168
|
-
})
|
|
1169
|
-
);
|
|
1170
|
-
});
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
describe('create', () => {
|
|
1174
|
-
it('should create a new user with hashed password', async () => {
|
|
1175
|
-
const userData = {
|
|
1176
|
-
email: 'new@test.com',
|
|
1177
|
-
password: 'Password123!',
|
|
1178
|
-
name: 'New User',
|
|
1179
|
-
};
|
|
1180
|
-
|
|
1181
|
-
prismaMock.user.findFirst.mockResolvedValue(null);
|
|
1182
|
-
prismaMock.user.create.mockResolvedValue({
|
|
1183
|
-
id: 'new-id',
|
|
1184
|
-
...userData,
|
|
1185
|
-
password: 'hashed',
|
|
1186
|
-
role: 'user',
|
|
1187
|
-
createdAt: new Date(),
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
const result = await service.create(userData);
|
|
1191
|
-
|
|
1192
|
-
expect(result.email).toBe(userData.email);
|
|
1193
|
-
expect(prismaMock.user.create).toHaveBeenCalledWith(
|
|
1194
|
-
expect.objectContaining({
|
|
1195
|
-
data: expect.objectContaining({
|
|
1196
|
-
email: userData.email,
|
|
1197
|
-
password: expect.not.stringContaining(userData.password),
|
|
1198
|
-
}),
|
|
1199
|
-
})
|
|
1200
|
-
);
|
|
1201
|
-
});
|
|
1202
|
-
});
|
|
1203
|
-
});
|
|
140
|
+
1. Security (helmet, cors)
|
|
141
|
+
2. Rate limiting
|
|
142
|
+
3. Body parsing
|
|
143
|
+
4. Logging
|
|
144
|
+
5. Authentication
|
|
145
|
+
6. Routes
|
|
146
|
+
7. 404 handler
|
|
147
|
+
8. Error handler
|
|
1204
148
|
```
|
|
1205
149
|
|
|
1206
|
-
##
|
|
1207
|
-
|
|
1208
|
-
### REST API for E-commerce Platform
|
|
1209
|
-
|
|
1210
|
-
```typescript
|
|
1211
|
-
// src/routes/products.ts
|
|
1212
|
-
import { Router } from 'express';
|
|
1213
|
-
import { ProductController } from '../controllers/ProductController';
|
|
1214
|
-
import { authenticate, authorize } from '../middleware/auth';
|
|
1215
|
-
import { validate } from '../middleware/validate';
|
|
1216
|
-
import { cache } from '../middleware/cache';
|
|
1217
|
-
import { upload } from '../middleware/upload';
|
|
1218
|
-
import { z } from 'zod';
|
|
1219
|
-
|
|
1220
|
-
const router = Router();
|
|
1221
|
-
const controller = new ProductController();
|
|
1222
|
-
|
|
1223
|
-
const productSchemas = {
|
|
1224
|
-
list: z.object({
|
|
1225
|
-
category: z.string().optional(),
|
|
1226
|
-
minPrice: z.coerce.number().positive().optional(),
|
|
1227
|
-
maxPrice: z.coerce.number().positive().optional(),
|
|
1228
|
-
search: z.string().optional(),
|
|
1229
|
-
sortBy: z.enum(['price', 'name', 'createdAt', 'popularity']).default('createdAt'),
|
|
1230
|
-
sortOrder: z.enum(['asc', 'desc']).default('desc'),
|
|
1231
|
-
page: z.coerce.number().positive().default(1),
|
|
1232
|
-
limit: z.coerce.number().positive().max(50).default(20),
|
|
1233
|
-
}),
|
|
1234
|
-
create: z.object({
|
|
1235
|
-
name: z.string().min(1).max(255),
|
|
1236
|
-
description: z.string().max(5000),
|
|
1237
|
-
price: z.number().positive(),
|
|
1238
|
-
categoryId: z.string().uuid(),
|
|
1239
|
-
inventory: z.number().int().nonnegative().default(0),
|
|
1240
|
-
sku: z.string().optional(),
|
|
1241
|
-
}),
|
|
1242
|
-
};
|
|
1243
|
-
|
|
1244
|
-
// Public routes
|
|
1245
|
-
router.get(
|
|
1246
|
-
'/',
|
|
1247
|
-
validate({ query: productSchemas.list }),
|
|
1248
|
-
cache({ ttl: 60 }),
|
|
1249
|
-
controller.list
|
|
1250
|
-
);
|
|
1251
|
-
|
|
1252
|
-
router.get(
|
|
1253
|
-
'/:id',
|
|
1254
|
-
cache({ ttl: 300 }),
|
|
1255
|
-
controller.getById
|
|
1256
|
-
);
|
|
150
|
+
## Best Practices
|
|
1257
151
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
controller.create
|
|
1266
|
-
);
|
|
152
|
+
| Do | Avoid |
|
|
153
|
+
|----|-------|
|
|
154
|
+
| Use async/await with try-catch | Callback patterns |
|
|
155
|
+
| Validate all inputs | Trusting client data |
|
|
156
|
+
| Use typed request/response | `any` types |
|
|
157
|
+
| Centralize error handling | Scattered try-catch |
|
|
158
|
+
| Use dependency injection | Direct imports in controllers |
|
|
1267
159
|
|
|
1268
|
-
|
|
1269
|
-
'/:id',
|
|
1270
|
-
authenticate(),
|
|
1271
|
-
authorize('admin'),
|
|
1272
|
-
controller.update
|
|
1273
|
-
);
|
|
160
|
+
## Project Structure
|
|
1274
161
|
|
|
1275
|
-
export { router as productRouter };
|
|
1276
162
|
```
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
export function setupWebSocket(server: Server) {
|
|
1288
|
-
const io = new SocketIOServer(server, {
|
|
1289
|
-
cors: {
|
|
1290
|
-
origin: process.env.CORS_ORIGINS?.split(',') || '*',
|
|
1291
|
-
credentials: true,
|
|
1292
|
-
},
|
|
1293
|
-
});
|
|
1294
|
-
|
|
1295
|
-
// Authentication middleware
|
|
1296
|
-
io.use(async (socket, next) => {
|
|
1297
|
-
try {
|
|
1298
|
-
const token = socket.handshake.auth.token;
|
|
1299
|
-
if (!token) {
|
|
1300
|
-
return next(new Error('Authentication required'));
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
const user = await verifyToken(token);
|
|
1304
|
-
socket.data.user = user;
|
|
1305
|
-
next();
|
|
1306
|
-
} catch (error) {
|
|
1307
|
-
next(new Error('Invalid token'));
|
|
1308
|
-
}
|
|
1309
|
-
});
|
|
1310
|
-
|
|
1311
|
-
io.on('connection', (socket) => {
|
|
1312
|
-
const user = socket.data.user;
|
|
1313
|
-
logger.info(`User connected: ${user.id}`);
|
|
1314
|
-
|
|
1315
|
-
// Join user's personal room
|
|
1316
|
-
socket.join(`user:${user.id}`);
|
|
1317
|
-
|
|
1318
|
-
// Join organization rooms
|
|
1319
|
-
socket.on('join:organization', async (orgId: string) => {
|
|
1320
|
-
// Verify user belongs to organization
|
|
1321
|
-
const membership = await checkMembership(user.id, orgId);
|
|
1322
|
-
if (membership) {
|
|
1323
|
-
socket.join(`org:${orgId}`);
|
|
1324
|
-
socket.emit('joined:organization', { orgId });
|
|
1325
|
-
}
|
|
1326
|
-
});
|
|
1327
|
-
|
|
1328
|
-
// Handle real-time notifications
|
|
1329
|
-
socket.on('disconnect', () => {
|
|
1330
|
-
logger.info(`User disconnected: ${user.id}`);
|
|
1331
|
-
});
|
|
1332
|
-
});
|
|
1333
|
-
|
|
1334
|
-
return io;
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
// Emit events from services
|
|
1338
|
-
export function emitToUser(io: SocketIOServer, userId: string, event: string, data: unknown) {
|
|
1339
|
-
io.to(`user:${userId}`).emit(event, data);
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
export function emitToOrganization(io: SocketIOServer, orgId: string, event: string, data: unknown) {
|
|
1343
|
-
io.to(`org:${orgId}`).emit(event, data);
|
|
1344
|
-
}
|
|
163
|
+
src/
|
|
164
|
+
├── app.ts # Express setup
|
|
165
|
+
├── server.ts # Server entry
|
|
166
|
+
├── config/ # Environment config
|
|
167
|
+
├── controllers/ # Route handlers
|
|
168
|
+
├── middleware/ # Custom middleware
|
|
169
|
+
├── routes/ # Route definitions
|
|
170
|
+
├── services/ # Business logic
|
|
171
|
+
├── utils/ # Helpers
|
|
172
|
+
└── types/ # TypeScript types
|
|
1345
173
|
```
|
|
1346
174
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
### Do's
|
|
1350
|
-
|
|
1351
|
-
- Use TypeScript for type safety
|
|
1352
|
-
- Implement proper error handling with custom error classes
|
|
1353
|
-
- Use validation middleware for all inputs
|
|
1354
|
-
- Use dependency injection for testability
|
|
1355
|
-
- Implement proper logging with correlation IDs
|
|
1356
|
-
- Use environment variables for configuration
|
|
1357
|
-
- Implement graceful shutdown handling
|
|
1358
|
-
- Use compression and rate limiting
|
|
1359
|
-
- Write integration and unit tests
|
|
1360
|
-
- Use async/await consistently
|
|
1361
|
-
|
|
1362
|
-
### Don'ts
|
|
1363
|
-
|
|
1364
|
-
- Don't use callbacks when async/await is available
|
|
1365
|
-
- Don't catch errors without proper handling
|
|
1366
|
-
- Don't expose stack traces in production
|
|
1367
|
-
- Don't store secrets in code
|
|
1368
|
-
- Don't skip input validation
|
|
1369
|
-
- Don't use synchronous file operations
|
|
1370
|
-
- Don't ignore error handling in middleware
|
|
1371
|
-
- Don't use `any` type unnecessarily
|
|
1372
|
-
- Don't forget to handle promise rejections
|
|
1373
|
-
- Don't skip security headers
|
|
1374
|
-
|
|
1375
|
-
## References
|
|
1376
|
-
|
|
1377
|
-
- [Express.js Documentation](https://expressjs.com/)
|
|
1378
|
-
- [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices)
|
|
1379
|
-
- [Prisma Documentation](https://www.prisma.io/docs)
|
|
1380
|
-
- [Zod Documentation](https://zod.dev/)
|
|
1381
|
-
- [TypeScript Express Tutorial](https://developer.okta.com/blog/2018/11/15/node-express-typescript)
|
|
175
|
+
For detailed examples and patterns, see reference files above.
|