create-xpressforge 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/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # create-xpressforge
2
+
3
+ > Production-ready Node.js + Express project scaffolder
4
+
5
+ [![npm version](https://img.shields.io/npm/v/create-xpressforge)](https://npmjs.com/package/create-xpressforge)
6
+ [![license](https://img.shields.io/npm/l/create-xpressforge)](LICENSE)
7
+
8
+ ## Usage
9
+
10
+ ```bash
11
+ npx create-xpressforge my-app
12
+ ```
13
+
14
+ Or install globally:
15
+
16
+ ```bash
17
+ npm install -g create-xpressforge
18
+ create-xpressforge my-app
19
+ ```
20
+
21
+ ## What you get
22
+
23
+ Interactive prompts let you choose:
24
+
25
+ - **Structure** — MVC, Modular (feature-based), or Layered (controller/service/repository)
26
+ - **Database** — MongoDB (Mongoose), PostgreSQL or MySQL (Prisma), or none
27
+ - **Auth** — JWT with refresh tokens, Session, or none
28
+ - **Extras** — Rate limiting, Helmet, CORS, Morgan, Validation, Multer, Socket.io, Swagger
29
+ - **Language** — JavaScript (ES Modules) or TypeScript
30
+
31
+ Every generated project includes:
32
+
33
+ - Global error handler with Mongoose/Prisma/JWT error detection
34
+ - Consistent `apiResponse` helper (`sendSuccess`, `sendError`, `sendPaginated`)
35
+ - Custom logger utility
36
+ - 404 not-found middleware
37
+ - Working User CRUD example
38
+ - `.env` + `.env.example` with all variables listed
39
+ - Auto-generated README with your stack details
40
+
41
+ ## Author
42
+
43
+ Hammad Sadi <hammad.sadi@yahoo.com>
44
+
45
+ ## License
46
+
47
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/index.js';
4
+
5
+ run();
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "create-xpressforge",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready Node.js + Express project scaffolder — MVC, Modular, Layered structures with DB, Auth & more",
5
+ "author": "Hammad Sadi <hammad.sadi@yahoo.com>",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "create-xpressforge": "./bin/cli.js"
9
+ },
10
+ "main": "./src/index.js",
11
+ "keywords": [
12
+ "express",
13
+ "nodejs",
14
+ "scaffold",
15
+ "cli",
16
+ "mvc",
17
+ "modular",
18
+ "boilerplate",
19
+ "generator",
20
+ "production",
21
+ "mongodb",
22
+ "postgresql",
23
+ "jwt"
24
+ ],
25
+ "files": [
26
+ "bin",
27
+ "src"
28
+ ],
29
+ "engines": {
30
+ "node": ">=16.0.0"
31
+ },
32
+ "dependencies": {
33
+ "@inquirer/prompts": "^5.0.0",
34
+ "chalk": "^5.3.0",
35
+ "ora": "^8.0.1",
36
+ "fs-extra": "^11.2.0",
37
+ "gradient-string": "^2.0.2"
38
+ },
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/hammadsadi/create-xpressforge"
42
+ },
43
+ "homepage": "https://github.com/hammadsadi/create-xpressforge#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/hammadsadi/create-xpressforge/issues"
46
+ }
47
+ }
@@ -0,0 +1,65 @@
1
+ export function generateAppJs(answers) {
2
+ const { extras, database, auth, structure } = answers;
3
+
4
+ const imports = [
5
+ `import express from 'express';`,
6
+ extras.includes('cors') ? `import cors from 'cors';` : '',
7
+ extras.includes('helmet') ? `import helmet from 'helmet';` : '',
8
+ extras.includes('morgan') ? `import morgan from 'morgan';` : '',
9
+ extras.includes('rateLimit') ? `import rateLimit from 'express-rate-limit';` : '',
10
+ extras.includes('swagger') ? `import swaggerUi from 'swagger-ui-express';\nimport { swaggerSpec } from './config/swagger.js';` : '',
11
+ database !== 'none' ? `import { connectDB } from './config/db.js';` : '',
12
+ `import { errorHandler } from './middlewares/errorHandler.js';`,
13
+ `import { notFound } from './middlewares/notFound.js';`,
14
+ structure === 'mvc' ? `import routes from './routes/index.js';` : '',
15
+ structure === 'modular' ? `import routes from './routes/index.js';` : '',
16
+ structure === 'layered' ? `import routes from './routes/index.js';` : '',
17
+ ].filter(Boolean).join('\n');
18
+
19
+ const dbInit = database !== 'none' ? `\n// Connect to database\nconnectDB();\n` : '';
20
+
21
+ const rateLimitMiddleware = extras.includes('rateLimit') ? `
22
+ const limiter = rateLimit({
23
+ windowMs: 15 * 60 * 1000, // 15 minutes
24
+ max: 100,
25
+ message: { success: false, message: 'Too many requests, please try again later' },
26
+ standardHeaders: true,
27
+ legacyHeaders: false,
28
+ });
29
+ app.use('/api', limiter);
30
+ ` : '';
31
+
32
+ const middlewares = [
33
+ extras.includes('helmet') ? `app.use(helmet());` : '',
34
+ extras.includes('cors') ? `app.use(cors({ origin: process.env.CLIENT_URL || '*', credentials: true }));` : '',
35
+ `app.use(express.json({ limit: '10mb' }));`,
36
+ `app.use(express.urlencoded({ extended: true, limit: '10mb' }));`,
37
+ extras.includes('morgan') ? `app.use(morgan(process.env.NODE_ENV === 'development' ? 'dev' : 'combined'));` : '',
38
+ ].filter(Boolean).join('\n');
39
+
40
+ const swaggerSetup = extras.includes('swagger') ? `\n// API Documentation\napp.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));\n` : '';
41
+
42
+ const healthCheck = `
43
+ // Health check
44
+ app.get('/health', (req, res) => {
45
+ res.json({ success: true, message: 'Server is healthy', timestamp: new Date().toISOString() });
46
+ });
47
+ `;
48
+
49
+ return `${imports}
50
+ ${dbInit}
51
+ const app = express();
52
+ ${rateLimitMiddleware}
53
+ // Middlewares
54
+ ${middlewares}
55
+ ${swaggerSetup}${healthCheck}
56
+ // Routes
57
+ app.use('/api/v1', routes);
58
+
59
+ // Error handlers
60
+ app.use(notFound);
61
+ app.use(errorHandler);
62
+
63
+ export default app;
64
+ `;
65
+ }
@@ -0,0 +1,210 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ export async function generateAuthFiles(answers, targetDir, ext) {
5
+ const { auth, database } = answers;
6
+
7
+ if (auth === 'jwt') {
8
+ // JWT middleware
9
+ await fs.writeFile(
10
+ path.join(targetDir, 'src', 'middlewares', `authenticate.${ext}`),
11
+ jwtMiddleware()
12
+ );
13
+
14
+ // Auth controller
15
+ await fs.ensureDir(path.join(targetDir, 'src', 'controllers'));
16
+ await fs.writeFile(
17
+ path.join(targetDir, 'src', 'controllers', `authController.${ext}`),
18
+ authController(database)
19
+ );
20
+
21
+ // Auth routes
22
+ await fs.writeFile(
23
+ path.join(targetDir, 'src', 'routes', `authRoutes.${ext}`),
24
+ authRoutes()
25
+ );
26
+
27
+ // User model (if DB)
28
+ if (database === 'mongodb') {
29
+ await fs.writeFile(
30
+ path.join(targetDir, 'src', 'models', `User.${ext}`),
31
+ userModelMongo()
32
+ );
33
+ }
34
+ }
35
+
36
+ if (auth === 'session') {
37
+ await fs.writeFile(
38
+ path.join(targetDir, 'src', 'middlewares', `sessionAuth.${ext}`),
39
+ sessionMiddleware()
40
+ );
41
+ }
42
+ }
43
+
44
+ function jwtMiddleware() {
45
+ return `import jwt from 'jsonwebtoken';
46
+ import { env } from '../config/env.js';
47
+
48
+ export const authenticate = (req, res, next) => {
49
+ const authHeader = req.headers.authorization;
50
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
51
+ return res.status(401).json({ success: false, message: 'No token provided' });
52
+ }
53
+
54
+ const token = authHeader.split(' ')[1];
55
+ try {
56
+ const decoded = jwt.verify(token, env.JWT_SECRET);
57
+ req.user = decoded;
58
+ next();
59
+ } catch (err) {
60
+ return res.status(401).json({ success: false, message: err.name === 'TokenExpiredError' ? 'Token expired' : 'Invalid token' });
61
+ }
62
+ };
63
+
64
+ export const authorize = (...roles) => (req, res, next) => {
65
+ if (!roles.includes(req.user?.role)) {
66
+ return res.status(403).json({ success: false, message: 'Access denied' });
67
+ }
68
+ next();
69
+ };
70
+ `;
71
+ }
72
+
73
+ function authController(database) {
74
+ const userImport = database === 'mongodb'
75
+ ? `import User from '../models/User.js';`
76
+ : `import { prisma } from '../config/db.js';`;
77
+
78
+ return `import jwt from 'jsonwebtoken';
79
+ import bcrypt from 'bcryptjs';
80
+ ${userImport}
81
+ import { env } from '../config/env.js';
82
+ import { sendSuccess, sendError } from '../utils/apiResponse.js';
83
+
84
+ const signToken = (payload, secret, expiresIn) =>
85
+ jwt.sign(payload, secret, { expiresIn });
86
+
87
+ export const register = async (req, res, next) => {
88
+ try {
89
+ const { name, email, password } = req.body;
90
+
91
+ const existing = await ${database === 'mongodb' ? 'User.findOne({ email })' : 'prisma.user.findUnique({ where: { email } })'};
92
+ if (existing) return sendError(res, 'Email already registered', 409);
93
+
94
+ const hashed = await bcrypt.hash(password, 12);
95
+ const user = await ${database === 'mongodb'
96
+ ? 'User.create({ name, email, password: hashed })'
97
+ : 'prisma.user.create({ data: { name, email, password: hashed } })'};
98
+
99
+ const accessToken = signToken({ id: user._id || user.id, role: user.role }, env.JWT_SECRET, env.JWT_EXPIRES_IN);
100
+ const refreshToken = signToken({ id: user._id || user.id }, env.JWT_REFRESH_SECRET, env.JWT_REFRESH_EXPIRES_IN);
101
+
102
+ sendSuccess(res, { accessToken, refreshToken, user: { id: user._id || user.id, name: user.name, email: user.email } }, 'Registered successfully', 201);
103
+ } catch (err) { next(err); }
104
+ };
105
+
106
+ export const login = async (req, res, next) => {
107
+ try {
108
+ const { email, password } = req.body;
109
+
110
+ const user = await ${database === 'mongodb'
111
+ ? 'User.findOne({ email }).select(\'+password\')'
112
+ : 'prisma.user.findUnique({ where: { email } })'};
113
+ if (!user || !(await bcrypt.compare(password, user.password))) {
114
+ return sendError(res, 'Invalid email or password', 401);
115
+ }
116
+
117
+ const accessToken = signToken({ id: user._id || user.id, role: user.role }, env.JWT_SECRET, env.JWT_EXPIRES_IN);
118
+ const refreshToken = signToken({ id: user._id || user.id }, env.JWT_REFRESH_SECRET, env.JWT_REFRESH_EXPIRES_IN);
119
+
120
+ sendSuccess(res, { accessToken, refreshToken, user: { id: user._id || user.id, name: user.name, email: user.email } }, 'Login successful');
121
+ } catch (err) { next(err); }
122
+ };
123
+
124
+ export const refreshToken = async (req, res, next) => {
125
+ try {
126
+ const { token } = req.body;
127
+ if (!token) return sendError(res, 'Refresh token required', 401);
128
+
129
+ const decoded = jwt.verify(token, env.JWT_REFRESH_SECRET);
130
+ const accessToken = signToken({ id: decoded.id }, env.JWT_SECRET, env.JWT_EXPIRES_IN);
131
+ sendSuccess(res, { accessToken }, 'Token refreshed');
132
+ } catch (err) { next(err); }
133
+ };
134
+
135
+ export const getMe = async (req, res, next) => {
136
+ try {
137
+ const user = await ${database === 'mongodb'
138
+ ? 'User.findById(req.user.id).select(\'-password\')'
139
+ : 'prisma.user.findUnique({ where: { id: req.user.id }, select: { id: true, name: true, email: true, role: true, createdAt: true } })'};
140
+ if (!user) return sendError(res, 'User not found', 404);
141
+ sendSuccess(res, user);
142
+ } catch (err) { next(err); }
143
+ };
144
+ `;
145
+ }
146
+
147
+ function authRoutes() {
148
+ return `import { Router } from 'express';
149
+ import { register, login, refreshToken, getMe } from '../controllers/authController.js';
150
+ import { authenticate } from '../middlewares/authenticate.js';
151
+
152
+ const router = Router();
153
+
154
+ router.post('/register', register);
155
+ router.post('/login', login);
156
+ router.post('/refresh', refreshToken);
157
+ router.get('/me', authenticate, getMe);
158
+
159
+ export default router;
160
+ `;
161
+ }
162
+
163
+ function userModelMongo() {
164
+ return `import mongoose from 'mongoose';
165
+
166
+ const userSchema = new mongoose.Schema({
167
+ name: {
168
+ type: String,
169
+ required: [true, 'Name is required'],
170
+ trim: true,
171
+ minlength: 2,
172
+ maxlength: 50,
173
+ },
174
+ email: {
175
+ type: String,
176
+ required: [true, 'Email is required'],
177
+ unique: true,
178
+ lowercase: true,
179
+ trim: true,
180
+ match: [/^\\S+@\\S+\\.\\S+$/, 'Invalid email format'],
181
+ },
182
+ password: {
183
+ type: String,
184
+ required: [true, 'Password is required'],
185
+ minlength: 6,
186
+ select: false,
187
+ },
188
+ role: {
189
+ type: String,
190
+ enum: ['user', 'admin'],
191
+ default: 'user',
192
+ },
193
+ }, { timestamps: true });
194
+
195
+ userSchema.index({ email: 1 });
196
+
197
+ const User = mongoose.model('User', userSchema);
198
+ export default User;
199
+ `;
200
+ }
201
+
202
+ function sessionMiddleware() {
203
+ return `export const requireAuth = (req, res, next) => {
204
+ if (!req.session?.userId) {
205
+ return res.status(401).json({ success: false, message: 'Not authenticated' });
206
+ }
207
+ next();
208
+ };
209
+ `;
210
+ }
@@ -0,0 +1,75 @@
1
+ export function generateDbConfig(answers) {
2
+ const { database } = answers;
3
+
4
+ if (database === 'mongodb') {
5
+ return `import mongoose from 'mongoose';
6
+ import { logger } from '../utils/logger.js';
7
+
8
+ const MAX_RETRIES = 5;
9
+ let retries = 0;
10
+
11
+ export const connectDB = async () => {
12
+ try {
13
+ const conn = await mongoose.connect(process.env.MONGO_URI, {
14
+ serverSelectionTimeoutMS: 5000,
15
+ });
16
+ logger.info(\`MongoDB connected: \${conn.connection.host}\`);
17
+ retries = 0;
18
+ } catch (err) {
19
+ retries++;
20
+ logger.error(\`MongoDB connection failed (attempt \${retries}/\${MAX_RETRIES}): \${err.message}\`);
21
+ if (retries < MAX_RETRIES) {
22
+ logger.info(\`Retrying in 5 seconds...\`);
23
+ setTimeout(connectDB, 5000);
24
+ } else {
25
+ logger.error('Max retries reached. Exiting...');
26
+ process.exit(1);
27
+ }
28
+ }
29
+ };
30
+
31
+ mongoose.connection.on('disconnected', () => {
32
+ logger.warn('MongoDB disconnected. Attempting reconnect...');
33
+ connectDB();
34
+ });
35
+
36
+ process.on('SIGINT', async () => {
37
+ await mongoose.connection.close();
38
+ logger.info('MongoDB connection closed due to app termination');
39
+ process.exit(0);
40
+ });
41
+ `;
42
+ }
43
+
44
+ if (database === 'postgresql' || database === 'mysql') {
45
+ return `import { PrismaClient } from '@prisma/client';
46
+ import { logger } from '../utils/logger.js';
47
+
48
+ const globalForPrisma = globalThis;
49
+
50
+ export const prisma = globalForPrisma.prisma ?? new PrismaClient({
51
+ log: process.env.NODE_ENV === 'development'
52
+ ? ['query', 'info', 'warn', 'error']
53
+ : ['error'],
54
+ });
55
+
56
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
57
+
58
+ export const connectDB = async () => {
59
+ try {
60
+ await prisma.$connect();
61
+ logger.info('Database connected via Prisma');
62
+ } catch (err) {
63
+ logger.error('Database connection failed: ' + err.message);
64
+ process.exit(1);
65
+ }
66
+ };
67
+
68
+ process.on('beforeExit', async () => {
69
+ await prisma.$disconnect();
70
+ });
71
+ `;
72
+ }
73
+
74
+ return '';
75
+ }
@@ -0,0 +1,59 @@
1
+ export function generateEnv(answers) {
2
+ const { database, auth, extras, projectName } = answers;
3
+
4
+ let content = `# App
5
+ NODE_ENV=development
6
+ PORT=3000
7
+ CLIENT_URL=http://localhost:3000
8
+
9
+ `;
10
+
11
+ if (database === 'mongodb') {
12
+ content += `# MongoDB
13
+ MONGO_URI=mongodb://localhost:27017/${projectName}
14
+
15
+ `;
16
+ }
17
+
18
+ if (database === 'postgresql') {
19
+ content += `# PostgreSQL (Prisma)
20
+ DATABASE_URL=postgresql://user:password@localhost:5432/${projectName}?schema=public
21
+
22
+ `;
23
+ }
24
+
25
+ if (database === 'mysql') {
26
+ content += `# MySQL (Prisma)
27
+ DATABASE_URL=mysql://user:password@localhost:3306/${projectName}
28
+
29
+ `;
30
+ }
31
+
32
+ if (auth === 'jwt') {
33
+ content += `# JWT
34
+ JWT_SECRET=your_super_secret_jwt_key_change_this_in_production
35
+ JWT_EXPIRES_IN=7d
36
+ JWT_REFRESH_SECRET=your_super_secret_refresh_key_change_this_too
37
+ JWT_REFRESH_EXPIRES_IN=30d
38
+
39
+ `;
40
+ }
41
+
42
+ if (auth === 'session') {
43
+ content += `# Session
44
+ SESSION_SECRET=your_session_secret_change_this_in_production
45
+ SESSION_MAX_AGE=86400000
46
+
47
+ `;
48
+ }
49
+
50
+ if (extras.includes('multer')) {
51
+ content += `# File Upload
52
+ MAX_FILE_SIZE=5242880
53
+ UPLOAD_DIR=uploads
54
+
55
+ `;
56
+ }
57
+
58
+ return content.trimEnd() + '\n';
59
+ }
@@ -0,0 +1,70 @@
1
+ export function generatePackageJson(answers) {
2
+ const { projectName, database, auth, extras, language } = answers;
3
+
4
+ const deps = {
5
+ express: '^5.0.0',
6
+ };
7
+
8
+ const devDeps = {
9
+ nodemon: '^3.1.0',
10
+ };
11
+
12
+ if (database === 'mongodb') deps['mongoose'] = '^8.0.0';
13
+ if (database === 'postgresql' || database === 'mysql') {
14
+ deps['@prisma/client'] = '^5.0.0';
15
+ devDeps['prisma'] = '^5.0.0';
16
+ }
17
+
18
+ if (auth === 'jwt') { deps['jsonwebtoken'] = '^9.0.0'; deps['bcryptjs'] = '^2.4.3'; }
19
+ if (auth === 'session') { deps['express-session'] = '^1.18.0'; deps['connect-mongo'] = '^5.1.0'; }
20
+
21
+ if (extras.includes('cors')) deps['cors'] = '^2.8.5';
22
+ if (extras.includes('helmet')) deps['helmet'] = '^7.1.0';
23
+ if (extras.includes('morgan')) deps['morgan'] = '^1.10.0';
24
+ if (extras.includes('rateLimit')) deps['express-rate-limit'] = '^7.0.0';
25
+ if (extras.includes('validation')) deps['express-validator'] = '^7.0.0';
26
+ if (extras.includes('multer')) deps['multer'] = '^1.4.5';
27
+ if (extras.includes('socket')) deps['socket.io'] = '^4.7.0';
28
+ if (extras.includes('swagger')) {
29
+ deps['swagger-ui-express'] = '^5.0.0';
30
+ deps['swagger-jsdoc'] = '^6.2.8';
31
+ }
32
+
33
+ deps['dotenv'] = '^16.4.0';
34
+
35
+ if (language === 'ts') {
36
+ devDeps['typescript'] = '^5.4.0';
37
+ devDeps['tsx'] = '^4.7.0';
38
+ devDeps['@types/node'] = '^20.0.0';
39
+ devDeps['@types/express'] = '^5.0.0';
40
+ if (deps['cors']) devDeps['@types/cors'] = '^2.8.17';
41
+ if (deps['morgan']) devDeps['@types/morgan'] = '^1.9.9';
42
+ if (deps['bcryptjs']) devDeps['@types/bcryptjs'] = '^2.4.6';
43
+ if (deps['jsonwebtoken']) devDeps['@types/jsonwebtoken'] = '^9.0.6';
44
+ if (deps['multer']) devDeps['@types/multer'] = '^1.4.11';
45
+ }
46
+
47
+ const scripts = language === 'ts'
48
+ ? {
49
+ dev: 'tsx watch server.ts',
50
+ build: 'tsc',
51
+ start: 'node dist/server.js',
52
+ lint: 'tsc --noEmit',
53
+ }
54
+ : {
55
+ dev: 'nodemon server.js',
56
+ start: 'node server.js',
57
+ };
58
+
59
+ return {
60
+ name: projectName,
61
+ version: '1.0.0',
62
+ description: '',
63
+ type: 'module',
64
+ main: language === 'ts' ? 'dist/server.js' : 'server.js',
65
+ scripts,
66
+ dependencies: deps,
67
+ devDependencies: devDeps,
68
+ engines: { node: '>=18.0.0' },
69
+ };
70
+ }
@@ -0,0 +1,275 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { generatePackageJson } from './packageJsonGenerator.js';
6
+ import { generateEnv } from './envGenerator.js';
7
+ import { generateAppJs } from './appGenerator.js';
8
+ import { generateDbConfig } from './dbGenerator.js';
9
+ import { generateAuthFiles } from './authGenerator.js';
10
+ import { generateStructure } from './structureGenerator.js';
11
+ import { generateReadme } from './readmeGenerator.js';
12
+
13
+ export async function generateProject(answers, targetDir) {
14
+ const spinner = ora({ text: 'Creating project...', color: 'cyan' }).start();
15
+
16
+ try {
17
+ // 1. Check if directory exists
18
+ if (await fs.pathExists(targetDir)) {
19
+ spinner.fail(chalk.red(`Directory "${answers.projectName}" already exists!`));
20
+ process.exit(1);
21
+ }
22
+
23
+ await fs.ensureDir(targetDir);
24
+ spinner.text = 'Scaffolding folder structure...';
25
+
26
+ // 2. Generate folder structure based on chosen pattern
27
+ await generateStructure(answers, targetDir);
28
+ spinner.text = 'Writing configuration files...';
29
+
30
+ // 3. package.json
31
+ const pkg = generatePackageJson(answers);
32
+ await fs.writeJson(path.join(targetDir, 'package.json'), pkg, { spaces: 2 });
33
+
34
+ // 4. .env + .env.example
35
+ const envContent = generateEnv(answers);
36
+ await fs.writeFile(path.join(targetDir, '.env'), envContent);
37
+ await fs.writeFile(path.join(targetDir, '.env.example'), envContent.replace(/=.+/gm, '='));
38
+
39
+ // 5. .gitignore
40
+ await fs.writeFile(path.join(targetDir, '.gitignore'), gitignoreContent());
41
+
42
+ // 6. app.js / app.ts
43
+ const appContent = generateAppJs(answers);
44
+ const ext = answers.language === 'ts' ? 'ts' : 'js';
45
+ await fs.writeFile(path.join(targetDir, 'src', `app.${ext}`), appContent);
46
+
47
+ // 7. server.js entry
48
+ await fs.writeFile(path.join(targetDir, `server.${ext}`), serverContent(answers));
49
+
50
+ // 8. DB config
51
+ if (answers.database !== 'none') {
52
+ spinner.text = 'Setting up database connection...';
53
+ const dbContent = generateDbConfig(answers);
54
+ await fs.writeFile(path.join(targetDir, 'src', 'config', `db.${ext}`), dbContent);
55
+ }
56
+
57
+ // 9. env config
58
+ await fs.writeFile(
59
+ path.join(targetDir, 'src', 'config', `env.${ext}`),
60
+ envConfigContent(answers)
61
+ );
62
+
63
+ // 10. Auth files
64
+ if (answers.auth !== 'none') {
65
+ spinner.text = 'Generating auth files...';
66
+ await generateAuthFiles(answers, targetDir, ext);
67
+ }
68
+
69
+ // 11. Utils
70
+ await fs.writeFile(
71
+ path.join(targetDir, 'src', 'utils', `apiResponse.${ext}`),
72
+ apiResponseContent()
73
+ );
74
+ await fs.writeFile(
75
+ path.join(targetDir, 'src', 'utils', `logger.${ext}`),
76
+ loggerContent()
77
+ );
78
+ await fs.writeFile(
79
+ path.join(targetDir, 'src', 'middlewares', `errorHandler.${ext}`),
80
+ errorHandlerContent()
81
+ );
82
+ await fs.writeFile(
83
+ path.join(targetDir, 'src', 'middlewares', `notFound.${ext}`),
84
+ notFoundContent()
85
+ );
86
+
87
+ // 12. TypeScript config
88
+ if (answers.language === 'ts') {
89
+ await fs.writeJson(path.join(targetDir, 'tsconfig.json'), tsConfig(), { spaces: 2 });
90
+ }
91
+
92
+ // 13. README
93
+ spinner.text = 'Generating README...';
94
+ const readme = generateReadme(answers);
95
+ await fs.writeFile(path.join(targetDir, 'README.md'), readme);
96
+
97
+ spinner.succeed(chalk.green('Project created successfully!'));
98
+
99
+ // Success message
100
+ console.log('\n' + chalk.bold(' Next steps:\n'));
101
+ console.log(chalk.cyan(` cd ${answers.projectName}`));
102
+ console.log(chalk.cyan(' npm install'));
103
+ if (answers.database === 'postgresql' || answers.database === 'mysql') {
104
+ console.log(chalk.cyan(' npx prisma migrate dev'));
105
+ }
106
+ console.log(chalk.cyan(' cp .env.example .env # fill in your values'));
107
+ console.log(chalk.cyan(' npm run dev\n'));
108
+ console.log(chalk.dim(' Happy building! — create-xpressforge by Hammad Sadi\n'));
109
+
110
+ } catch (err) {
111
+ spinner.fail(chalk.red('Failed to create project'));
112
+ await fs.remove(targetDir).catch(() => {});
113
+ throw err;
114
+ }
115
+ }
116
+
117
+ function gitignoreContent() {
118
+ return `node_modules/
119
+ .env
120
+ dist/
121
+ build/
122
+ *.log
123
+ .DS_Store
124
+ coverage/
125
+ .nyc_output/
126
+ `;
127
+ }
128
+
129
+ function serverContent(answers) {
130
+ const ext = answers.language === 'ts' ? "import app from './src/app.js';" : "import app from './src/app.js';";
131
+ return `${ext}
132
+ import { env } from './src/config/env.js';
133
+
134
+ const PORT = env.PORT || 3000;
135
+
136
+ app.listen(PORT, () => {
137
+ console.log(\`Server running on http://localhost:\${PORT}\`);
138
+ console.log(\`Environment: \${env.NODE_ENV}\`);
139
+ });
140
+ `;
141
+ }
142
+
143
+ function envConfigContent(answers) {
144
+ return `export const env = {
145
+ PORT: process.env.PORT || 3000,
146
+ NODE_ENV: process.env.NODE_ENV || 'development',
147
+ ${answers.database === 'mongodb' ? ` MONGO_URI: process.env.MONGO_URI,\n` : ''}${answers.database === 'postgresql' || answers.database === 'mysql' ? ` DATABASE_URL: process.env.DATABASE_URL,\n` : ''}${answers.auth === 'jwt' ? ` JWT_SECRET: process.env.JWT_SECRET,\n JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '7d',\n JWT_REFRESH_SECRET: process.env.JWT_REFRESH_SECRET,\n JWT_REFRESH_EXPIRES_IN: process.env.JWT_REFRESH_EXPIRES_IN || '30d',\n` : ''}};
148
+ `;
149
+ }
150
+
151
+ function apiResponseContent() {
152
+ return `/**
153
+ * Consistent API response helper
154
+ */
155
+ export const sendSuccess = (res, data = null, message = 'Success', statusCode = 200) => {
156
+ return res.status(statusCode).json({
157
+ success: true,
158
+ message,
159
+ data,
160
+ });
161
+ };
162
+
163
+ export const sendError = (res, message = 'Something went wrong', statusCode = 500, errors = null) => {
164
+ return res.status(statusCode).json({
165
+ success: false,
166
+ message,
167
+ ...(errors && { errors }),
168
+ });
169
+ };
170
+
171
+ export const sendPaginated = (res, data, pagination) => {
172
+ return res.status(200).json({
173
+ success: true,
174
+ data,
175
+ pagination: {
176
+ total: pagination.total,
177
+ page: pagination.page,
178
+ limit: pagination.limit,
179
+ totalPages: Math.ceil(pagination.total / pagination.limit),
180
+ },
181
+ });
182
+ };
183
+ `;
184
+ }
185
+
186
+ function loggerContent() {
187
+ return `import { env } from '../config/env.js';
188
+
189
+ const levels = { error: 0, warn: 1, info: 2, debug: 3 };
190
+ const colors = { error: '\\x1b[31m', warn: '\\x1b[33m', info: '\\x1b[36m', debug: '\\x1b[35m', reset: '\\x1b[0m' };
191
+
192
+ const log = (level, message, meta = {}) => {
193
+ if (env.NODE_ENV === 'test') return;
194
+ const timestamp = new Date().toISOString();
195
+ const color = colors[level] || '';
196
+ const metaStr = Object.keys(meta).length ? ' ' + JSON.stringify(meta) : '';
197
+ console.log(\`\${color}[\${timestamp}] [\${level.toUpperCase()}] \${message}\${metaStr}\${colors.reset}\`);
198
+ };
199
+
200
+ export const logger = {
201
+ error: (msg, meta) => log('error', msg, meta),
202
+ warn: (msg, meta) => log('warn', msg, meta),
203
+ info: (msg, meta) => log('info', msg, meta),
204
+ debug: (msg, meta) => log('debug', msg, meta),
205
+ };
206
+ `;
207
+ }
208
+
209
+ function errorHandlerContent() {
210
+ return `import { logger } from '../utils/logger.js';
211
+ import { env } from '../config/env.js';
212
+
213
+ export const errorHandler = (err, req, res, next) => {
214
+ let statusCode = err.statusCode || err.status || 500;
215
+ let message = err.message || 'Internal Server Error';
216
+
217
+ // Mongoose duplicate key
218
+ if (err.code === 11000) {
219
+ statusCode = 409;
220
+ const field = Object.keys(err.keyValue || {})[0];
221
+ message = \`\${field} already exists\`;
222
+ }
223
+
224
+ // Mongoose validation error
225
+ if (err.name === 'ValidationError') {
226
+ statusCode = 422;
227
+ message = Object.values(err.errors).map(e => e.message).join(', ');
228
+ }
229
+
230
+ // JWT errors
231
+ if (err.name === 'JsonWebTokenError') { statusCode = 401; message = 'Invalid token'; }
232
+ if (err.name === 'TokenExpiredError') { statusCode = 401; message = 'Token expired'; }
233
+
234
+ logger.error(message, { statusCode, path: req.path, method: req.method });
235
+
236
+ res.status(statusCode).json({
237
+ success: false,
238
+ message,
239
+ ...(env.NODE_ENV === 'development' && { stack: err.stack }),
240
+ });
241
+ };
242
+ `;
243
+ }
244
+
245
+ function notFoundContent() {
246
+ return `export const notFound = (req, res) => {
247
+ res.status(404).json({
248
+ success: false,
249
+ message: \`Route \${req.method} \${req.originalUrl} not found\`,
250
+ });
251
+ };
252
+ `;
253
+ }
254
+
255
+ function tsConfig() {
256
+ return {
257
+ compilerOptions: {
258
+ target: 'ES2022',
259
+ module: 'ESNext',
260
+ moduleResolution: 'node',
261
+ outDir: './dist',
262
+ rootDir: './',
263
+ strict: true,
264
+ esModuleInterop: true,
265
+ skipLibCheck: true,
266
+ forceConsistentCasingInFileNames: true,
267
+ resolveJsonModule: true,
268
+ declaration: true,
269
+ declarationMap: true,
270
+ sourceMap: true,
271
+ },
272
+ include: ['src/**/*', 'server.ts'],
273
+ exclude: ['node_modules', 'dist'],
274
+ };
275
+ }
@@ -0,0 +1,77 @@
1
+ export function generateReadme(answers) {
2
+ const { projectName, structure, database, auth, extras, language } = answers;
3
+
4
+ const dbSetup = database === 'mongodb'
5
+ ? `Set your \`MONGO_URI\` in \`.env\``
6
+ : database !== 'none'
7
+ ? `Set your \`DATABASE_URL\` in \`.env\`, then run:\n\`\`\`bash\nnpx prisma migrate dev\n\`\`\``
8
+ : '';
9
+
10
+ return `# ${projectName}
11
+
12
+ > Scaffolded with [create-xpressforge](https://github.com/hammad-sadi/create-xpressforge)
13
+
14
+ ## Stack
15
+
16
+ | | |
17
+ |---|---|
18
+ | Runtime | Node.js (ES Modules) |
19
+ | Framework | Express.js v5 |
20
+ | Language | ${language === 'ts' ? 'TypeScript' : 'JavaScript'} |
21
+ | Structure | ${structure.toUpperCase()} |
22
+ | Database | ${database === 'none' ? 'None' : database.charAt(0).toUpperCase() + database.slice(1)} |
23
+ | Auth | ${auth === 'none' ? 'None' : auth.toUpperCase()} |
24
+
25
+ ## Getting started
26
+
27
+ \`\`\`bash
28
+ npm install
29
+ cp .env.example .env # fill in your values
30
+ ${dbSetup}
31
+ npm run dev
32
+ \`\`\`
33
+
34
+ ## Project structure
35
+
36
+ \`\`\`
37
+ src/
38
+ ├── config/ # DB connection, env validation
39
+ ├── controllers/ # Request handlers
40
+ ├── middlewares/ # Auth, error handler, not-found
41
+ ├── models/ # Data models / schemas
42
+ ├── routes/ # Express routers
43
+ ├── services/ # Business logic
44
+ └── utils/ # apiResponse, logger
45
+ \`\`\`
46
+
47
+ ## API endpoints
48
+
49
+ | Method | Endpoint | Description | Auth |
50
+ |--------|----------|-------------|------|
51
+ ${auth !== 'none' ? `| POST | /api/v1/auth/register | Register | No |
52
+ | POST | /api/v1/auth/login | Login | No |
53
+ | POST | /api/v1/auth/refresh | Refresh token | No |
54
+ | GET | /api/v1/auth/me | Get current user | Yes |
55
+ ` : ''}| GET | /api/v1/users | List users | ${auth !== 'none' ? 'Yes' : 'No'} |
56
+ | GET | /api/v1/users/:id | Get user | ${auth !== 'none' ? 'Yes' : 'No'} |
57
+ | PUT | /api/v1/users/:id | Update user | ${auth !== 'none' ? 'Yes' : 'No'} |
58
+ | DELETE | /api/v1/users/:id | Delete user | ${auth !== 'none' ? 'Admin' : 'No'} |
59
+ | GET | /health | Health check | No |
60
+
61
+ ## Scripts
62
+
63
+ \`\`\`bash
64
+ npm run dev # development with hot reload
65
+ npm start # production
66
+ ${language === 'ts' ? 'npm run build # compile TypeScript\nnpm run lint # type check' : ''}
67
+ \`\`\`
68
+
69
+ ## Environment variables
70
+
71
+ See \`.env.example\` for all required variables.
72
+
73
+ ---
74
+
75
+ Built with ❤️ by Hammad Sadi — powered by [create-xpressforge](https://npmjs.com/package/create-xpressforge)
76
+ `;
77
+ }
@@ -0,0 +1,265 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+
4
+ export async function generateStructure(answers, targetDir) {
5
+ const { structure, database, auth } = answers;
6
+ const ext = answers.language === 'ts' ? 'ts' : 'js';
7
+
8
+ const base = (p) => path.join(targetDir, p);
9
+
10
+ // Common folders
11
+ const commonDirs = [
12
+ 'src/config',
13
+ 'src/middlewares',
14
+ 'src/utils',
15
+ 'src/routes',
16
+ ];
17
+
18
+ if (structure === 'mvc') {
19
+ const dirs = [
20
+ ...commonDirs,
21
+ 'src/controllers',
22
+ 'src/models',
23
+ 'src/services',
24
+ ];
25
+ await Promise.all(dirs.map(d => fs.ensureDir(base(d))));
26
+ await writeMvcFiles(answers, targetDir, ext);
27
+ }
28
+
29
+ if (structure === 'modular') {
30
+ const dirs = [
31
+ ...commonDirs,
32
+ 'src/modules/user/controller',
33
+ 'src/modules/user/service',
34
+ 'src/modules/user/model',
35
+ 'src/modules/user/routes',
36
+ 'src/modules/user/dto',
37
+ ];
38
+ await Promise.all(dirs.map(d => fs.ensureDir(base(d))));
39
+ await writeModularFiles(answers, targetDir, ext);
40
+ }
41
+
42
+ if (structure === 'layered') {
43
+ const dirs = [
44
+ ...commonDirs,
45
+ 'src/controllers',
46
+ 'src/services',
47
+ 'src/repositories',
48
+ 'src/models',
49
+ ];
50
+ await Promise.all(dirs.map(d => fs.ensureDir(base(d))));
51
+ await writeLayeredFiles(answers, targetDir, ext);
52
+ }
53
+ }
54
+
55
+ async function writeMvcFiles(answers, targetDir, ext) {
56
+ const base = (p) => path.join(targetDir, p);
57
+
58
+ // Example User controller
59
+ await fs.writeFile(base(`src/controllers/userController.${ext}`), `import { sendSuccess, sendError, sendPaginated } from '../utils/apiResponse.js';
60
+ ${answers.database === 'mongodb' ? "import User from '../models/User.js';" : "import { prisma } from '../config/db.js';"}
61
+
62
+ export const getAllUsers = async (req, res, next) => {
63
+ try {
64
+ const page = parseInt(req.query.page) || 1;
65
+ const limit = parseInt(req.query.limit) || 10;
66
+ const skip = (page - 1) * limit;
67
+
68
+ ${answers.database === 'mongodb'
69
+ ? `const [users, total] = await Promise.all([\n User.find().select('-password').skip(skip).limit(limit).lean(),\n User.countDocuments(),\n ]);`
70
+ : `const [users, total] = await Promise.all([\n prisma.user.findMany({ skip, take: limit, select: { id: true, name: true, email: true, role: true, createdAt: true } }),\n prisma.user.count(),\n ]);`}
71
+
72
+ sendPaginated(res, users, { total, page, limit });
73
+ } catch (err) { next(err); }
74
+ };
75
+
76
+ export const getUserById = async (req, res, next) => {
77
+ try {
78
+ const user = await ${answers.database === 'mongodb'
79
+ ? "User.findById(req.params.id).select('-password')"
80
+ : "prisma.user.findUnique({ where: { id: req.params.id }, select: { id: true, name: true, email: true, role: true } })"};
81
+ if (!user) return sendError(res, 'User not found', 404);
82
+ sendSuccess(res, user);
83
+ } catch (err) { next(err); }
84
+ };
85
+
86
+ export const updateUser = async (req, res, next) => {
87
+ try {
88
+ const { name } = req.body;
89
+ const user = await ${answers.database === 'mongodb'
90
+ ? "User.findByIdAndUpdate(req.params.id, { name }, { new: true, runValidators: true }).select('-password')"
91
+ : "prisma.user.update({ where: { id: req.params.id }, data: { name }, select: { id: true, name: true, email: true } })"};
92
+ if (!user) return sendError(res, 'User not found', 404);
93
+ sendSuccess(res, user, 'User updated');
94
+ } catch (err) { next(err); }
95
+ };
96
+
97
+ export const deleteUser = async (req, res, next) => {
98
+ try {
99
+ const user = await ${answers.database === 'mongodb'
100
+ ? "User.findByIdAndDelete(req.params.id)"
101
+ : "prisma.user.delete({ where: { id: req.params.id } })"};
102
+ if (!user) return sendError(res, 'User not found', 404);
103
+ sendSuccess(res, null, 'User deleted');
104
+ } catch (err) { next(err); }
105
+ };
106
+ `);
107
+
108
+ // Routes index
109
+ await fs.writeFile(base(`src/routes/index.${ext}`), `import { Router } from 'express';
110
+ import userRoutes from './userRoutes.js';
111
+ ${answers.auth !== 'none' ? "import authRoutes from './authRoutes.js';" : ''}
112
+
113
+ const router = Router();
114
+
115
+ ${answers.auth !== 'none' ? "router.use('/auth', authRoutes);" : ''}
116
+ router.use('/users', userRoutes);
117
+
118
+ export default router;
119
+ `);
120
+
121
+ // User routes
122
+ await fs.writeFile(base(`src/routes/userRoutes.${ext}`), `import { Router } from 'express';
123
+ import { getAllUsers, getUserById, updateUser, deleteUser } from '../controllers/userController.js';
124
+ ${answers.auth === 'jwt' ? "import { authenticate, authorize } from '../middlewares/authenticate.js';" : ''}
125
+
126
+ const router = Router();
127
+
128
+ ${answers.auth === 'jwt' ? "router.use(authenticate);" : ''}
129
+ router.get('/', getAllUsers);
130
+ router.get('/:id', getUserById);
131
+ router.put('/:id', updateUser);
132
+ router.delete('/:id', ${answers.auth === 'jwt' ? "authorize('admin'), " : ''}deleteUser);
133
+
134
+ export default router;
135
+ `);
136
+ }
137
+
138
+ async function writeModularFiles(answers, targetDir, ext) {
139
+ const base = (p) => path.join(targetDir, p);
140
+
141
+ // user module controller
142
+ await fs.writeFile(base(`src/modules/user/controller/user.controller.${ext}`), `import { UserService } from '../service/user.service.js';
143
+ import { sendSuccess, sendError } from '../../../utils/apiResponse.js';
144
+
145
+ const userService = new UserService();
146
+
147
+ export const getUsers = async (req, res, next) => {
148
+ try {
149
+ const users = await userService.findAll();
150
+ sendSuccess(res, users);
151
+ } catch (err) { next(err); }
152
+ };
153
+
154
+ export const getUser = async (req, res, next) => {
155
+ try {
156
+ const user = await userService.findById(req.params.id);
157
+ if (!user) return sendError(res, 'User not found', 404);
158
+ sendSuccess(res, user);
159
+ } catch (err) { next(err); }
160
+ };
161
+ `);
162
+
163
+ // user service
164
+ await fs.writeFile(base(`src/modules/user/service/user.service.${ext}`), `${answers.database === 'mongodb' ? "import User from '../model/user.model.js';" : "import { prisma } from '../../../config/db.js';"}
165
+
166
+ export class UserService {
167
+ async findAll() {
168
+ return ${answers.database === 'mongodb' ? "User.find().select('-password').lean()" : "prisma.user.findMany({ select: { id: true, name: true, email: true } })"};
169
+ }
170
+ async findById(id) {
171
+ return ${answers.database === 'mongodb' ? "User.findById(id).select('-password')" : "prisma.user.findUnique({ where: { id } })"};
172
+ }
173
+ }
174
+ `);
175
+
176
+ // user routes
177
+ await fs.writeFile(base(`src/modules/user/routes/user.routes.${ext}`), `import { Router } from 'express';
178
+ import { getUsers, getUser } from '../controller/user.controller.js';
179
+
180
+ const router = Router();
181
+ router.get('/', getUsers);
182
+ router.get('/:id', getUser);
183
+ export default router;
184
+ `);
185
+
186
+ // main routes index
187
+ await fs.writeFile(base(`src/routes/index.${ext}`), `import { Router } from 'express';
188
+ import userRoutes from '../modules/user/routes/user.routes.js';
189
+ ${answers.auth !== 'none' ? "import authRoutes from './authRoutes.js';" : ''}
190
+
191
+ const router = Router();
192
+ ${answers.auth !== 'none' ? "router.use('/auth', authRoutes);" : ''}
193
+ router.use('/users', userRoutes);
194
+ export default router;
195
+ `);
196
+ }
197
+
198
+ async function writeLayeredFiles(answers, targetDir, ext) {
199
+ const base = (p) => path.join(targetDir, p);
200
+
201
+ // Repository layer
202
+ await fs.writeFile(base(`src/repositories/userRepository.${ext}`), `${answers.database === 'mongodb' ? "import User from '../models/User.js';" : "import { prisma } from '../config/db.js';"}
203
+
204
+ export class UserRepository {
205
+ async findAll({ skip = 0, limit = 10 } = {}) {
206
+ return ${answers.database === 'mongodb' ? "User.find().select('-password').skip(skip).limit(limit).lean()" : "prisma.user.findMany({ skip, take: limit, select: { id: true, name: true, email: true } })"};
207
+ }
208
+ async findById(id) {
209
+ return ${answers.database === 'mongodb' ? "User.findById(id).select('-password')" : "prisma.user.findUnique({ where: { id } })"};
210
+ }
211
+ async findByEmail(email) {
212
+ return ${answers.database === 'mongodb' ? "User.findOne({ email })" : "prisma.user.findUnique({ where: { email } })"};
213
+ }
214
+ async update(id, data) {
215
+ return ${answers.database === 'mongodb' ? "User.findByIdAndUpdate(id, data, { new: true }).select('-password')" : "prisma.user.update({ where: { id }, data })"};
216
+ }
217
+ async delete(id) {
218
+ return ${answers.database === 'mongodb' ? "User.findByIdAndDelete(id)" : "prisma.user.delete({ where: { id } })"};
219
+ }
220
+ }
221
+ `);
222
+
223
+ // Service layer
224
+ await fs.writeFile(base(`src/services/userService.${ext}`), `import { UserRepository } from '../repositories/userRepository.js';
225
+
226
+ export class UserService {
227
+ constructor() { this.repo = new UserRepository(); }
228
+
229
+ async getAllUsers(query) { return this.repo.findAll(query); }
230
+ async getUserById(id) {
231
+ const user = await this.repo.findById(id);
232
+ if (!user) throw Object.assign(new Error('User not found'), { statusCode: 404 });
233
+ return user;
234
+ }
235
+ async updateUser(id, data) { return this.repo.update(id, data); }
236
+ async deleteUser(id) { return this.repo.delete(id); }
237
+ }
238
+ `);
239
+
240
+ // Controller
241
+ await fs.writeFile(base(`src/controllers/userController.${ext}`), `import { UserService } from '../services/userService.js';
242
+ import { sendSuccess } from '../utils/apiResponse.js';
243
+
244
+ const userService = new UserService();
245
+
246
+ export const getAllUsers = async (req, res, next) => { try { sendSuccess(res, await userService.getAllUsers(req.query)); } catch(e){next(e);} };
247
+ export const getUserById = async (req, res, next) => { try { sendSuccess(res, await userService.getUserById(req.params.id)); } catch(e){next(e);} };
248
+ export const updateUser = async (req, res, next) => { try { sendSuccess(res, await userService.updateUser(req.params.id, req.body), 'User updated'); } catch(e){next(e);} };
249
+ export const deleteUser = async (req, res, next) => { try { await userService.deleteUser(req.params.id); sendSuccess(res, null, 'User deleted'); } catch(e){next(e);} };
250
+ `);
251
+
252
+ // Routes
253
+ await fs.writeFile(base(`src/routes/index.${ext}`), `import { Router } from 'express';
254
+ import { getAllUsers, getUserById, updateUser, deleteUser } from '../controllers/userController.js';
255
+ ${answers.auth !== 'none' ? "import authRoutes from './authRoutes.js';\nimport { authenticate } from '../middlewares/authenticate.js';" : ''}
256
+
257
+ const router = Router();
258
+ ${answers.auth !== 'none' ? "router.use('/auth', authRoutes);" : ''}
259
+ router.get('/users', ${answers.auth === 'jwt' ? 'authenticate, ' : ''}getAllUsers);
260
+ router.get('/users/:id', ${answers.auth === 'jwt' ? 'authenticate, ' : ''}getUserById);
261
+ router.put('/users/:id', ${answers.auth === 'jwt' ? 'authenticate, ' : ''}updateUser);
262
+ router.delete('/users/:id', ${answers.auth === 'jwt' ? 'authenticate, ' : ''}deleteUser);
263
+ export default router;
264
+ `);
265
+ }
package/src/index.js ADDED
@@ -0,0 +1,38 @@
1
+ import gradient from 'gradient-string';
2
+ import chalk from 'chalk';
3
+ import path from 'path';
4
+ import { askQuestions } from './prompts/questions.js';
5
+ import { generateProject } from './generators/projectGenerator.js';
6
+
7
+ export async function run() {
8
+ console.clear();
9
+
10
+ const banner = gradient(['#6C63FF', '#48CAE4'])(
11
+ `
12
+ ██╗ ██╗██████╗ ██████╗ ███████╗███████╗███████╗
13
+ ╚██╗██╔╝██╔══██╗██╔══██╗██╔════╝██╔════╝██╔════╝
14
+ ╚███╔╝ ██████╔╝██████╔╝█████╗ ███████╗███████╗
15
+ ██╔██╗ ██╔═══╝ ██╔══██╗██╔══╝ ╚════██║╚════██║
16
+ ██╔╝ ██╗██║ ██║ ██║███████╗███████║███████║
17
+ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝
18
+ F O R G E
19
+ `
20
+ );
21
+
22
+ console.log(banner);
23
+ console.log(chalk.dim(' Production-ready Node.js + Express scaffolder\n'));
24
+ console.log(chalk.dim(' by Hammad Sadi\n'));
25
+
26
+ try {
27
+ const answers = await askQuestions();
28
+ const targetDir = path.resolve(process.cwd(), answers.projectName);
29
+ await generateProject(answers, targetDir);
30
+ } catch (err) {
31
+ if (err.name === 'ExitPromptError') {
32
+ console.log(chalk.yellow('\n Cancelled. See you next time!\n'));
33
+ process.exit(0);
34
+ }
35
+ console.error(chalk.red('\n Error: ' + err.message));
36
+ process.exit(1);
37
+ }
38
+ }
@@ -0,0 +1,68 @@
1
+ import { input, select, checkbox } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+
4
+ export async function askQuestions() {
5
+ console.log(chalk.bold(' Let\'s configure your project:\n'));
6
+
7
+ const projectName = await input({
8
+ message: 'Project name:',
9
+ default: 'my-express-app',
10
+ validate: (val) => {
11
+ if (!val.trim()) return 'Project name cannot be empty';
12
+ if (!/^[a-z0-9-_]+$/.test(val)) return 'Use lowercase letters, numbers, hyphens only';
13
+ return true;
14
+ },
15
+ });
16
+
17
+ const structure = await select({
18
+ message: 'Project structure:',
19
+ choices: [
20
+ { name: 'MVC — controllers, models, views, routes', value: 'mvc' },
21
+ { name: 'Modular — feature-based modules (recommended for large apps)', value: 'modular' },
22
+ { name: 'Layered — controllers, services, repositories, models', value: 'layered' },
23
+ ],
24
+ });
25
+
26
+ const database = await select({
27
+ message: 'Database:',
28
+ choices: [
29
+ { name: 'MongoDB — with Mongoose ODM', value: 'mongodb' },
30
+ { name: 'PostgreSQL — with Prisma ORM', value: 'postgresql' },
31
+ { name: 'MySQL — with Sequelize ORM', value: 'mysql' },
32
+ { name: 'None — no database', value: 'none' },
33
+ ],
34
+ });
35
+
36
+ const auth = await select({
37
+ message: 'Authentication:',
38
+ choices: [
39
+ { name: 'JWT — access + refresh token', value: 'jwt' },
40
+ { name: 'Session — express-session + cookie', value: 'session' },
41
+ { name: 'None — skip auth setup', value: 'none' },
42
+ ],
43
+ });
44
+
45
+ const extras = await checkbox({
46
+ message: 'Extra features (space to select):',
47
+ choices: [
48
+ { name: 'Rate limiting (express-rate-limit)', value: 'rateLimit', checked: true },
49
+ { name: 'Security headers (helmet)', value: 'helmet', checked: true },
50
+ { name: 'CORS (cors)', value: 'cors', checked: true },
51
+ { name: 'Request logger (morgan)', value: 'morgan', checked: true },
52
+ { name: 'Input validation (express-validator)', value: 'validation', checked: false },
53
+ { name: 'File upload (multer)', value: 'multer', checked: false },
54
+ { name: 'Socket.io (real-time)', value: 'socket', checked: false },
55
+ { name: 'Swagger docs (swagger-ui-express)', value: 'swagger', checked: false },
56
+ ],
57
+ });
58
+
59
+ const language = await select({
60
+ message: 'Language:',
61
+ choices: [
62
+ { name: 'JavaScript — ES Modules (type: module)', value: 'js' },
63
+ { name: 'TypeScript — fully typed', value: 'ts' },
64
+ ],
65
+ });
66
+
67
+ return { projectName, structure, database, auth, extras, language };
68
+ }