@codeenthusiast09/create-express-app 1.0.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 +90 -0
- package/bin/cli.cjs +12 -0
- package/dist/generator.d.ts +59 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +178 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +113 -0
- package/dist/index.js.map +1 -0
- package/dist/installer.d.ts +37 -0
- package/dist/installer.d.ts.map +1 -0
- package/dist/installer.js +146 -0
- package/dist/installer.js.map +1 -0
- package/dist/prompts.d.ts +19 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +67 -0
- package/dist/prompts.js.map +1 -0
- package/package.json +59 -0
- package/templates/boilerplate/.env.example +24 -0
- package/templates/boilerplate/.eslintrc.cjs +27 -0
- package/templates/boilerplate/.prettierrc +9 -0
- package/templates/boilerplate/DEPENDENCIES.md +43 -0
- package/templates/boilerplate/README.md +282 -0
- package/templates/boilerplate/docker/.dockerignore +46 -0
- package/templates/boilerplate/docker/Dockerfile +61 -0
- package/templates/boilerplate/docker/docker-compose.yml +68 -0
- package/templates/boilerplate/drizzle/drizzle.config.ts +13 -0
- package/templates/boilerplate/drizzle/schema.ts +22 -0
- package/templates/boilerplate/jest.config.cjs +24 -0
- package/templates/boilerplate/nodemon.json +11 -0
- package/templates/boilerplate/package.json +61 -0
- package/templates/boilerplate/prisma/schema.prisma +0 -0
- package/templates/boilerplate/scripts/generate-module.cjs +397 -0
- package/templates/boilerplate/src/common/middleware/error.middleware.ts +121 -0
- package/templates/boilerplate/src/common/middleware/validation.middleware.ts +50 -0
- package/templates/boilerplate/src/common/utils/http-logger.ts +24 -0
- package/templates/boilerplate/src/common/utils/logger.ts +34 -0
- package/templates/boilerplate/src/common/utils/response-helper.ts +140 -0
- package/templates/boilerplate/src/config/env.ts +24 -0
- package/templates/boilerplate/src/config/index.ts +92 -0
- package/templates/boilerplate/src/database/drizzle.connection.ts +50 -0
- package/templates/boilerplate/src/database/index.ts +20 -0
- package/templates/boilerplate/src/database/mongoose.connection.ts +56 -0
- package/templates/boilerplate/src/database/prisma.connection.ts +50 -0
- package/templates/boilerplate/src/modules/.gitkeep +0 -0
- package/templates/boilerplate/src/server.ts +121 -0
- package/templates/boilerplate/src/types/express.types.ts +29 -0
- package/templates/boilerplate/src/types/index.ts +5 -0
- package/templates/boilerplate/src/types/response.types.ts +54 -0
- package/templates/boilerplate/tsconfig.json +72 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Module Generator
|
|
8
|
+
*
|
|
9
|
+
* Creates a new module with all necessary files:
|
|
10
|
+
* - controller.ts
|
|
11
|
+
* - service.ts
|
|
12
|
+
* - routes.ts
|
|
13
|
+
* - validation.ts
|
|
14
|
+
* - repository.interface.ts
|
|
15
|
+
* - repository.ts
|
|
16
|
+
*
|
|
17
|
+
* Usage: npm run generate:module <module-name>
|
|
18
|
+
* Example: npm run generate:module hazard-matrix
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Get module name from command line
|
|
22
|
+
const moduleName = process.argv[2];
|
|
23
|
+
|
|
24
|
+
if (!moduleName) {
|
|
25
|
+
console.error('❌ Error: Please provide a module name');
|
|
26
|
+
console.log('Usage: npm run generate:module <module-name>');
|
|
27
|
+
console.log('Example: npm run generate:module hazard-matrix');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Validate module name (allow lowercase letters, hyphens, and underscores)
|
|
32
|
+
if (!/^[a-z][a-z0-9-_]*$/.test(moduleName)) {
|
|
33
|
+
console.error(
|
|
34
|
+
'❌ Error: Module name must start with a lowercase letter and contain only lowercase letters, numbers, hyphens, and underscores',
|
|
35
|
+
);
|
|
36
|
+
console.log('Valid examples: users, user-profiles, hazard-matrix');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert kebab-case or snake_case to PascalCase
|
|
42
|
+
* Examples:
|
|
43
|
+
* hazard-matrix -> HazardMatrix
|
|
44
|
+
* user_profiles -> UserProfiles
|
|
45
|
+
* users -> Users
|
|
46
|
+
*/
|
|
47
|
+
function toPascalCase(str) {
|
|
48
|
+
return str
|
|
49
|
+
.split(/[-_]/)
|
|
50
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
51
|
+
.join('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert kebab-case or snake_case to camelCase
|
|
56
|
+
* Examples:
|
|
57
|
+
* hazard-matrix -> hazardMatrix
|
|
58
|
+
* user_profiles -> userProfiles
|
|
59
|
+
* users -> users
|
|
60
|
+
*/
|
|
61
|
+
function toCamelCase(str) {
|
|
62
|
+
const parts = str.split(/[-_]/);
|
|
63
|
+
return (
|
|
64
|
+
parts[0] +
|
|
65
|
+
parts
|
|
66
|
+
.slice(1)
|
|
67
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
68
|
+
.join('')
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Generate names
|
|
73
|
+
const ModuleName = toPascalCase(moduleName); // HazardMatrix (for class names, schemas)
|
|
74
|
+
const moduleNameCamel = toCamelCase(moduleName); // hazardMatrix (for variable names)
|
|
75
|
+
const modulePath = path.join(process.cwd(), 'src', 'modules', moduleName); // hazard-matrix (keep original)
|
|
76
|
+
|
|
77
|
+
// Check if module already exists
|
|
78
|
+
if (fs.existsSync(modulePath)) {
|
|
79
|
+
console.error(`❌ Error: Module "${moduleName}" already exists!`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Create module directory
|
|
84
|
+
console.log(`📁 Creating module: ${moduleName}...`);
|
|
85
|
+
fs.mkdirSync(modulePath, { recursive: true });
|
|
86
|
+
|
|
87
|
+
// File templates
|
|
88
|
+
const templates = {
|
|
89
|
+
// Controller
|
|
90
|
+
'controller.ts': `import { Request, Response } from 'express';
|
|
91
|
+
import { ${moduleNameCamel}Service } from './${moduleName}.service';
|
|
92
|
+
import { ResponseHelper } from '@/common/utils/response-helper';
|
|
93
|
+
import { asyncHandler } from '@/common/middleware/error.middleware';
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* ${ModuleName} Controller
|
|
97
|
+
*/
|
|
98
|
+
export class ${ModuleName}Controller {
|
|
99
|
+
/**
|
|
100
|
+
* Create ${moduleName}
|
|
101
|
+
*/
|
|
102
|
+
create = asyncHandler(async (req: Request, res: Response) => {
|
|
103
|
+
const data = await ${moduleNameCamel}Service.create(req.body);
|
|
104
|
+
ResponseHelper.created(res, data, '${ModuleName} created successfully');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get all ${moduleName}
|
|
109
|
+
*/
|
|
110
|
+
getAll = asyncHandler(async (_req: Request, res: Response) => {
|
|
111
|
+
const data = await ${moduleNameCamel}Service.findAll();
|
|
112
|
+
ResponseHelper.success(res, data, '${ModuleName} retrieved successfully');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get ${moduleName} by ID
|
|
117
|
+
*/
|
|
118
|
+
getById = asyncHandler(async (req: Request, res: Response) => {
|
|
119
|
+
const id = req.params.id;
|
|
120
|
+
if (!id) {
|
|
121
|
+
ResponseHelper.badRequest(res, 'ID is required');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const data = await ${moduleNameCamel}Service.findById(id);
|
|
125
|
+
ResponseHelper.success(res, data, '${ModuleName} retrieved successfully');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Update ${moduleName}
|
|
130
|
+
*/
|
|
131
|
+
update = asyncHandler(async (req: Request, res: Response) => {
|
|
132
|
+
const id = req.params.id;
|
|
133
|
+
if (!id) {
|
|
134
|
+
ResponseHelper.badRequest(res, 'ID is required');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const data = await ${moduleNameCamel}Service.update(id, req.body);
|
|
138
|
+
ResponseHelper.success(res, data, '${ModuleName} updated successfully');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Delete ${moduleName}
|
|
143
|
+
*/
|
|
144
|
+
delete = asyncHandler(async (req: Request, res: Response) => {
|
|
145
|
+
const id = req.params.id;
|
|
146
|
+
if (!id) {
|
|
147
|
+
ResponseHelper.badRequest(res, 'ID is required');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
await ${moduleNameCamel}Service.delete(id);
|
|
151
|
+
ResponseHelper.noContent(res);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export const ${moduleNameCamel}Controller = new ${ModuleName}Controller();
|
|
156
|
+
`,
|
|
157
|
+
|
|
158
|
+
// Service
|
|
159
|
+
'service.ts': `import { ${moduleNameCamel}Repository } from './${moduleName}.repository';
|
|
160
|
+
import { AppError } from '@/common/middleware/error.middleware';
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* ${ModuleName} Service
|
|
164
|
+
*
|
|
165
|
+
* Business logic for ${moduleName} operations.
|
|
166
|
+
*/
|
|
167
|
+
class ${ModuleName}Service {
|
|
168
|
+
/**
|
|
169
|
+
* Find all ${moduleName}
|
|
170
|
+
*/
|
|
171
|
+
async findAll() {
|
|
172
|
+
return await ${moduleNameCamel}Repository.findAll();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Find ${moduleName} by ID
|
|
177
|
+
*/
|
|
178
|
+
async findById(id: string) {
|
|
179
|
+
const data = await ${moduleNameCamel}Repository.findById(id);
|
|
180
|
+
if (!data) {
|
|
181
|
+
throw new AppError('${ModuleName} not found', 404, '${ModuleName.toUpperCase().replace(/-/g, '_')}_NOT_FOUND');
|
|
182
|
+
}
|
|
183
|
+
return data;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Create ${moduleName}
|
|
188
|
+
*/
|
|
189
|
+
async create(data: any) {
|
|
190
|
+
return await ${moduleNameCamel}Repository.create(data);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Update ${moduleName}
|
|
195
|
+
*/
|
|
196
|
+
async update(id: string, data: any) {
|
|
197
|
+
const updated = await ${moduleNameCamel}Repository.update(id, data);
|
|
198
|
+
if (!updated) {
|
|
199
|
+
throw new AppError('${ModuleName} not found', 404, '${ModuleName.toUpperCase().replace(/-/g, '_')}_NOT_FOUND');
|
|
200
|
+
}
|
|
201
|
+
return updated;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Delete ${moduleName}
|
|
206
|
+
*/
|
|
207
|
+
async delete(id: string) {
|
|
208
|
+
const deleted = await ${moduleNameCamel}Repository.delete(id);
|
|
209
|
+
if (!deleted) {
|
|
210
|
+
throw new AppError('${ModuleName} not found', 404, '${ModuleName.toUpperCase().replace(/-/g, '_')}_NOT_FOUND');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export const ${moduleNameCamel}Service = new ${ModuleName}Service();
|
|
216
|
+
`,
|
|
217
|
+
|
|
218
|
+
// Routes
|
|
219
|
+
'routes.ts': `import { Router } from 'express';
|
|
220
|
+
import { ${moduleNameCamel}Controller } from './${moduleName}.controller';
|
|
221
|
+
import { validate } from '@/common/middleware/validation.middleware';
|
|
222
|
+
import { create${ModuleName}Schema, update${ModuleName}Schema, idParamSchema } from './${moduleName}.validation';
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* ${ModuleName} Routes
|
|
226
|
+
*/
|
|
227
|
+
const router = Router();
|
|
228
|
+
|
|
229
|
+
router.get('/', ${moduleNameCamel}Controller.getAll);
|
|
230
|
+
router.get('/:id', validate({ params: idParamSchema }), ${moduleNameCamel}Controller.getById);
|
|
231
|
+
router.post('/', validate({ body: create${ModuleName}Schema }), ${moduleNameCamel}Controller.create);
|
|
232
|
+
router.patch('/:id', validate({ params: idParamSchema, body: update${ModuleName}Schema }), ${moduleNameCamel}Controller.update);
|
|
233
|
+
router.delete('/:id', validate({ params: idParamSchema }), ${moduleNameCamel}Controller.delete);
|
|
234
|
+
|
|
235
|
+
export default router;
|
|
236
|
+
`,
|
|
237
|
+
|
|
238
|
+
// Validation
|
|
239
|
+
'validation.ts': `import { z } from 'zod';
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* ${ModuleName} Validation Schemas
|
|
243
|
+
*/
|
|
244
|
+
|
|
245
|
+
export const create${ModuleName}Schema = z.object({
|
|
246
|
+
name: z.string().min(1, 'Name is required'),
|
|
247
|
+
// Add more fields as needed
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
export const update${ModuleName}Schema = z.object({
|
|
251
|
+
name: z.string().min(1).optional(),
|
|
252
|
+
// Add more fields as needed
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
export const idParamSchema = z.object({
|
|
256
|
+
id: z.string().min(1, 'ID is required'),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
export type Create${ModuleName}Input = z.infer<typeof create${ModuleName}Schema>;
|
|
260
|
+
export type Update${ModuleName}Input = z.infer<typeof update${ModuleName}Schema>;
|
|
261
|
+
`,
|
|
262
|
+
|
|
263
|
+
// Repository Interface
|
|
264
|
+
'repository.interface.ts': `/**
|
|
265
|
+
* ${ModuleName} Repository Interface
|
|
266
|
+
*/
|
|
267
|
+
|
|
268
|
+
export interface I${ModuleName}Repository {
|
|
269
|
+
findAll(): Promise<any[]>;
|
|
270
|
+
findById(id: string): Promise<any | null>;
|
|
271
|
+
create(data: any): Promise<any>;
|
|
272
|
+
update(id: string, data: any): Promise<any | null>;
|
|
273
|
+
delete(id: string): Promise<boolean>;
|
|
274
|
+
}
|
|
275
|
+
`,
|
|
276
|
+
|
|
277
|
+
// Repository Implementation
|
|
278
|
+
'repository.ts': `import { I${ModuleName}Repository } from './${moduleName}.repository.interface';
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* ${ModuleName} Repository
|
|
282
|
+
*
|
|
283
|
+
* TODO: Implement based on your database choice:
|
|
284
|
+
* - For Mongoose: Import and use Mongoose model
|
|
285
|
+
* - For Prisma: Import and use Prisma client
|
|
286
|
+
* - For Drizzle: Import and use Drizzle db instance
|
|
287
|
+
*/
|
|
288
|
+
class ${ModuleName}Repository implements I${ModuleName}Repository {
|
|
289
|
+
async findAll() {
|
|
290
|
+
// TODO: Implement database query
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async findById(_id: string) {
|
|
295
|
+
// TODO: Implement database query
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async create(data: any) {
|
|
300
|
+
// TODO: Implement database insertion
|
|
301
|
+
return data;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async update(_id: string, _data: any) {
|
|
305
|
+
// TODO: Implement database update
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async delete(_id: string) {
|
|
310
|
+
// TODO: Implement database deletion
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export const ${moduleNameCamel}Repository = new ${ModuleName}Repository();
|
|
316
|
+
`,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Generate files
|
|
320
|
+
Object.entries(templates).forEach(([filename, content]) => {
|
|
321
|
+
const filePath = path.join(modulePath, `${moduleName}.${filename}`);
|
|
322
|
+
fs.writeFileSync(filePath, content);
|
|
323
|
+
console.log(`✓ Created ${moduleName}.${filename}`);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ========== Auto-register routes ==========
|
|
327
|
+
console.log();
|
|
328
|
+
console.log('📝 Registering routes in server.ts...');
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const serverPath = path.join(process.cwd(), 'src', 'server.ts');
|
|
332
|
+
let serverContent = fs.readFileSync(serverPath, 'utf-8');
|
|
333
|
+
|
|
334
|
+
// 1. Add import statement at the top (after other imports)
|
|
335
|
+
const importStatement = `import ${moduleNameCamel}Routes from './modules/${moduleName}/${moduleName}.routes';`;
|
|
336
|
+
|
|
337
|
+
// Find the last import statement
|
|
338
|
+
const lastImportIndex = serverContent.lastIndexOf('import ');
|
|
339
|
+
const endOfLastImport = serverContent.indexOf('\n', lastImportIndex);
|
|
340
|
+
|
|
341
|
+
// Insert the new import after the last import
|
|
342
|
+
serverContent =
|
|
343
|
+
serverContent.slice(0, endOfLastImport + 1) +
|
|
344
|
+
importStatement +
|
|
345
|
+
'\n' +
|
|
346
|
+
serverContent.slice(endOfLastImport + 1);
|
|
347
|
+
|
|
348
|
+
// 2. Add route registration before error handlers
|
|
349
|
+
const routeRegistration = `\n/**\n * ${ModuleName} Routes\n */\napp.use('/api/${moduleName}', ${moduleNameCamel}Routes);\n`;
|
|
350
|
+
|
|
351
|
+
// Find the "Error Handling" comment (this should be before error handlers)
|
|
352
|
+
const errorHandlingComment = '/**\n * Error Handling (must be last)\n */';
|
|
353
|
+
const errorHandlingIndex = serverContent.indexOf(errorHandlingComment);
|
|
354
|
+
|
|
355
|
+
if (errorHandlingIndex === -1) {
|
|
356
|
+
// Fallback: insert before the first app.use with error handler
|
|
357
|
+
const notFoundHandlerIndex = serverContent.indexOf('app.use(notFoundHandler)');
|
|
358
|
+
if (notFoundHandlerIndex !== -1) {
|
|
359
|
+
serverContent =
|
|
360
|
+
serverContent.slice(0, notFoundHandlerIndex) +
|
|
361
|
+
routeRegistration +
|
|
362
|
+
serverContent.slice(notFoundHandlerIndex);
|
|
363
|
+
} else {
|
|
364
|
+
console.warn(
|
|
365
|
+
'⚠️ Could not find error handler location. You may need to manually register the routes.',
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
// Insert before the error handling comment
|
|
370
|
+
serverContent =
|
|
371
|
+
serverContent.slice(0, errorHandlingIndex) +
|
|
372
|
+
routeRegistration +
|
|
373
|
+
serverContent.slice(errorHandlingIndex);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Write the updated content back
|
|
377
|
+
fs.writeFileSync(serverPath, serverContent);
|
|
378
|
+
console.log(`✓ Routes registered in server.ts`);
|
|
379
|
+
console.log(` - Import: ${importStatement}`);
|
|
380
|
+
console.log(` - Route: app.use('/api/${moduleName}', ${moduleNameCamel}Routes)`);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.error('⚠️ Error auto-registering routes:', error.message);
|
|
383
|
+
console.log(' Please manually add the following to src/server.ts:');
|
|
384
|
+
console.log(
|
|
385
|
+
` import ${moduleNameCamel}Routes from './modules/${moduleName}/${moduleName}.routes';`,
|
|
386
|
+
);
|
|
387
|
+
console.log(` app.use('/api/${moduleName}', ${moduleNameCamel}Routes);`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
console.log();
|
|
391
|
+
console.log(`✅ Module "${moduleName}" created successfully!`);
|
|
392
|
+
console.log();
|
|
393
|
+
console.log('📝 Next steps:');
|
|
394
|
+
console.log(`1. Implement database operations in ${moduleName}.repository.ts`);
|
|
395
|
+
console.log(`2. Update validation schemas in ${moduleName}.validation.ts`);
|
|
396
|
+
console.log(`3. Start coding! Routes are already registered at /api/${moduleName}`);
|
|
397
|
+
console.log();
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { ZodError } from 'zod';
|
|
3
|
+
import { logger } from '@/common/utils/logger';
|
|
4
|
+
import { config } from '@/config';
|
|
5
|
+
import { ResponseHelper } from '@/common/utils/response-helper';
|
|
6
|
+
import { ErrorDetail } from '@/types/response.types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom Application Error
|
|
10
|
+
*
|
|
11
|
+
* Throw this for expected errors:
|
|
12
|
+
* throw new AppError('User not found', 404);
|
|
13
|
+
*/
|
|
14
|
+
export class AppError extends Error {
|
|
15
|
+
constructor(
|
|
16
|
+
public message: string,
|
|
17
|
+
public statusCode: number = 500,
|
|
18
|
+
public code: string = 'APP_ERROR',
|
|
19
|
+
public isOperational: boolean = true,
|
|
20
|
+
) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = this.constructor.name;
|
|
23
|
+
Error.captureStackTrace(this, this.constructor);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* MongoDB Duplicate Key Error Interface
|
|
29
|
+
*/
|
|
30
|
+
interface MongoError extends Error {
|
|
31
|
+
code?: number;
|
|
32
|
+
keyPattern?: Record<string, number>;
|
|
33
|
+
keyValue?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Global Error Handler
|
|
38
|
+
*
|
|
39
|
+
* Catches all errors and sends appropriate responses.
|
|
40
|
+
* MUST be registered LAST in middleware chain.
|
|
41
|
+
*/
|
|
42
|
+
export const errorHandler = (err: Error, req: Request, res: Response, _next: NextFunction) => {
|
|
43
|
+
// Log the error
|
|
44
|
+
logger.error({ err, req: { method: req.method, url: req.url } });
|
|
45
|
+
|
|
46
|
+
// Handle custom AppError
|
|
47
|
+
if (err instanceof AppError) {
|
|
48
|
+
// Client errors (4xx)
|
|
49
|
+
if (err.statusCode < 500) {
|
|
50
|
+
return ResponseHelper.fail(res, err.code, err.message, err.statusCode);
|
|
51
|
+
}
|
|
52
|
+
// Server errors (5xx)
|
|
53
|
+
return ResponseHelper.error(res, err.code, err.message, err.statusCode, err);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Handle Zod validation errors
|
|
57
|
+
if (err instanceof ZodError) {
|
|
58
|
+
const details: ErrorDetail[] = err.errors.map((error) => ({
|
|
59
|
+
field: error.path.join('.'),
|
|
60
|
+
message: error.message,
|
|
61
|
+
code: error.code,
|
|
62
|
+
}));
|
|
63
|
+
return ResponseHelper.validationError(res, details);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Handle JWT errors
|
|
67
|
+
if (err.name === 'JsonWebTokenError') {
|
|
68
|
+
return ResponseHelper.unauthorized(res, 'Invalid token');
|
|
69
|
+
}
|
|
70
|
+
if (err.name === 'TokenExpiredError') {
|
|
71
|
+
return ResponseHelper.unauthorized(res, 'Token expired');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle Mongoose errors
|
|
75
|
+
if (err.name === 'ValidationError') {
|
|
76
|
+
return ResponseHelper.badRequest(res, 'Validation failed');
|
|
77
|
+
}
|
|
78
|
+
if (err.name === 'CastError') {
|
|
79
|
+
return ResponseHelper.badRequest(res, 'Invalid ID format');
|
|
80
|
+
}
|
|
81
|
+
const mongoError = err as MongoError;
|
|
82
|
+
if (mongoError.code === 11000) {
|
|
83
|
+
const field = mongoError.keyPattern ? Object.keys(mongoError.keyPattern)[0] : 'field';
|
|
84
|
+
return ResponseHelper.conflict(res, `Duplicate value for ${field ?? 'unknown field'}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Default: Unknown error
|
|
88
|
+
return ResponseHelper.internalError(
|
|
89
|
+
res,
|
|
90
|
+
config.nodeEnv === 'development' ? err.message : 'An unexpected error occurred',
|
|
91
|
+
err,
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Async Handler Wrapper
|
|
97
|
+
*
|
|
98
|
+
* Wraps async route handlers to catch errors automatically.
|
|
99
|
+
*
|
|
100
|
+
* Usage:
|
|
101
|
+
* router.get('/users', asyncHandler(async (req, res) => {
|
|
102
|
+
* const users = await userService.findAll();
|
|
103
|
+
* ResponseHelper.success(res, users);
|
|
104
|
+
* }));
|
|
105
|
+
*/
|
|
106
|
+
export const asyncHandler = (
|
|
107
|
+
fn: (req: Request, res: Response, next: NextFunction) => Promise<void>,
|
|
108
|
+
) => {
|
|
109
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
110
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 404 Not Found Handler
|
|
116
|
+
*
|
|
117
|
+
* Register BEFORE error handler.
|
|
118
|
+
*/
|
|
119
|
+
export const notFoundHandler = (req: Request, res: Response) => {
|
|
120
|
+
return ResponseHelper.notFound(res, `Route ${req.method} ${req.url} not found`);
|
|
121
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { AnyZodObject, ZodError } from 'zod';
|
|
3
|
+
import { ResponseHelper } from '@/common/utils/response-helper';
|
|
4
|
+
import { ErrorDetail } from '@/types/response.types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validation Middleware
|
|
8
|
+
*
|
|
9
|
+
* Validates request body, query, or params against Zod schema.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* router.post('/users', validate({ body: createUserSchema }), createUser);
|
|
13
|
+
*/
|
|
14
|
+
interface ValidationSchemas {
|
|
15
|
+
body?: AnyZodObject;
|
|
16
|
+
query?: AnyZodObject;
|
|
17
|
+
params?: AnyZodObject;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const validate = (schemas: ValidationSchemas) => {
|
|
21
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
22
|
+
try {
|
|
23
|
+
if (schemas.body) {
|
|
24
|
+
req.body = await schemas.body.parseAsync(req.body);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (schemas.query) {
|
|
28
|
+
req.query = await schemas.query.parseAsync(req.query);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (schemas.params) {
|
|
32
|
+
req.params = await schemas.params.parseAsync(req.params);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return next();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error instanceof ZodError) {
|
|
38
|
+
const details: ErrorDetail[] = error.errors.map((err) => ({
|
|
39
|
+
field: err.path.join('.'),
|
|
40
|
+
message: err.message,
|
|
41
|
+
code: err.code,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
return ResponseHelper.validationError(res, details);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return next(error);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import pinoHttp from 'pino-http';
|
|
2
|
+
import { Request, Response } from 'express';
|
|
3
|
+
import { logger } from './logger';
|
|
4
|
+
|
|
5
|
+
export const httpLogger = pinoHttp({
|
|
6
|
+
logger,
|
|
7
|
+
|
|
8
|
+
customLogLevel: (_req, res, err) => {
|
|
9
|
+
if (res.statusCode >= 500 || err) return 'error';
|
|
10
|
+
if (res.statusCode >= 400) return 'warn';
|
|
11
|
+
return 'info';
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
serializers: {
|
|
15
|
+
req: (req: Request) => ({
|
|
16
|
+
method: req.method,
|
|
17
|
+
url: req.url,
|
|
18
|
+
userId: req.user?.id,
|
|
19
|
+
}),
|
|
20
|
+
res: (res: Response) => ({
|
|
21
|
+
statusCode: res.statusCode,
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
import { config } from '@/config';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Application Logger
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* logger.info('User created', { userId: 123 });
|
|
9
|
+
* logger.error('Failed to connect', { error });
|
|
10
|
+
* logger.debug('Cache hit', { key: 'user:123' });
|
|
11
|
+
*/
|
|
12
|
+
export const logger = pino({
|
|
13
|
+
level: config.logging.level,
|
|
14
|
+
|
|
15
|
+
// Pretty print in development, JSON in production
|
|
16
|
+
transport:
|
|
17
|
+
config.nodeEnv === 'development'
|
|
18
|
+
? {
|
|
19
|
+
target: 'pino-pretty',
|
|
20
|
+
options: {
|
|
21
|
+
colorize: true,
|
|
22
|
+
translateTime: 'HH:MM:ss Z',
|
|
23
|
+
ignore: 'pid,hostname',
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
: undefined,
|
|
27
|
+
|
|
28
|
+
// Add environment to every log
|
|
29
|
+
// base: {
|
|
30
|
+
// env: config.nodeEnv,
|
|
31
|
+
// },
|
|
32
|
+
|
|
33
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
34
|
+
});
|