express-genix 2.0.1 → 3.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.
Files changed (33) hide show
  1. package/README.md +13 -1
  2. package/index.js +22 -1
  3. package/lib/features.js +8 -0
  4. package/lib/generator.js +77 -2
  5. package/package.json +2 -2
  6. package/templates/config/queue.js.ejs +29 -0
  7. package/templates/config/schema.prisma.ejs +2 -0
  8. package/templates/controllers/adminController.js.ejs +109 -0
  9. package/templates/controllers/authController.js.ejs +9 -4
  10. package/templates/controllers/userController.js.ejs +1 -0
  11. package/templates/core/app.js.ejs +50 -1
  12. package/templates/core/env.ejs +17 -0
  13. package/templates/core/env.example.ejs +17 -0
  14. package/templates/core/package.json.ejs +8 -1
  15. package/templates/core/server.js.ejs +5 -2
  16. package/templates/graphql/resolvers.js.ejs +61 -0
  17. package/templates/graphql/typeDefs.js.ejs +53 -0
  18. package/templates/jobs/worker.js.ejs +60 -0
  19. package/templates/middleware/auditLog.js.ejs +62 -0
  20. package/templates/middleware/metrics.js.ejs +65 -0
  21. package/templates/middleware/rbac.js.ejs +86 -0
  22. package/templates/middleware/upload.js.ejs +50 -0
  23. package/templates/models/User.mongo.js.ejs +29 -0
  24. package/templates/models/User.postgres.js.ejs +7 -1
  25. package/templates/routes/adminRoutes.js.ejs +150 -0
  26. package/templates/routes/index.js.ejs +6 -0
  27. package/templates/routes/jobRoutes.js.ejs +85 -0
  28. package/templates/routes/uploadRoutes.js.ejs +100 -0
  29. package/templates/services/authService.js.ejs +1 -0
  30. package/templates/services/emailService.js.ejs +88 -0
  31. package/templates/services/userService.mongodb.js.ejs +32 -2
  32. package/templates/services/userService.postgres.js.ejs +33 -1
  33. package/templates/services/userService.prisma.js.ejs +50 -6
package/README.md CHANGED
@@ -15,20 +15,32 @@ A production-grade CLI tool that generates Express.js applications with best-in-
15
15
 
16
16
  **Security & Auth**
17
17
  - JWT access + refresh tokens with token blacklist logout
18
+ - Role-Based Access Control (RBAC) — admin, moderator, user roles with permission system
19
+ - Admin panel routes for user management (list, view, update roles, delete)
18
20
  - Password reset flow (forgot-password / reset-password with crypto tokens)
19
21
  - Optional Redis-backed blacklist for production multi-instance deployments
20
22
  - Zod request validation with pre-built schemas (register, login, reset, etc.)
21
23
  - bcrypt password hashing, input sanitization (`validator`)
22
24
  - Helmet, CORS, environment validation on startup
23
25
  - Auto-generated cryptographically secure JWT secrets
26
+ - Soft deletes with restore capability
24
27
 
25
28
  **API & Documentation**
26
29
  - Swagger UI + swagger-jsdoc annotation-based docs with example request/response bodies
30
+ - GraphQL (Apollo Server) with type definitions and resolvers
27
31
  - Consistent `{ success, data, meta }` response envelope
32
+ - API versioning (`/api/v1/` prefix)
28
33
  - Paginated list endpoints
29
34
  - Request ID / correlation tracking
30
35
  - Response caching middleware (Redis-backed, configurable TTL)
31
36
 
37
+ **Services & Infrastructure**
38
+ - Email service (Nodemailer) with welcome and password reset emails
39
+ - File uploads (Multer) with type filtering and size limits
40
+ - Background jobs (BullMQ) with Redis-backed queues and workers
41
+ - Prometheus metrics (`/metrics` endpoint with prom-client)
42
+ - Audit logging middleware with request tracking and sensitive field redaction
43
+
32
44
  **Developer Experience**
33
45
  - Interactive prompts — pick language, database, features via checkbox
34
46
  - Winston or Pino logger (you choose)
@@ -71,7 +83,7 @@ You'll be prompted for:
71
83
  1. **Project name**
72
84
  2. **Language** — JavaScript or TypeScript
73
85
  3. **Database** — MongoDB, PostgreSQL (Sequelize), PostgreSQL (Prisma), or None
