create-xpress-backend 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.
@@ -0,0 +1,587 @@
1
+ 'use strict';
2
+
3
+ const { buildSharedFiles, buildReadme } = require('./shared');
4
+
5
+ function buildTsTemplates({ projectName, port, includeFileUpload, includeAuth }) {
6
+ const files = {};
7
+
8
+ // ─── package.json ─────────────────────────────────────────────────────────
9
+ const deps = {
10
+ '@prisma/adapter-pg': '^7.3.0',
11
+ '@prisma/client': '^7.3.0',
12
+ '@prisma/config': '^7.3.0',
13
+ bcryptjs: '^3.0.2',
14
+ cors: '^2.8.5',
15
+ dotenv: '^17.2.3',
16
+ express: '^4.21.2',
17
+ helmet: '^8.1.0',
18
+ joi: '^18.0.1',
19
+ jsonwebtoken: '^9.0.2',
20
+ morgan: '^1.10.1',
21
+ pg: '^8.18.0',
22
+ };
23
+ if (includeFileUpload) deps['multer'] = '^2.0.2';
24
+
25
+ const devDeps = {
26
+ '@types/bcryptjs': '^2.4.6',
27
+ '@types/cors': '^2.8.17',
28
+ '@types/express': '^5.0.0',
29
+ '@types/jsonwebtoken': '^9.0.6',
30
+ '@types/morgan': '^1.9.9',
31
+ '@types/node': '^20.14.0',
32
+ '@types/pg': '^8.11.6',
33
+ 'ts-node-dev': '^2.0.0',
34
+ typescript: '^5.4.5',
35
+ prisma: '^7.3.0',
36
+ };
37
+ if (includeFileUpload) devDeps['@types/multer'] = '^1.4.11';
38
+
39
+ files['package.json'] = JSON.stringify({
40
+ name: projectName,
41
+ version: '1.0.0',
42
+ description: '',
43
+ main: 'dist/server.js',
44
+ scripts: {
45
+ dev: 'ts-node-dev --respawn --transpile-only src/server.ts',
46
+ build: 'tsc',
47
+ start: 'node dist/server.js',
48
+ 'db:generate': 'prisma generate',
49
+ 'db:push': 'prisma db push',
50
+ 'db:reset': 'prisma db push --force-reset',
51
+ 'db:seed': 'ts-node-dev --transpile-only src/database/seed.ts',
52
+ 'db:studio': 'prisma studio',
53
+ 'db:dev': 'prisma db push && prisma generate',
54
+ },
55
+ keywords: [],
56
+ author: {
57
+ name: 'ayushsolanki29',
58
+ url: 'https://github.com/ayushsolanki29',
59
+ },
60
+ license: 'MIT',
61
+ homepage: 'https://github.com/ayushsolanki29/create-xpress-backend#readme',
62
+ repository: {
63
+ type: 'git',
64
+ url: 'https://github.com/ayushsolanki29/create-xpress-backend',
65
+ },
66
+ bugs: {
67
+ url: 'https://github.com/ayushsolanki29/create-xpress-backend/issues',
68
+ },
69
+ _createdWith: 'create-xpress-backend',
70
+ _scaffold: 'https://github.com/ayushsolanki29/create-xpress-backend',
71
+ dependencies: deps,
72
+ devDependencies: devDeps,
73
+ }, null, 2);
74
+
75
+ // ─── tsconfig.json ────────────────────────────────────────────────────────
76
+ files['tsconfig.json'] = JSON.stringify({
77
+ compilerOptions: {
78
+ target: 'ES2020',
79
+ module: 'commonjs',
80
+ lib: ['ES2020'],
81
+ outDir: './dist',
82
+ rootDir: './src',
83
+ strict: true,
84
+ esModuleInterop: true,
85
+ skipLibCheck: true,
86
+ forceConsistentCasingInFileNames: true,
87
+ resolveJsonModule: true,
88
+ declaration: true,
89
+ declarationMap: true,
90
+ sourceMap: true,
91
+ },
92
+ include: ['src/**/*'],
93
+ exclude: ['node_modules', 'dist'],
94
+ }, null, 2);
95
+
96
+ // ─── shared files (env, gitignore, prisma schema, prisma.config, seed) ────
97
+ Object.assign(files, buildSharedFiles({ projectName, port, includeFileUpload, includeAuth, language: 'ts' }));
98
+
99
+ // ─── src/types/express.d.ts (augment req.user) ───────────────────────────
100
+ files['src/types/express.d.ts'] = `import { JwtPayload } from 'jsonwebtoken';
101
+
102
+ declare global {
103
+ namespace Express {
104
+ interface Request {
105
+ user?: string | JwtPayload;
106
+ }
107
+ }
108
+ }
109
+ `;
110
+
111
+ // ─── src/database/prisma.ts ───────────────────────────────────────────────
112
+ files['src/database/prisma.ts'] = `// src/database/prisma.ts
113
+ import 'dotenv/config';
114
+ import { PrismaClient } from '@prisma/client';
115
+ import { PrismaPg } from '@prisma/adapter-pg';
116
+ import { Pool } from 'pg';
117
+
118
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
119
+ const adapter = new PrismaPg(pool);
120
+
121
+ const prisma = new PrismaClient({ adapter, log: ['error', 'warn'] });
122
+
123
+ export default prisma;
124
+ `;
125
+
126
+ // ─── src/middleware/validate.ts ───────────────────────────────────────────
127
+ files['src/middleware/validate.ts'] = `// src/middleware/validate.ts
128
+ import { Request, Response, NextFunction } from 'express';
129
+ import { ObjectSchema } from 'joi';
130
+
131
+ const validate =
132
+ (schema: ObjectSchema) =>
133
+ (req: Request, res: Response, next: NextFunction): void => {
134
+ const { error } = schema.validate(req.body);
135
+ if (error) {
136
+ res.status(400).json({ success: false, message: error.details[0].message });
137
+ return;
138
+ }
139
+ next();
140
+ };
141
+
142
+ export default validate;
143
+ `;
144
+
145
+ // ─── src/middleware/upload.ts (optional) ──────────────────────────────────
146
+ if (includeFileUpload) {
147
+ files['src/middleware/upload.ts'] = `// src/middleware/upload.ts
148
+ import multer, { FileFilterCallback } from 'multer';
149
+ import path from 'path';
150
+ import fs from 'fs';
151
+ import { Request } from 'express';
152
+
153
+ const uploadDir = path.join(process.cwd(), 'uploads');
154
+ if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
155
+
156
+ const storage = multer.diskStorage({
157
+ destination: (_req, _file, cb) => cb(null, uploadDir),
158
+ filename: (_req, file, cb) => {
159
+ const unique = Date.now() + '-' + Math.round(Math.random() * 1e9);
160
+ cb(null, file.fieldname + '-' + unique + path.extname(file.originalname));
161
+ },
162
+ });
163
+
164
+ const fileFilter = (_req: Request, file: Express.Multer.File, cb: FileFilterCallback): void => {
165
+ const allowed = [
166
+ 'image/jpeg', 'image/png', 'image/gif',
167
+ 'application/pdf', 'text/plain',
168
+ 'application/msword',
169
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
170
+ ];
171
+ allowed.includes(file.mimetype) ? cb(null, true) : cb(new Error('Invalid file type'));
172
+ };
173
+
174
+ const upload = multer({
175
+ storage,
176
+ limits: { fileSize: (parseInt(process.env.MAX_FILE_SIZE ?? '10')) * 1024 * 1024 },
177
+ fileFilter,
178
+ });
179
+
180
+ export default upload;
181
+ `;
182
+ }
183
+
184
+ // ─── src/middleware/auth.ts (optional) ────────────────────────────────────
185
+ if (includeAuth) {
186
+ files['src/middleware/auth.ts'] = `// src/middleware/auth.ts
187
+ import { Request, Response, NextFunction } from 'express';
188
+ import jwt from 'jsonwebtoken';
189
+
190
+ const authMiddleware = (req: Request, res: Response, next: NextFunction): void => {
191
+ const header = req.headers.authorization;
192
+ if (!header || !header.startsWith('Bearer ')) {
193
+ res.status(401).json({ success: false, message: 'No token provided' });
194
+ return;
195
+ }
196
+ const token = header.split(' ')[1];
197
+ try {
198
+ req.user = jwt.verify(token, process.env.JWT_SECRET as string);
199
+ next();
200
+ } catch {
201
+ res.status(401).json({ success: false, message: 'Invalid or expired token' });
202
+ }
203
+ };
204
+
205
+ export default authMiddleware;
206
+ `;
207
+ }
208
+
209
+ // ─── src/app.ts ───────────────────────────────────────────────────────────
210
+ files['src/app.ts'] = `import express, { Request, Response, NextFunction } from 'express';
211
+ import cors from 'cors';
212
+ import helmet from 'helmet';
213
+ import morgan from 'morgan';
214
+ import path from 'path';
215
+ import router from './modules';
216
+
217
+ const app = express();
218
+
219
+ const CLIENT_URL = process.env.CLIENT_URL ?? '*';
220
+
221
+ app.set('trust proxy', 1);
222
+ app.use(helmet());
223
+ app.use(cors({ origin: CLIENT_URL, credentials: true }));
224
+ app.use(morgan('dev'));
225
+ app.use(express.json({ limit: '10mb' }));
226
+ app.use(express.urlencoded({ extended: true }));
227
+ ${includeFileUpload ? `\napp.use('/uploads', express.static(path.join(__dirname, '../uploads')));\n` : ''}
228
+ app.use('/api', router);
229
+
230
+ app.get('/health', (_req: Request, res: Response) => {
231
+ res.status(200).json({
232
+ status: 'OK',
233
+ message: 'API Running ✅',
234
+ timestamp: new Date().toISOString(),
235
+ uptime: process.uptime(),
236
+ });
237
+ });
238
+
239
+ app.use((_req: Request, res: Response) => {
240
+ res.status(404).json({ success: false, message: 'Route not found' });
241
+ });
242
+
243
+ app.use((error: Error & { status?: number }, _req: Request, res: Response, _next: NextFunction) => {
244
+ console.error('🔥 Error:', error);
245
+ res.status(error.status ?? 500).json({
246
+ success: false,
247
+ message: error.message ?? 'Internal server error',
248
+ });
249
+ });
250
+
251
+ export default app;
252
+ `;
253
+
254
+ // ─── src/server.ts ────────────────────────────────────────────────────────
255
+ files['src/server.ts'] = `import 'dotenv/config';
256
+ import http from 'http';
257
+ import app from './app';
258
+
259
+ const PORT = process.env.PORT ?? ${port};
260
+ const CLIENT_URL = process.env.CLIENT_URL ?? 'http://localhost:3000';
261
+ const SERVER_URL = process.env.SERVER_URL ?? \`http://localhost:\${PORT}\`;
262
+
263
+ const server = http.createServer(app);
264
+
265
+ server.listen(PORT, () => {
266
+ console.log('\\n========================================');
267
+ console.log(' ${projectName} backend online');
268
+ console.log(' Server :', SERVER_URL);
269
+ console.log(' Client :', CLIENT_URL);
270
+ console.log(' Health :', \`\${SERVER_URL}/health\`);
271
+ console.log(' Started :', new Date().toLocaleString());
272
+ console.log('========================================\\n');
273
+ });
274
+
275
+ const shutdown = () => {
276
+ console.log('\\n⚠️ Shutting down...');
277
+ server.close(() => { console.log('✅ Server closed.'); process.exit(0); });
278
+ setTimeout(() => { console.log('⏳ Forced exit.'); process.exit(1); }, 5000);
279
+ };
280
+
281
+ process.on('SIGINT', shutdown);
282
+ process.on('SIGTERM', shutdown);
283
+
284
+ export default server;
285
+ `;
286
+
287
+ // ─── src/modules/index.ts ─────────────────────────────────────────────────
288
+ files['src/modules/index.ts'] = `import { Router } from 'express';
289
+ import demoRoutes from './demo/demo.routes';
290
+ ${includeAuth ? `import authRoutes from './auth/auth.routes';\n` : ''}
291
+ const router = Router();
292
+
293
+ router.use('/demo', demoRoutes);
294
+ ${includeAuth ? `router.use('/auth', authRoutes);\n` : ''}
295
+ export default router;
296
+ `;
297
+
298
+ // ─── demo module ──────────────────────────────────────────────────────────
299
+ files['src/modules/demo/demo.validation.ts'] = `// src/modules/demo/demo.validation.ts
300
+ import Joi from 'joi';
301
+
302
+ export const createDemoValidation = Joi.object({
303
+ name: Joi.string().min(3).max(100).required(),
304
+ email: Joi.string().email().required(),
305
+ description: Joi.string().max(500).optional(),
306
+ });
307
+
308
+ export const updateDemoValidation = Joi.object({
309
+ name: Joi.string().min(3).max(100).optional(),
310
+ email: Joi.string().email().optional(),
311
+ description: Joi.string().max(500).optional(),
312
+ });
313
+ `;
314
+
315
+ files['src/modules/demo/demo.service.ts'] = `// src/modules/demo/demo.service.ts
316
+ import prisma from '../../database/prisma';
317
+ ${includeFileUpload ? `import fs from 'fs';\n` : ''}
318
+ class DemoService {
319
+ async createDemo(data: { name: string; email: string }) {
320
+ return prisma.user.create({ data: { email: data.email, name: data.name } });
321
+ }
322
+
323
+ async getAllDemos() {
324
+ return prisma.user.findMany({
325
+ ${includeFileUpload ? `include: { files: true },` : ''}
326
+ orderBy: { createdAt: 'desc' },
327
+ });
328
+ }
329
+
330
+ async getDemoById(id: string) {
331
+ const demo = await prisma.user.findUnique({
332
+ where: { id },
333
+ ${includeFileUpload ? `include: { files: true },` : ''}
334
+ });
335
+ if (!demo) throw new Error('Demo not found');
336
+ return demo;
337
+ }
338
+
339
+ async updateDemo(id: string, data: { name?: string; email?: string }) {
340
+ return prisma.user.update({ where: { id }, data });
341
+ }
342
+
343
+ async deleteDemo(id: string) {
344
+ await prisma.user.delete({ where: { id } });
345
+ return { message: 'Demo deleted successfully' };
346
+ }
347
+ ${includeFileUpload ? `
348
+ async uploadFile(userId: string, file: Express.Multer.File) {
349
+ try {
350
+ return await prisma.file.create({
351
+ data: {
352
+ filename: file.filename,
353
+ originalName: file.originalname,
354
+ path: file.path,
355
+ size: file.size,
356
+ mimetype: file.mimetype,
357
+ userId,
358
+ },
359
+ });
360
+ } catch (error: any) {
361
+ if (file.path && fs.existsSync(file.path)) fs.unlinkSync(file.path);
362
+ throw new Error(\`Failed to upload file: \${error.message}\`);
363
+ }
364
+ }
365
+
366
+ async getUserFiles(userId: string) {
367
+ return prisma.file.findMany({ where: { userId }, orderBy: { createdAt: 'desc' } });
368
+ }
369
+
370
+ async deleteFile(fileId: string, userId: string) {
371
+ const file = await prisma.file.findFirst({ where: { id: fileId, userId } });
372
+ if (!file) throw new Error('File not found');
373
+ if (fs.existsSync(file.path)) fs.unlinkSync(file.path);
374
+ await prisma.file.delete({ where: { id: fileId } });
375
+ return { message: 'File deleted successfully' };
376
+ }
377
+ ` : ''}
378
+ }
379
+
380
+ export default new DemoService();
381
+ `;
382
+
383
+ files['src/modules/demo/demo.controller.ts'] = `// src/modules/demo/demo.controller.ts
384
+ import { Request, Response, NextFunction } from 'express';
385
+ import demoService from './demo.service';
386
+
387
+ class DemoController {
388
+ async createDemo(req: Request, res: Response, next: NextFunction) {
389
+ try {
390
+ const result = await demoService.createDemo(req.body);
391
+ res.status(201).json({ success: true, message: 'Demo created successfully', data: result });
392
+ } catch (error) { next(error); }
393
+ }
394
+
395
+ async getAllDemos(_req: Request, res: Response, next: NextFunction) {
396
+ try {
397
+ const result = await demoService.getAllDemos();
398
+ res.status(200).json({ success: true, data: result });
399
+ } catch (error) { next(error); }
400
+ }
401
+
402
+ async getDemoById(req: Request, res: Response, next: NextFunction) {
403
+ try {
404
+ const result = await demoService.getDemoById(req.params.id);
405
+ res.status(200).json({ success: true, data: result });
406
+ } catch (error) { next(error); }
407
+ }
408
+
409
+ async updateDemo(req: Request, res: Response, next: NextFunction) {
410
+ try {
411
+ const result = await demoService.updateDemo(req.params.id, req.body);
412
+ res.status(200).json({ success: true, message: 'Demo updated successfully', data: result });
413
+ } catch (error) { next(error); }
414
+ }
415
+
416
+ async deleteDemo(req: Request, res: Response, next: NextFunction) {
417
+ try {
418
+ const result = await demoService.deleteDemo(req.params.id);
419
+ res.status(200).json({ success: true, message: result.message });
420
+ } catch (error) { next(error); }
421
+ }
422
+ ${includeFileUpload ? `
423
+ async uploadFile(req: Request, res: Response, next: NextFunction) {
424
+ try {
425
+ if (!req.file) { res.status(400).json({ success: false, message: 'No file uploaded' }); return; }
426
+ const userId = (req.user as any)?.id ?? 'demo-user-id';
427
+ const result = await demoService.uploadFile(userId, req.file);
428
+ res.status(201).json({ success: true, message: 'File uploaded successfully', data: result });
429
+ } catch (error) { next(error); }
430
+ }
431
+
432
+ async getUserFiles(req: Request, res: Response, next: NextFunction) {
433
+ try {
434
+ const userId = (req.user as any)?.id ?? 'demo-user-id';
435
+ const result = await demoService.getUserFiles(userId);
436
+ res.status(200).json({ success: true, data: result });
437
+ } catch (error) { next(error); }
438
+ }
439
+
440
+ async deleteFile(req: Request, res: Response, next: NextFunction) {
441
+ try {
442
+ const userId = (req.user as any)?.id ?? 'demo-user-id';
443
+ const result = await demoService.deleteFile(req.params.fileId, userId);
444
+ res.status(200).json({ success: true, message: result.message });
445
+ } catch (error) { next(error); }
446
+ }
447
+ ` : ''}
448
+ }
449
+
450
+ export default new DemoController();
451
+ `;
452
+
453
+ files['src/modules/demo/demo.routes.ts'] = `// src/modules/demo/demo.routes.ts
454
+ import { Router } from 'express';
455
+ import demoController from './demo.controller';
456
+ import { createDemoValidation, updateDemoValidation } from './demo.validation';
457
+ import validate from '../../middleware/validate';
458
+ ${includeFileUpload ? `import upload from '../../middleware/upload';\n` : ''}
459
+ const router = Router();
460
+
461
+ router.post('/', validate(createDemoValidation), demoController.createDemo.bind(demoController));
462
+ router.get('/', demoController.getAllDemos.bind(demoController));
463
+ router.get('/:id', demoController.getDemoById.bind(demoController));
464
+ router.put('/:id', validate(updateDemoValidation), demoController.updateDemo.bind(demoController));
465
+ router.delete('/:id', demoController.deleteDemo.bind(demoController));
466
+ ${includeFileUpload ? `
467
+ router.post('/upload', upload.single('file'), demoController.uploadFile.bind(demoController));
468
+ router.get('/files/all', demoController.getUserFiles.bind(demoController));
469
+ router.delete('/files/:fileId', demoController.deleteFile.bind(demoController));
470
+ ` : ''}
471
+ export default router;
472
+ `;
473
+
474
+ // ─── auth module (optional) ───────────────────────────────────────────────
475
+ if (includeAuth) {
476
+ files['src/modules/auth/auth.validation.ts'] = `// src/modules/auth/auth.validation.ts
477
+ import Joi from 'joi';
478
+
479
+ export const registerValidation = Joi.object({
480
+ name: Joi.string().min(2).max(100).required(),
481
+ email: Joi.string().email().required(),
482
+ password: Joi.string().min(6).required(),
483
+ });
484
+
485
+ export const loginValidation = Joi.object({
486
+ email: Joi.string().email().required(),
487
+ password: Joi.string().required(),
488
+ });
489
+ `;
490
+
491
+ files['src/modules/auth/auth.service.ts'] = `// src/modules/auth/auth.service.ts
492
+ import bcrypt from 'bcryptjs';
493
+ import jwt from 'jsonwebtoken';
494
+ import prisma from '../../database/prisma';
495
+
496
+ class AuthService {
497
+ async register(data: { name: string; email: string; password: string }) {
498
+ const existing = await prisma.user.findUnique({ where: { email: data.email } });
499
+ if (existing) {
500
+ const err: any = new Error('Email already registered');
501
+ err.status = 409;
502
+ throw err;
503
+ }
504
+ const hashed = await bcrypt.hash(data.password, 10);
505
+ const user = await prisma.user.create({
506
+ data: { name: data.name, email: data.email, password: hashed },
507
+ });
508
+ const { password: _, ...safe } = user;
509
+ return safe;
510
+ }
511
+
512
+ async login(data: { email: string; password: string }) {
513
+ const user = await prisma.user.findUnique({ where: { email: data.email } });
514
+ if (!user) { const e: any = new Error('Invalid credentials'); e.status = 401; throw e; }
515
+
516
+ const valid = await bcrypt.compare(data.password, user.password!);
517
+ if (!valid) { const e: any = new Error('Invalid credentials'); e.status = 401; throw e; }
518
+
519
+ const token = jwt.sign(
520
+ { id: user.id, email: user.email },
521
+ process.env.JWT_SECRET as string,
522
+ { expiresIn: process.env.JWT_EXPIRES_IN ?? '7d' }
523
+ );
524
+ const { password: _, ...safe } = user;
525
+ return { user: safe, token };
526
+ }
527
+ }
528
+
529
+ export default new AuthService();
530
+ `;
531
+
532
+ files['src/modules/auth/auth.controller.ts'] = `// src/modules/auth/auth.controller.ts
533
+ import { Request, Response, NextFunction } from 'express';
534
+ import authService from './auth.service';
535
+
536
+ class AuthController {
537
+ async register(req: Request, res: Response, next: NextFunction) {
538
+ try {
539
+ const result = await authService.register(req.body);
540
+ res.status(201).json({ success: true, message: 'Registered successfully', data: result });
541
+ } catch (error) { next(error); }
542
+ }
543
+
544
+ async login(req: Request, res: Response, next: NextFunction) {
545
+ try {
546
+ const result = await authService.login(req.body);
547
+ res.status(200).json({ success: true, message: 'Login successful', data: result });
548
+ } catch (error) { next(error); }
549
+ }
550
+ }
551
+
552
+ export default new AuthController();
553
+ `;
554
+
555
+ files['src/modules/auth/auth.routes.ts'] = `// src/modules/auth/auth.routes.ts
556
+ import { Router } from 'express';
557
+ import authController from './auth.controller';
558
+ import { registerValidation, loginValidation } from './auth.validation';
559
+ import validate from '../../middleware/validate';
560
+
561
+ const router = Router();
562
+
563
+ router.post('/register', validate(registerValidation), authController.register.bind(authController));
564
+ router.post('/login', validate(loginValidation), authController.login.bind(authController));
565
+
566
+ export default router;
567
+ `;
568
+ }
569
+
570
+ // ─── prisma.config.ts ─────────────────────────────────────────────────────
571
+ files['prisma.config.ts'] = `import { defineConfig } from '@prisma/config';
572
+ import 'dotenv/config';
573
+
574
+ export default defineConfig({
575
+ datasource: {
576
+ url: process.env.DATABASE_URL,
577
+ },
578
+ });
579
+ `;
580
+
581
+ // ─── README ───────────────────────────────────────────────────────────────
582
+ files['README.md'] = buildReadme({ projectName, port, includeFileUpload, includeAuth, language: 'ts' });
583
+
584
+ return files;
585
+ }
586
+
587
+ module.exports = { buildTsTemplates };