nodly
Version:
Nodly is an interactive CLI for scaffolding modern Node.js/Express.js backend projects with instant setup for databases, mailers, queues, sockets, and more. Skip boilerplate and start building features fast.
618 lines (540 loc) • 18.2 kB
JavaScript
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import chalk from "chalk";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const args = process.argv.slice(2);
const templatesDir = path.join(__dirname, "templates");
// --- Add Middleware ---
if (args[0] === "add" && args[1] === "middleware" && args[2]) {
// Check if middlewares folder exists in current directory
const middlewaresDir = path.join(process.cwd(), "middlewares");
if (!fs.existsSync(middlewaresDir)) {
console.log(chalk.red("✖ No 'middlewares' folder found. Please run this command from your Nodly project root."));
process.exit(1);
}
const name = args[2];
const fileName = `${name}.middleware.js`;
const middlewarePath = path.join(middlewaresDir, fileName);
// Check if file already exists
if (fs.existsSync(middlewarePath)) {
console.log(chalk.red(`✖ Middleware already exists: middlewares/${fileName}`));
process.exit(1);
}
const template =
`// middlewares/${fileName}
import ApiResponse from "../utils/apiresponses.js";
export const ${name}Middleware = (req, res, next) => {
try {
// Your middleware logic here
next();
} catch (error) {
console.error("${name} Middleware Error:", error);
return ApiResponse.error(res, "${name} middleware failed");
}
};
`;
fs.writeFileSync(middlewarePath, template);
console.log(chalk.green(`✔ Created middleware: middlewares/${fileName}`));
process.exit(0);
}
// --- Add Controller ---
if (args[0] === "add" && args[1] === "controller" && args[2]) {
const name = args[2];
const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
const fileName = `${name}.controller.js`;
const controllerPath = path.join(process.cwd(), "controllers", fileName);
// Check if file already exists
if (fs.existsSync(controllerPath)) {
console.log(chalk.red(`✖ Controller already exists: controllers/${fileName}`));
process.exit(1);
}
const template =
`// controllers/${fileName}
import ApiResponse from "../utils/apiresponses.js";
export const get${pascalName} = (req, res) => {
return ApiResponse.success(res, "${pascalName} fetched successfully");
};
export const create${pascalName} = (req, res) => {
// Example: const data = req.body;
return ApiResponse.success(res, "${pascalName} created successfully");
};
// Add more controller methods as needed
`;
fs.writeFileSync(controllerPath, template);
console.log(chalk.green(`✔ Created controller: controllers/${fileName}`));
process.exit(0);
}
// --- Add Router ---
if (args[0] === "add" && args[1] === "router" && args[2]) {
const name = args[2];
const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
const fileName = `${name}.routes.js`;
const routerPath = path.join(process.cwd(), "routes", fileName);
// Check if file already exists
if (fs.existsSync(routerPath)) {
console.log(chalk.red(`✖ Router already exists: routes/${fileName}`));
process.exit(1);
}
const template =
`// routes/${fileName}
import { Router } from "express";
import { get${pascalName}, create${pascalName} } from "../controllers/${name}.controller.js";
const router = Router();
router.get("/", get${pascalName});
router.post("/", create${pascalName});
// Add more routes as needed
export default router;
`;
fs.writeFileSync(routerPath, template);
console.log(chalk.green(`✔ Created router: routes/${fileName}`));
process.exit(0);
}
// Only import prompts, ora, and run project creation logic BELOW this line!
import prompts from "prompts";
import ora from "ora";
import { execSync } from "child_process";
const answers = await prompts([
{
type: "text",
name: "projectName",
message: "Enter project name:",
initial: "my-nodly-app",
},
{
type: "multiselect",
name: "databases",
message: "Which database(s) do you want to use?",
choices: [
{ title: "MySQL2", value: "mysql2" },
{ title: "PostgreSQL", value: "postgresql" },
{ title: "MongoDB (Mongoose)", value: "mongoose" },
{ title: "Redis", value: "redis" },
{ title: "Prisma", value: "prisma" },
{ title: "Supabase", value: "supabase" },
{ title: "TypeORM", value: "typeorm" },
{ title: "Drizzle ORM", value: "drizzle" },
{ title: "Firebase", value: "firebase" },
],
},
{
type: "multiselect",
name: "queues",
message: "Which message queue(s) do you want to use?",
choices: [
{ title: "BullMQ", value: "bullmq" },
{ title: "RabbitMQ", value: "rabbitmq" },
{ title: "Kafka", value: "kafka" },
],
},
{
type: "confirm",
name: "useMailer",
message: "Do you want to include Nodemailer?",
initial: true,
},
{
type: "confirm",
name: "useSocketIO",
message: "Do you want to include Socket.IO?",
initial: true,
},
{
type: "toggle",
name: "useJWT",
message: "Do you want to include JWT authentication?",
initial: true,
active: "yes",
inactive: "no",
},
{
type: prev => prev ? "text" : null,
name: "jwtSecret",
message: "Enter a JWT secret (leave blank for default):",
initial: "supersecretkey",
},
]);
// 1. Create project directory
const targetDir = path.join(process.cwd(), answers.projectName);
fs.mkdirSync(targetDir, { recursive: true });
// 2. Copy common files (Dockerfile, server.js, etc.)
["Dockerfile", "server.js", ".env"].forEach((file) => {
fs.cpSync(path.join(templatesDir, file), path.join(targetDir, file));
});
// Copy essential folders (routes, controllers, middlewares, utils)
["routes", "controllers", "middlewares", "utils"].forEach((folder) => {
const srcFolder = path.join(templatesDir, folder);
const destFolder = path.join(targetDir, folder);
if (fs.existsSync(srcFolder)) {
fs.cpSync(srcFolder, destFolder, { recursive: true });
}
});
// Conditionally copy mailers folder and sendMail.js
if (answers.useMailer) {
const mailersSrc = path.join(templatesDir, "config", "mailers");
const mailersDest = path.join(targetDir, "config", "mailers");
if (fs.existsSync(mailersSrc)) {
fs.cpSync(mailersSrc, mailersDest, { recursive: true });
}
const sendMailSrc = path.join(templatesDir, "utils", "sendMail.js");
const sendMailDest = path.join(targetDir, "utils", "sendMail.js");
if (fs.existsSync(sendMailSrc)) {
fs.cpSync(sendMailSrc, sendMailDest);
}
console.log(chalk.yellow("✔ Nodemailer files added."));
console.log(chalk.green("Nodemailer mailer integration enabled!"));
}
// Conditionally copy sockets folder
if (answers.useSocketIO) {
const socketsSrc = path.join(templatesDir, "config", "sockets");
const socketsDest = path.join(targetDir, "config", "sockets");
if (fs.existsSync(socketsSrc)) {
fs.cpSync(socketsSrc, socketsDest, { recursive: true });
}
console.log(chalk.yellow("✔ Socket.IO files added."));
}
// Conditionally copy nodemailer.js and socketIO.js in config logic
const allConfigs = [
"redis",
"mysql2",
"mongoose",
"prisma",
"postgresql",
"drizzle",
"supabase",
"typeorm",
"firebase",
"bullmq",
"rabbitmq",
"kafka",
];
allConfigs.forEach((cfg) => {
let src, dest;
if (["bullmq", "rabbitmq"].includes(cfg)) {
src = path.join(templatesDir, "config", "queues", cfg, `${cfg}.js`);
dest = path.join(targetDir, "config", "queues", cfg, `${cfg}.js`);
if (answers.queues && answers.queues.includes(cfg)) {
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.cpSync(src, dest);
console.log(chalk.green(`Queue integration enabled: ${cfg}`));
}
} else if (cfg === "kafka") {
// Copy both producer and consumer files if Kafka is selected
if (answers.queues && answers.queues.includes("kafka")) {
const kafkaSrcDir = path.join(templatesDir, "config", "queues", "kafka");
const kafkaDestDir = path.join(targetDir, "config", "queues", "kafka");
fs.mkdirSync(kafkaDestDir, { recursive: true });
fs.cpSync(
path.join(kafkaSrcDir, "producer.js"),
path.join(kafkaDestDir, "producer.js")
);
fs.cpSync(
path.join(kafkaSrcDir, "consumer.js"),
path.join(kafkaDestDir, "consumer.js")
);
console.log(chalk.green(`Queue integration enabled: ${cfg}`));
}
} else {
src = path.join(templatesDir, "config", "databases", `${cfg}.js`);
dest = path.join(targetDir, "config", "databases", `${cfg}.js`);
if (answers.databases.includes(cfg)) {
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.cpSync(src, dest);
console.log(chalk.green(`Database integration enabled: ${cfg}`));
}
}
if (cfg === "kafka") {
// Copy both producer and consumer files if Kafka is selected
if (answers.queues && answers.queues.includes("kafka")) {
const kafkaSrcDir = path.join(templatesDir, "config", "queues", "kafka");
const kafkaDestDir = path.join(targetDir, "config", "queues", "kafka");
fs.mkdirSync(kafkaDestDir, { recursive: true });
fs.cpSync(
path.join(kafkaSrcDir, "producer.js"),
path.join(kafkaDestDir, "producer.js")
);
fs.cpSync(
path.join(kafkaSrcDir, "consumer.js"),
path.join(kafkaDestDir, "consumer.js")
);
}
}
});
// 4. Write .env file (already well-structured in your template)
fs.cpSync(path.join(templatesDir, ".env"), path.join(targetDir, ".env"));
// 5. Install dependencies
const baseDeps = [
"express",
"dotenv",
"cors",
"morgan",
"express-rate-limit",
"jsonwebtoken",
"chalk",
"prompts",
"ora",
];
// Add nodemon as a dev dependency
const devDeps = ["nodemon"];
const deps = [...baseDeps];
if (answers.useMailer) deps.push("nodemailer");
if (answers.useSocketIO) deps.push("socket.io");
if (answers.databases.includes("mongoose")) deps.push("mongoose");
if (answers.databases.includes("redis")) deps.push("ioredis");
if (answers.databases.includes("mysql2")) deps.push("mysql2");
if (answers.databases.includes("postgresql")) deps.push("pg");
if (answers.databases.includes("prisma")) deps.push("prisma", "@prisma/client");
if (answers.databases.includes("typeorm")) deps.push("typeorm", "reflect-metadata");
if (answers.databases.includes("drizzle")) deps.push("drizzle-orm", "pg");
if (answers.databases.includes("supabase")) deps.push("@supabase/supabase-js");
if (answers.databases.includes("firebase")) deps.push("firebase");
if (answers.queues && answers.queues.includes("bullmq")) deps.push("bullmq");
if (answers.queues && answers.queues.includes("kafka")) deps.push("kafkajs");
if (answers.queues && answers.queues.includes("rabbitmq")) deps.push("amqplib");
// Show the user which packages will be installed
console.log(chalk.cyan("\nThe following npm packages will be installed:"));
console.log(chalk.green(deps.join(" ")));
console.log(chalk.cyan("The following dev packages will be installed:"));
console.log(chalk.green(devDeps.join(" ")));
console.log();
const spinner = ora("Installing dependencies...").start();
try {
execSync(`npm init -y`, { stdio: "inherit", cwd: targetDir });
execSync(`npm install ${deps.join(" ")}`, {
stdio: "inherit",
cwd: targetDir,
});
execSync(`npm install -D ${devDeps.join(" ")}`, {
stdio: "inherit",
cwd: targetDir,
});
spinner.succeed("Dependencies installed!");
} catch (err) {
spinner.fail("Failed to install dependencies.");
console.error(err);
}
// 6. Add start script and type:module
const pkgPath = path.join(targetDir, "package.json");
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
pkg.scripts = pkg.scripts || {};
pkg.scripts.start = "node server.js";
pkg.scripts.dev = "nodemon server.js";
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
// Patch server.js only if Socket.IO is selected
const serverPath = path.join(targetDir, "server.js");
let content = fs.readFileSync(serverPath, "utf8");
if (answers.useSocketIO) {
content = content.replace(
"// SOCKET.IO_PLACEHOLDER",
`import http from "http";\nimport { initSocketIO } from "./config/sockets/socketIO.js";`
);
content = content.replace(
"// SOCKET.IO_BOOTSTRAP",
`const socketServer = http.createServer(server);\n initSocketIO(socketServer);\n;`
);
console.log(chalk.yellow("✔ Socket.IO code added to server.js."));
} else {
// Remove both placeholders and use basic app.listen
content = content.replace("// SOCKET.IO_PLACEHOLDER", "");
content = content.replace("// SOCKET.IO_BOOTSTRAP", "");
}
fs.writeFileSync(serverPath, content);
// Initialize Prisma if selected
if (answers.databases.includes("prisma")) {
try {
execSync(`npx prisma init`, { stdio: "inherit", cwd: targetDir });
console.log(
chalk.yellow(
"✔ Prisma initialized (prisma/schema.prisma and .env created)."
)
);
} catch (err) {
console.error("Failed to initialize Prisma:", err);
}
}
// TypeORM reminder
if (answers.databases.includes("typeorm")) {
console.log(
chalk.blue(
"👉 Add your entities to the TypeORM config and run migrations as needed."
)
);
}
// Drizzle reminder
if (answers.databases.includes("drizzle")) {
console.log(
chalk.blue(
"👉 Set up your Drizzle models and run migrations as needed."
)
);
}
// 7. Write default .env file
const envVars = {
// App
PORT: "5000",
NODE_ENV: "development",
JWT_SECRET: "supersecretkey",
// Mail
MAIL_HOST: "smtp.yourprovider.com",
MAIL_PORT: "587",
MAIL_SECURE: "false",
MAIL_USER: "your@email.com",
MAIL_PASS: "yourpassword",
// MYSQL2
MYSQL_HOST: "localhost",
MYSQL_PORT: "3306",
MYSQL_USER: "root",
MYSQL_PASSWORD: "yourpassword",
MYSQL_DATABASE: "nodly_db",
// POSTGRESQL
POSTGRES_URL: "postgresql://user:password@localhost:5432/nodly_db",
// MONGODB
MONGO_URI: "mongodb://localhost:27017/nodly_db",
// REDIS
REDIS_HOST: "127.0.0.1",
REDIS_PORT: "6379",
REDIS_PASSWORD: "yourpassword",
// PRISMA
DATABASE_URL: "postgresql://user:password@localhost:5432/nodly_db",
// TYPEORM
TYPEORM_DATABASE_URL: "postgresql://user:password@localhost:5432/nodly_db",
// DRIZZLE
DRIZZLE_DATABASE_URL: "postgresql://user:password@localhost:5432/nodly_db",
// SUPABASE
SUPABASE_URL: "https://your-supabase-url.supabase.co",
SUPABASE_ANON_KEY: "your-anon-key",
// FIREBASE
FIREBASE_API_KEY: "your-firebase-api-key",
FIREBASE_AUTH_DOMAIN: "your-app.firebaseapp.com",
FIREBASE_PROJECT_ID: "your-project-id",
FIREBASE_STORAGE_BUCKET: "your-app.appspot.com",
FIREBASE_MESSAGING_SENDER_ID: "your-messaging-sender-id",
FIREBASE_APP_ID: "your-app-id",
// QUEUES
RABBITMQ_URL: "amqp://localhost",
KAFKA_BROKER: "localhost:9092",
};
const envPath = path.join(targetDir, ".env");
let envContent = `#App
PORT=5000
NODE_ENV=development
`;
if (answers.useJWT) {
envContent += `JWT_SECRET=${answers.jwtSecret || "supersecretkey"}\n`;
}
if (answers.useMailer) {
envContent += `
# Mail
MAIL_HOST=${envVars.MAIL_HOST}
MAIL_PORT=${envVars.MAIL_PORT}
MAIL_SECURE=${envVars.MAIL_SECURE}
MAIL_USER=${envVars.MAIL_USER}
MAIL_PASS=${envVars.MAIL_PASS}
`;
}
if (answers.databases.includes("mysql2")) {
envContent += `
# MYSQL2
MYSQL_HOST=${envVars.MYSQL_HOST}
MYSQL_PORT=${envVars.MYSQL_PORT}
MYSQL_USER=${envVars.MYSQL_USER}
MYSQL_PASSWORD=${envVars.MYSQL_PASSWORD}
MYSQL_DATABASE=${envVars.MYSQL_DATABASE}
`;
}
if (answers.databases.includes("postgresql")) {
envContent += `
# POSTGRESQL
POSTGRES_URL=${envVars.POSTGRES_URL}
`;
}
if (answers.databases.includes("mongoose")) {
envContent += `
# MONGODB
MONGO_URI=${envVars.MONGO_URI}
`;
}
if (answers.databases.includes("redis")) {
envContent += `
# REDIS
REDIS_HOST=${envVars.REDIS_HOST}
REDIS_PORT=${envVars.REDIS_PORT}
REDIS_PASSWORD=${envVars.REDIS_PASSWORD}
`;
}
if (answers.databases.includes("prisma")) {
envContent += `
# PRISMA
DATABASE_URL=${envVars.DATABASE_URL}
`;
}
if (answers.databases.includes("typeorm")) {
envContent += `
# TYPEORM
TYPEORM_DATABASE_URL=${envVars.TYPEORM_DATABASE_URL}
`;
}
if (answers.databases.includes("drizzle")) {
envContent += `
# DRIZZLE
DRIZZLE_DATABASE_URL=${envVars.DRIZZLE_DATABASE_URL}
`;
}
if (answers.databases.includes("supabase")) {
envContent += `
# SUPABASE
SUPABASE_URL=${envVars.SUPABASE_URL}
SUPABASE_ANON_KEY=${envVars.SUPABASE_ANON_KEY}
`;
}
if (answers.databases.includes("firebase")) {
envContent += `
# FIREBASE
FIREBASE_API_KEY=${envVars.FIREBASE_API_KEY}
FIREBASE_AUTH_DOMAIN=${envVars.FIREBASE_AUTH_DOMAIN}
FIREBASE_PROJECT_ID=${envVars.FIREBASE_PROJECT_ID}
FIREBASE_STORAGE_BUCKET=${envVars.FIREBASE_STORAGE_BUCKET}
FIREBASE_MESSAGING_SENDER_ID=${envVars.FIREBASE_MESSAGING_SENDER_ID}
FIREBASE_APP_ID=${envVars.FIREBASE_APP_ID}
`;
}
if (answers.queues && answers.queues.includes("rabbitmq")) {
envContent += `
# RABBITMQ
RABBITMQ_URL=${envVars.RABBITMQ_URL}
`;
}
if (answers.queues && answers.queues.includes("kafka")) {
envContent += `
# KAFKA
KAFKA_BROKER=${envVars.KAFKA_BROKER}
`;
}
// Write the .env file
fs.writeFileSync(path.join(targetDir, ".env"), envContent.trim());
// JWT utility file
if (answers.useJWT) {
const jwtUtilSrc = path.join(templatesDir, "utils", "jsonwebtoken.js");
const jwtUtilDest = path.join(targetDir, "utils", "jsonwebtoken.js");
if (fs.existsSync(jwtUtilSrc)) {
fs.cpSync(jwtUtilSrc, jwtUtilDest);
} else {
// fallback: create a minimal file if template missing
fs.writeFileSync(jwtUtilDest,
`import jwt from "jsonwebtoken";
export const signToken = (payload, secret = process.env.JWT_SECRET, options = {}) =>
jwt.sign(payload, secret, options);
export const verifyToken = (token, secret = process.env.JWT_SECRET) =>
jwt.verify(token, secret);
`);
}
}
console.log(chalk.green(`\n✅ Nodly App setup complete at: ${targetDir}\n`));
console.log(
chalk.blue(`Run your app with: cd ${answers.projectName} && npm start`)
);
console.log(chalk.blue(`Don't forget to configure your .env file!`));
console.log(chalk.blue(`Happy coding! 🚀\n`));
console.log(chalk.green("\n✔ All selected integrations have been set up!"));