74
- 4. **Features** — Auth, Rate Limiting, Swagger, Redis Token Blacklist, Docker, CI/CD, WebSocket, Request ID
86
+ 4. **Features** — Auth, Rate Limiting, Swagger, Redis, Docker, CI/CD, WebSocket, Request ID, Email, File Uploads, Soft Deletes, Audit Logging, Prometheus Metrics, API Versioning, Background Jobs, GraphQL
75
87
  5. **Logger** — Winston or Pino
76
88
 
77
89
  The CLI generates your project, installs dependencies, formats code, and creates an initial git commit.
package/index.js CHANGED
@@ -75,6 +75,14 @@ async function main() {
75
75
  { name: 'CI/CD (GitHub Actions)', value: 'cicd' },
76
76
  { name: 'WebSocket (Socket.io)', value: 'websocket' },
77
77
  { name: 'Request ID / Correlation ID', value: 'requestId', checked: true },
78
+ { name: 'Email Service (Nodemailer)', value: 'email' },
79
+ { name: 'File Uploads (Multer)', value: 'fileUpload' },
80
+ { name: 'Soft Deletes', value: 'softDelete' },
81
+ { name: 'Audit Logging', value: 'auditLog' },
82
+ { name: 'Prometheus Metrics', value: 'metrics' },
83
+ { name: 'API Versioning (/api/v1)', value: 'apiVersioning' },
84
+ { name: 'Background Jobs (BullMQ)', value: 'backgroundJobs' },
85
+ { name: 'GraphQL (Apollo Server)', value: 'graphql' },
78
86
  ],
79
87
  when: (ans) => ans.db !== 'none',
80
88
  },
@@ -89,6 +97,11 @@ async function main() {
89
97
  { name: 'CI/CD (GitHub Actions)', value: 'cicd' },
90
98
  { name: 'WebSocket (Socket.io)', value: 'websocket' },
91
99
  { name: 'Request ID / Correlation ID', value: 'requestId', checked: true },
100
+ { name: 'File Uploads (Multer)', value: 'fileUpload' },
101
+ { name: 'Audit Logging', value: 'auditLog' },
102
+ { name: 'Prometheus Metrics', value: 'metrics' },
103
+ { name: 'API Versioning (/api/v1)', value: 'apiVersioning' },
104
+ { name: 'GraphQL (Apollo Server)', value: 'graphql' },
92
105
  ],
93
106
  when: (ans) => ans.db === 'none',
94
107
  },
@@ -121,6 +134,14 @@ async function main() {
121
134
  hasWebsocket: features.includes('websocket'),
122
135
  hasRequestId: features.includes('requestId'),
123
136
  hasRedis: features.includes('redis') && features.includes('auth') && answers.db !== 'none',
137
+ hasEmail: features.includes('email') && answers.db !== 'none',
138
+ hasFileUpload: features.includes('fileUpload'),
139
+ hasSoftDelete: features.includes('softDelete') && answers.db !== 'none',
140
+ hasAuditLog: features.includes('auditLog'),
141
+ hasMetrics: features.includes('metrics'),
142
+ hasApiVersioning: features.includes('apiVersioning'),
143
+ hasBackgroundJobs: features.includes('backgroundJobs'),
144
+ hasGraphQL: features.includes('graphql'),
124
145
  jwtSecret: generateSecret(),
125
146
  jwtRefreshSecret: generateSecret(),
126
147
  };
@@ -163,7 +184,7 @@ async function main() {
163
184
  To get started:
164
185
  cd ${config.projectName}
165
186
  npm run dev
166
- ${config.hasSwagger ? `\nAPI Documentation: http://localhost:3000/api-docs` : ''}
187
+ ${config.hasSwagger ? `\nAPI Documentation: http://localhost:3000/api-docs` : ''}${config.hasGraphQL ? `\nGraphQL Playground: http://localhost:3000/graphql` : ''}${config.hasMetrics ? `\nPrometheus Metrics: http://localhost:3000/metrics` : ''}
167
188
  Health Check: http://localhost:3000/health
168
189
 
169
190
  Available scripts:
package/lib/features.js CHANGED
@@ -232,6 +232,14 @@ const inferConfig = (projectDir, pkg) => {
232
232
  hasWebsocket: !!(pkg.dependencies && pkg.dependencies['socket.io']),
233
233
  hasRedis: !!(pkg.dependencies && pkg.dependencies.ioredis),
234
234
  hasRequestId: !!(pkg.dependencies && pkg.dependencies.uuid),
235
+ hasEmail: !!(pkg.dependencies && pkg.dependencies.nodemailer),
236
+ hasFileUpload: !!(pkg.dependencies && pkg.dependencies.multer),
237
+ hasSoftDelete: false,
238
+ hasAuditLog: false,
239
+ hasMetrics: !!(pkg.dependencies && pkg.dependencies['prom-client']),
240
+ hasApiVersioning: false,
241
+ hasBackgroundJobs: !!(pkg.dependencies && pkg.dependencies.bullmq),
242
+ hasGraphQL: !!(pkg.dependencies && pkg.dependencies['@apollo/server']),
235
243
  logger: pkg.dependencies && pkg.dependencies.pino ? 'pino' : 'winston',
236
244
  };
237
245
  };
package/lib/generator.js CHANGED
@@ -49,6 +49,18 @@ const createDirectoryStructure = (projectDir, config) => {
49
49
  dirs.push('.github', '.github/workflows');
50
50
  }
51
51
 
52
+ if (config.hasFileUpload) {
53
+ dirs.push('uploads');
54
+ }
55
+
56
+ if (config.hasBackgroundJobs) {
57
+ dirs.push('src/jobs');
58
+ }
59
+
60
+ if (config.hasGraphQL) {
61
+ dirs.push('src/graphql');
62
+ }
63
+
52
64
  fs.mkdirSync(projectDir);
53
65
  dirs.forEach((dir) => {
54
66
  fs.mkdirSync(path.join(projectDir, dir), { recursive: true });
@@ -128,6 +140,10 @@ const generateFiles = async (config, projectDir) => {
128
140
  configFiles.push({ template: 'config/redis.js.ejs', output: `src/config/redis.${ext}` });
129
141
  }
130
142
 
143
+ if (config.hasBackgroundJobs) {
144
+ configFiles.push({ template: 'config/queue.js.ejs', output: `src/config/queue.${ext}` });
145
+ }
146
+
131
147
  // Route files
132
148
  const routeFiles = [
133
149
  { template: 'routes/index.js.ejs', output: `src/routes/index.${ext}` },
@@ -141,11 +157,13 @@ const generateFiles = async (config, projectDir) => {
141
157
  if (config.hasAuth) {
142
158
  controllerFiles.push(
143
159
  { template: 'controllers/authController.js.ejs', output: `src/controllers/authController.${ext}` },
144
- { template: 'controllers/userController.js.ejs', output: `src/controllers/userController.${ext}` }
160
+ { template: 'controllers/userController.js.ejs', output: `src/controllers/userController.${ext}` },
161
+ { template: 'controllers/adminController.js.ejs', output: `src/controllers/adminController.${ext}` }
145
162
  );
146
163
  routeFiles.push(
147
164
  { template: 'routes/authRoutes.js.ejs', output: `src/routes/authRoutes.${ext}` },
148
- { template: 'routes/userRoutes.js.ejs', output: `src/routes/userRoutes.${ext}` }
165
+ { template: 'routes/userRoutes.js.ejs', output: `src/routes/userRoutes.${ext}` },
166
+ { template: 'routes/adminRoutes.js.ejs', output: `src/routes/adminRoutes.${ext}` }
149
167
  );
150
168
  serviceFiles.push(
151
169
  { template: 'services/authService.js.ejs', output: `src/services/authService.${ext}` }
@@ -178,6 +196,24 @@ const generateFiles = async (config, projectDir) => {
178
196
  );
179
197
  }
180
198
 
199
+ if (config.hasEmail) {
200
+ serviceFiles.push(
201
+ { template: 'services/emailService.js.ejs', output: `src/services/emailService.${ext}` }
202
+ );
203
+ }
204
+
205
+ if (config.hasFileUpload) {
206
+ routeFiles.push(
207
+ { template: 'routes/uploadRoutes.js.ejs', output: `src/routes/uploadRoutes.${ext}` }
208
+ );
209
+ }
210
+
211
+ if (config.hasBackgroundJobs) {
212
+ routeFiles.push(
213
+ { template: 'routes/jobRoutes.js.ejs', output: `src/routes/jobRoutes.${ext}` }
214
+ );
215
+ }
216
+
181
217
  // Middleware files
182
218
  const middlewareFiles = [
183
219
  { template: 'middleware/errorHandler.js.ejs', output: `src/middleware/errorHandler.${ext}` },
@@ -196,6 +232,30 @@ const generateFiles = async (config, projectDir) => {
196
232
  );
197
233
  }
198
234
 
235
+ if (config.hasAuth) {
236
+ middlewareFiles.push(
237
+ { template: 'middleware/rbac.js.ejs', output: `src/middleware/rbac.${ext}` }
238
+ );
239
+ }
240
+
241
+ if (config.hasFileUpload) {
242
+ middlewareFiles.push(
243
+ { template: 'middleware/upload.js.ejs', output: `src/middleware/upload.${ext}` }
244
+ );
245
+ }
246
+
247
+ if (config.hasAuditLog) {
248
+ middlewareFiles.push(
249
+ { template: 'middleware/auditLog.js.ejs', output: `src/middleware/auditLog.${ext}` }
250
+ );
251
+ }
252
+
253
+ if (config.hasMetrics) {
254
+ middlewareFiles.push(
255
+ { template: 'middleware/metrics.js.ejs', output: `src/middleware/metrics.${ext}` }
256
+ );
257
+ }
258
+
199
259
  if (config.hasRedis) {
200
260
  middlewareFiles.push(
201
261
  { template: 'middleware/cache.js.ejs', output: `src/middleware/cache.${ext}` }
@@ -245,6 +305,21 @@ const generateFiles = async (config, projectDir) => {
245
305
  ...testFiles,
246
306
  ];
247
307
 
308
+ // GraphQL files
309
+ if (config.hasGraphQL) {
310
+ allFiles.push(
311
+ { template: 'graphql/typeDefs.js.ejs', output: `src/graphql/typeDefs.${ext}` },
312
+ { template: 'graphql/resolvers.js.ejs', output: `src/graphql/resolvers.${ext}` }
313
+ );
314
+ }
315
+
316
+ // Background job worker
317
+ if (config.hasBackgroundJobs) {
318
+ allFiles.push(
319
+ { template: 'jobs/worker.js.ejs', output: `src/jobs/worker.${ext}` }
320
+ );
321
+ }
322
+
248
323
  const errors = [];
249
324
 
250
325
  for (const file of allFiles) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "express-genix",
3
- "version": "2.0.1",
4
- "description": "Production-grade CLI to generate Express apps with JWT, TypeScript, Prisma, MongoDB, PostgreSQL, rate-limiting, Swagger, CI/CD, and more",
3
+ "version": "3.0.0",
4
+ "description": "Production-grade CLI to generate Express apps with JWT, RBAC, GraphQL, TypeScript, Prisma, MongoDB, PostgreSQL, file uploads, email, background jobs, and more",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "express-genix": "index.js"
@@ -0,0 +1,29 @@
1
+ const { Queue } = require('bullmq');
2
+ const logger = require('../utils/logger');
3
+
4
+ const redisConnection = {
5
+ host: process.env.REDIS_HOST || 'localhost',
6
+ port: parseInt(process.env.REDIS_PORT, 10) || 6379,
7
+ };
8
+
9
+ // Define your queues here
10
+ const emailQueue = new Queue('email', { connection: redisConnection });
11
+ const defaultQueue = new Queue('default', { connection: redisConnection });
12
+
13
+ const addJob = async (queueName, jobName, data, options = {}) => {
14
+ const queues = { email: emailQueue, default: defaultQueue };
15
+ const queue = queues[queueName] || defaultQueue;
16
+
17
+ const job = await queue.add(jobName, data, {
18
+ attempts: 3,
19
+ backoff: { type: 'exponential', delay: 1000 },
20
+ removeOnComplete: { count: 100 },
21
+ removeOnFail: { count: 500 },
22
+ ...options,
23
+ });
24
+
25
+ logger.info(`Job added: ${jobName} (${job.id}) to queue ${queueName}`);
26
+ return job;
27
+ };
28
+
29
+ module.exports = { emailQueue, defaultQueue, addJob, redisConnection };
@@ -12,8 +12,10 @@ model User {
12
12
  username String @unique @db.VarChar(50)
13
13
  email String @unique
14
14
  password String
15
+ role String @default("user")
15
16
  createdAt DateTime @default(now()) @map("created_at")
16
17
  updatedAt DateTime @updatedAt @map("updated_at")
18
+ <% if (hasSoftDelete) { %> deletedAt DateTime? @map("deleted_at")<% } %>
17
19
 
18
20
  @@map("users")
19
21
  }
@@ -0,0 +1,109 @@
1
+ const userService = require('../services/userService');
2
+ const { AppError } = require('../utils/errors');
3
+ const { success } = require('../utils/response');
4
+
5
+ const listUsers = async (req, res, next) => {
6
+ try {
7
+ const page = parseInt(req.query.page, 10) || 1;
8
+ const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
9
+ const users = await userService.findAll({ page, limit });
10
+
11
+ return success(res, users);
12
+ } catch (error) {
13
+ next(error);
14
+ }
15
+ };
16
+
17
+ const getUserById = async (req, res, next) => {
18
+ try {
19
+ const user = await userService.findById(req.params.id);
20
+ if (!user) {
21
+ throw new AppError('User not found', 404);
22
+ }
23
+
24
+ return success(res, {
25
+ user: {
26
+ id: user.id,
27
+ username: user.username,
28
+ email: user.email,
29
+ role: user.role,
30
+ createdAt: user.createdAt,
31
+ },
32
+ });
33
+ } catch (error) {
34
+ next(error);
35
+ }
36
+ };
37
+
38
+ const updateUserRole = async (req, res, next) => {
39
+ try {
40
+ const { role } = req.body;
41
+ const validRoles = ['user', 'moderator', 'admin'];
42
+
43
+ if (!validRoles.includes(role)) {
44
+ throw new AppError(`Invalid role. Must be one of: ${validRoles.join(', ')}`, 400);
45
+ }
46
+
47
+ const user = await userService.findById(req.params.id);
48
+ if (!user) {
49
+ throw new AppError('User not found', 404);
50
+ }
51
+
52
+ const updatedUser = await userService.updateById(req.params.id, { role });
53
+
54
+ return success(res, {
55
+ message: 'User role updated successfully',
56
+ user: {
57
+ id: updatedUser.id,
58
+ username: updatedUser.username,
59
+ email: updatedUser.email,
60
+ role: updatedUser.role,
61
+ },
62
+ });
63
+ } catch (error) {
64
+ next(error);
65
+ }
66
+ };
67
+
68
+ const deleteUser = async (req, res, next) => {
69
+ try {
70
+ if (req.params.id === req.user.userId) {
71
+ throw new AppError('Cannot delete your own admin account', 400);
72
+ }
73
+
74
+ const user = await userService.findById(req.params.id);
75
+ if (!user) {
76
+ throw new AppError('User not found', 404);
77
+ }
78
+
79
+ await userService.deleteById(req.params.id);
80
+ return success(res, { message: 'User deleted successfully' });
81
+ } catch (error) {
82
+ next(error);
83
+ }
84
+ };
85
+ <% if (hasSoftDelete) { %>
86
+ const restoreUser = async (req, res, next) => {
87
+ try {
88
+ const user = await userService.restoreById(req.params.id);
89
+ if (!user) {
90
+ throw new AppError('User not found or not deleted', 404);
91
+ }
92
+ return success(res, { message: 'User restored successfully' });
93
+ } catch (error) {
94
+ next(error);
95
+ }
96
+ };
97
+
98
+ const listDeletedUsers = async (req, res, next) => {
99
+ try {
100
+ const page = parseInt(req.query.page, 10) || 1;
101
+ const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
102
+ const users = await userService.findDeleted({ page, limit });
103
+ return success(res, users);
104
+ } catch (error) {
105
+ next(error);
106
+ }
107
+ };
108
+ <% } %>
109
+ module.exports = { listUsers, getUserById, updateUserRole, deleteUser<% if (hasSoftDelete) { %>, restoreUser, listDeletedUsers<% } %> };
@@ -3,6 +3,7 @@ const authService = require('../services/authService');
3
3
  const userService = require('../services/userService');
4
4
  const { AppError } = require('../utils/errors');
5
5
  const { success, created } = require('../utils/response');
6
+ <% if (hasEmail) { %>const emailService = require('../services/emailService');<% } %>
6
7
 
7
8
  const register = async (req, res, next) => {
8
9
  try {
@@ -16,7 +17,10 @@ const register = async (req, res, next) => {
16
17
  const hashedPassword = await bcrypt.hash(password, 12);
17
18
  const user = await userService.create({ username, email, password: hashedPassword });
18
19
  const tokens = authService.generateTokens(user);
19
-
20
+ <% if (hasEmail) { %>
21
+ // Send welcome email (non-blocking)
22
+ emailService.sendWelcomeEmail(email, username).catch(() => {});
23
+ <% } %>
20
24
  return created(res, {
21
25
  message: 'User registered successfully',
22
26
  user: { id: user.id, username: user.username, email: user.email },
@@ -113,12 +117,13 @@ const forgotPassword = async (req, res, next) => {
113
117
  }
114
118
 
115
119
  const resetToken = await authService.generateResetToken(email);
116
-
120
+ <% if (hasEmail) { %>
121
+ await emailService.sendPasswordResetEmail(email, resetToken);
122
+ <% } else { %>
117
123
  // TODO: Send email with reset link
118
- // await emailService.sendResetEmail(email, resetToken);
119
- //
120
124
  // For development, log the token:
121
125
  console.log(`Password reset token for ${email}: ${resetToken}`);
126
+ <% } %>
122
127
 
123
128
  return success(res, { message: 'If an account with that email exists, a reset link has been sent.' });
124
129
  } catch (error) {
@@ -14,6 +14,7 @@ const getProfile = async (req, res, next) => {
14
14
  id: user.id,
15
15
  username: user.username,
16
16
  email: user.email,
17
+ role: user.role,
17
18
  createdAt: user.createdAt,
18
19
  },
19
20
  });
@@ -6,6 +6,13 @@ const morgan = require('morgan');
6
6
  <% if (hasSwagger) { %>const swaggerUi = require('swagger-ui-express');
7
7
  const swaggerSpec = require('./config/swagger');<% } %>
8
8
  <% if (hasRequestId) { %>const { requestId } = require('./middleware/requestId');<% } %>
9
+ <% if (hasMetrics) { %>const { metricsMiddleware, metricsEndpoint } = require('./middleware/metrics');<% } %>
10
+ <% if (hasAuditLog) { %>const { auditLog } = require('./middleware/auditLog');<% } %>
11
+ <% if (hasGraphQL) { %>const { ApolloServer } = require('@apollo/server');
12
+ const { expressMiddleware } = require('@apollo/server/express4');
13
+ const typeDefs = require('./graphql/typeDefs');
14
+ const resolvers = require('./graphql/resolvers');<% if (hasAuth) { %>
15
+ const authService = require('./services/authService');<% } %><% } %>
9
16
  const routes = require('./routes');
10
17
  const errorHandler = require('./middleware/errorHandler');
11
18
  <% if (hasDatabase || hasAuth) { %>const { validateEnv } = require('./utils/envValidator');
@@ -31,6 +38,18 @@ app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));
31
38
  // Body parsing
32
39
  app.use(express.json({ limit: '10mb' }));
33
40
  app.use(express.urlencoded({ extended: true }));
41
+ <% if (hasFileUpload) { %>
42
+ // Serve uploaded files
43
+ const path = require('path');
44
+ app.use('/uploads', express.static(path.join(process.cwd(), process.env.UPLOAD_DIR || 'uploads')));
45
+ <% } %>
46
+ <% if (hasMetrics) { %>
47
+ // Prometheus metrics
48
+ app.use(metricsMiddleware);
49
+ app.get('/metrics', metricsEndpoint);<% } %>
50
+ <% if (hasAuditLog) { %>
51
+ // Audit logging
52
+ app.use(auditLog());<% } %>
34
53
  <% if (hasRateLimit) { %>
35
54
  // Rate limiting
36
55
  const limiter = rateLimit({
@@ -96,9 +115,39 @@ app.get('/health', async (req, res) => {
96
115
  app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
97
116
  customCss: '.swagger-ui .topbar { display: none }',
98
117
  }));<% } %>
118
+ <% if (hasGraphQL) { %>
119
+ // GraphQL setup (async — call initGraphQL() before starting server)
120
+ let apolloServer;
121
+
122
+ const initGraphQL = async () => {
123
+ apolloServer = new ApolloServer({ typeDefs, resolvers });
124
+ await apolloServer.start();
125
+
126
+ app.use('/graphql', expressMiddleware(apolloServer, {
127
+ context: async ({ req }) => {
128
+ <% if (hasAuth) { %> // Extract user from JWT if present
129
+ const authHeader = req.headers.authorization;
130
+ if (authHeader) {
131
+ const token = authHeader.split(' ')[1];
132
+ try {
133
+ const user = authService.verifyToken(token);
134
+ return { user };
135
+ } catch {
136
+ return { user: null };
137
+ }
138
+ }
139
+ <% } %> return { user: null };
140
+ },
141
+ }));
142
+ };
99
143
 
144
+ app.initGraphQL = initGraphQL;
145
+ <% } %>
100
146
  // API routes
101
- app.use('/api', routes);
147
+ <% if (hasApiVersioning) { %>app.use('/api/v1', routes);
148
+ // Future versions: app.use('/api/v2', v2Routes);
149
+ <% } else { %>app.use('/api', routes);
150
+ <% } %>
102
151
 
103
152
  // 404 handler
104
153
  app.use('*', (req, res) => {
@@ -11,6 +11,23 @@ JWT_REFRESH_EXPIRE=7d
11
11
  <% } %><% if (hasRedis) { %>
12
12
  # Redis (token blacklist store)
13
13
  REDIS_URL=redis://localhost:6379
14
+ <% } %><% if (hasBackgroundJobs && !hasRedis) { %>
15
+ # Redis (required for BullMQ)
16
+ REDIS_HOST=localhost
17
+ REDIS_PORT=6379
18
+ <% } %><% if (hasEmail) { %>
19
+ # Email (SMTP)
20
+ SMTP_HOST=smtp.ethereal.email
21
+ SMTP_PORT=587
22
+ SMTP_SECURE=false
23
+ SMTP_USER=
24
+ SMTP_PASS=
25
+ SMTP_FROM="App" <noreply@example.com>
26
+ FRONTEND_URL=http://localhost:3000
27
+ <% } %><% if (hasFileUpload) { %>
28
+ # File Uploads
29
+ UPLOAD_DIR=uploads
30
+ UPLOAD_MAX_SIZE=5242880
14
31
  <% } %><% if (db === 'mongodb') { %>
15
32
  # Database
16
33
  MONGO_URI=mongodb://localhost:27017/<%= projectName %>
@@ -11,6 +11,23 @@ JWT_REFRESH_EXPIRE=7d
11
11
  <% } %><% if (hasRedis) { %>
12
12
  # Redis (token blacklist store)
13
13
  REDIS_URL=redis://localhost:6379
14
+ <% } %><% if (hasBackgroundJobs && !hasRedis) { %>
15
+ # Redis (required for BullMQ)
16
+ REDIS_HOST=localhost
17
+ REDIS_PORT=6379
18
+ <% } %><% if (hasEmail) { %>
19
+ # Email (SMTP)
20
+ SMTP_HOST=smtp.ethereal.email
21
+ SMTP_PORT=587
22
+ SMTP_SECURE=false
23
+ SMTP_USER=
24
+ SMTP_PASS=
25
+ SMTP_FROM="App" <noreply@example.com>
26
+ FRONTEND_URL=http://localhost:3000
27
+ <% } %><% if (hasFileUpload) { %>
28
+ # File Uploads
29
+ UPLOAD_DIR=uploads
30
+ UPLOAD_MAX_SIZE=5242880
14
31
  <% } %><% if (db === 'mongodb') { %>
15
32
  # Database
16
33
  MONGO_URI=mongodb://localhost:27017/<%= projectName %>
@@ -39,7 +39,14 @@
39
39
  "@prisma/client": "^5.22.0"<% } %><% if (hasWebsocket) { %>,
40
40
  "socket.io": "^4.8.0"<% } %><% if (hasRedis) { %>,
41
41
  "ioredis": "^5.4.0"<% } %><% if (hasRequestId) { %>,
42
- "uuid": "^10.0.0"<% } %><% if (logger === 'winston') { %>,
42
+ "uuid": "^10.0.0"<% } %><% if (hasEmail) { %>,
43
+ "nodemailer": "^6.9.0"<% } %><% if (hasFileUpload) { %>,
44
+ "multer": "^1.4.5-lts.1"<% } %><% if (hasMetrics) { %>,
45
+ "prom-client": "^15.1.0"<% } %><% if (hasGraphQL) { %>,
46
+ "@apollo/server": "^4.11.0",
47
+ "graphql": "^16.9.0",
48
+ "graphql-tag": "^2.12.6"<% } %><% if (hasBackgroundJobs) { %>,
49
+ "bullmq": "^5.12.0"<% } %><% if (logger === 'winston') { %>,
43
50
  "winston": "^3.15.0"<% } %><% if (logger === 'pino') { %>,
44
51
  "pino": "^9.5.0",
45
52
  "pino-pretty": "^13.0.0"<% } %>
@@ -6,7 +6,8 @@ const os = require('os');
6
6
  const app = require('./app');<% if (hasDatabase) { %>
7
7
  const db = require('./config/database');<% } %><% if (hasRedis) { %>
8
8
  const { connectRedis } = require('./config/redis');<% } %><% if (hasWebsocket) { %>
9
- const { setupWebSocket } = require('./config/websocket');<% } %>
9
+ const { setupWebSocket } = require('./config/websocket');<% } %><% if (hasBackgroundJobs) { %>
10
+ const { startWorkers } = require('./jobs/worker');<% } %>
10
11
  const { createLogger } = require('./utils/logger');
11
12
 
12
13
  const logger = createLogger('Server');
@@ -28,7 +29,9 @@ const startServer = async () => {
28
29
  });
29
30
  } else {<% if (hasDatabase) { %>
30
31
  await db.connect();<% } %><% if (hasRedis) { %>
31
- await connectRedis();<% } %>
32
+ await connectRedis();<% } %><% if (hasGraphQL) { %>
33
+ await app.initGraphQL();<% } %><% if (hasBackgroundJobs) { %>
34
+ startWorkers();<% } %>
32
35
 
33
36
  <% if (hasWebsocket) { %> const httpServer = http.createServer(app);
34
37
  const io = setupWebSocket(httpServer);
@@ -0,0 +1,61 @@
1
+ <% if (hasAuth) { %>const bcrypt = require('bcryptjs');
2
+ const authService = require('../services/authService');
3
+ const userService = require('../services/userService');
4
+ <% } %>
5
+ const resolvers = {
6
+ Query: {
7
+ hello: () => 'Hello from GraphQL!',
8
+ <% if (hasAuth) { %>
9
+ me: async (_, __, { user }) => {
10
+ if (!user) throw new Error('Authentication required');
11
+ return userService.findById(user.userId);
12
+ },
13
+
14
+ users: async (_, { page = 1, limit = 20 }, { user }) => {
15
+ if (!user || user.role !== 'admin') throw new Error('Admin access required');
16
+ return userService.findAll({ page, limit });
17
+ },
18
+ <% } %> },
19
+ <% if (hasAuth) { %>
20
+ Mutation: {
21
+ register: async (_, { input }) => {
22
+ const { username, email, password } = input;
23
+
24
+ const existing = await userService.findByEmail(email);
25
+ if (existing) throw new Error('User already exists with this email');
26
+
27
+ const hashedPassword = await bcrypt.hash(password, 12);
28
+ const user = await userService.create({ username, email, password: hashedPassword });
29
+ const tokens = authService.generateTokens(user);
30
+
31
+ return {
32
+ user: { id: user.id, username: user.username, email: user.email, role: user.role || 'user', createdAt: user.createdAt },
33
+ ...tokens,
34
+ };
35
+ },
36
+
37
+ login: async (_, { input }) => {
38
+ const { email, password } = input;
39
+
40
+ const user = await userService.findByEmail(email);
41
+ if (!user) throw new Error('Invalid credentials');
42
+
43
+ const valid = await bcrypt.compare(password, user.password);
44
+ if (!valid) throw new Error('Invalid credentials');
45
+
46
+ const tokens = authService.generateTokens(user);
47
+
48
+ return {
49
+ user: { id: user.id, username: user.username, email: user.email, role: user.role || 'user', createdAt: user.createdAt },
50
+ ...tokens,
51
+ };
52
+ },
53
+
54
+ updateProfile: async (_, { input }, { user }) => {
55
+ if (!user) throw new Error('Authentication required');
56
+ return userService.updateById(user.userId, input);
57
+ },
58
+ },
59
+ <% } %>};
60
+
61
+ module.exports = resolvers;