UNPKG

@saiteja13/express-template

Version:

530 lines (475 loc) 23.7 kB
#!/usr/bin/env node // Import using ESM syntax for Node.js import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { execSync } from 'child_process'; import pkg from 'enquirer'; import chalk from 'chalk'; import boxen from 'boxen'; import ora from 'ora'; import figlet from 'figlet'; // Get __dirname equivalent in ESM const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const { Confirm, Input, Select, MultiSelect } = pkg; // Display a nice welcome banner console.log( chalk.cyan( figlet.textSync("Express TS", { horizontalLayout: "full" }) ) ); console.log( boxen( chalk.white("A modern TypeScript Express starter generator") + "\n\n" + chalk.yellow("Created with ♥ by saiteja-in"), { padding: 1, margin: 1, borderStyle: "round", borderColor: "cyan" } ) ); const showHelp = () => { console.log(chalk.cyan.bold("\nExpress TypeScript CLI Help:")); console.log(chalk.white("This CLI tool helps you create a new Express TypeScript project with a well-structured boilerplate.")); console.log(chalk.white("\nUsage:")); console.log(chalk.yellow(" npx expressts [project-name]")); console.log(chalk.yellow(" npx expressts --help")); console.log(chalk.white("\nOptions:")); console.log(chalk.yellow(" --help, -h Display this help message")); console.log(chalk.white("\nWhen run without arguments, the CLI will guide you through the setup process interactively.")); process.exit(0); }; // Check for help flag if (process.argv.includes("--help") || process.argv.includes("-h")) { showHelp(); } // Function to create project structure const createProject = async () => { try { // Get project name let projectName = process.argv[2]; if (!projectName) { const namePrompt = await new Input({ name: "projectName", message: "What is your project name?", validate: (input) => input.trim() !== "" || "Project name is required" }).run(); projectName = namePrompt; } const currentDir = process.cwd(); const projectDir = path.join(currentDir, projectName); // Check if directory already exists if (fs.existsSync(projectDir) && projectName !== ".") { const overwrite = await new Confirm({ name: "overwrite", message: `Directory ${projectName} already exists. Continue and overwrite?` }).run(); if (!overwrite) { console.log(chalk.yellow("Project creation cancelled")); process.exit(0); } } // Database selection const databaseType = await new Select({ name: "database", message: "Select a database to configure (Optional)", choices: [ { name: "none", message: "None - I'll configure it later" }, { name: "mongodb", message: "MongoDB" }, { name: "postgres", message: "PostgreSQL" }, { name: "mysql", message: "MySQL" } ] }).run(); // Features selection const selectedFeatures = await new MultiSelect({ name: "features", message: "Select additional features to include:", choices: [ { name: "authentication", message: "JWT Authentication" }, { name: "logging", message: "Winston Logger" }, { name: "swagger", message: "Swagger Documentation" }, { name: "validation", message: "Request Validation (Zod)" } ] }).run(); // Package manager selection const packageManager = await new Select({ name: "packageManager", message: "Select your preferred package manager:", choices: ["npm", "yarn", "pnpm"] }).run(); // Start creating the project const spinner = ora("Creating project structure...").start(); // Create project directory and src subdirectories const srcDirs = ["controllers", "routes", "models", "db", "types", "utils"]; fs.mkdirSync(projectDir, { recursive: true }); fs.mkdirSync(path.join(projectDir, "src")); srcDirs.forEach((dir) => fs.mkdirSync(path.join(projectDir, "src", dir))); spinner.text = "Creating files..."; // Add database configuration based on selection let dbConnectionContent = `// Write your database connection here`; let extraDependencies = {}; let extraDevDependencies = {}; let extraImports = ""; let databaseConnectionCode = ""; if (databaseType === "mongodb") { dbConnectionContent = `import mongoose from "mongoose";\n export const connectDB = async (): Promise<void> => { try { const connection = await mongoose.connect(process.env.MONGODB_URI || "mongodb://localhost:27017/YOUR_DATABASE"); console.log(\`MongoDB connected: \${connection.connection.host}\`); } catch (error) { console.error("MongoDB connection error:", error); process.exit(1); } };\n`; extraDependencies.mongoose = "^8.0.0"; } else if (databaseType === "postgres") { dbConnectionContent = `import { Pool } from "pg";\n const pool = new Pool({ user: process.env.PGUSER, host: process.env.PGHOST, database: process.env.PGDATABASE, password: process.env.PGPASSWORD, port: parseInt(process.env.PGPORT || "5432"), });\n export const query = async (text: string, params: any[]) => { return pool.query(text, params); };\n`; extraDependencies.pg = "^8.11.3"; extraDevDependencies["@types/pg"] = "^8.10.9"; } else if (databaseType === "mysql") { dbConnectionContent = `import mysql from "mysql2/promise";\n export const pool = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, queueLimit: 0 });\n export const query = async (sql: string, params: any[] = []) => { const [rows] = await pool.execute(sql, params); return rows; };\n`; extraDependencies.mysql2 = "^3.6.5"; } // Add extra imports and code based on selected features if (selectedFeatures.includes("logging")) { extraDependencies.winston = "^3.11.0"; extraImports += `import winston from "winston";\n`; extraDependencies["winston-daily-rotate-file"] = "^4.7.1"; } if (selectedFeatures.includes("swagger")) { extraDependencies["swagger-ui-express"] = "^5.0.0"; extraDependencies["swagger-jsdoc"] = "^6.2.8"; extraDevDependencies["@types/swagger-ui-express"] = "^4.1.6"; extraDevDependencies["@types/swagger-jsdoc"] = "^6.0.4"; extraImports += `import swaggerUi from "swagger-ui-express";\nimport swaggerJsDoc from "swagger-jsdoc";\n`; } if (selectedFeatures.includes("validation")) { extraDependencies.zod = "^3.22.4"; } if (selectedFeatures.includes("authentication")) { extraDependencies.jsonwebtoken = "^9.0.2"; extraDependencies.bcryptjs = "^2.4.3"; extraDevDependencies["@types/jsonwebtoken"] = "^9.0.5"; extraDevDependencies["@types/bcryptjs"] = "^2.4.6"; } // Connect to database in index.ts if a database was selected if (databaseType !== "none") { if (databaseType === "mongodb") { databaseConnectionCode = `\n// Establish database connection\nimport { connectDB } from "./db/connection";\nconnectDB().catch(err => console.error("Database connection failed", err));\n`; } } // File templates const files = [ { path: "src/controllers/example.controllers.ts", content: `import { Request, Response } from "express";\nimport { ExampleType } from "../types/example.types";\nimport { capitalizeString, generateRandomId } from "../utils/example.utils";\nimport ApiError from "../utils/ApiError";\nimport ApiResponse from "../utils/ApiResponse";\nimport AsyncHandler from "../utils/AsyncHandler";\n\nexport const getExample = AsyncHandler(async (req: Request, res: Response) => {\n try {\n const exampleItem: ExampleType = {\n id: generateRandomId(),\n name: capitalizeString("example"),\n description: "This is an example item",\n };\n\n res.status(200).json(new ApiResponse(200, exampleItem));\n } catch (error) {\n throw new ApiError(500, "Failed to get example item");\n }\n});\n\nexport const postExample = AsyncHandler(async (req: Request, res: Response) => {\n try {\n const { name = "not provided", description = "not provided" } = req.body;\n const newExampleItem: ExampleType = {\n id: generateRandomId(),\n name, description\n };\n\n res.status(201).json(new ApiResponse(201, newExampleItem));\n } catch (error) {\n throw new ApiError(500, "Failed to create example item");\n }\n});`, }, { path: "src/routes/example.routes.ts", content: `import { Router } from "express";\nimport { getExample, postExample } from "../controllers/example.controllers";\n\nconst router = Router();\n\nrouter.get("/", getExample);\nrouter.post("/", postExample);\n\nexport default router;`, }, { path: "src/controllers/health.controllers.ts", content: `import { Request, Response } from "express";\nimport ApiError from "../utils/ApiError";\nimport ApiResponse from "../utils/ApiResponse";\nimport AsyncHandler from "../utils/AsyncHandler";\n\nexport const getHealth = AsyncHandler(async (req: Request, res: Response) => {\n try {\n res.status(200).json(new ApiResponse(200, { status: "OK" }, "Server is healthy"));\n } catch (error) {\n throw new ApiError(500, "Failed to get health status");\n }\n});`, }, { path: "src/routes/health.routes.ts", content: `import { Router } from "express";\nimport { getHealth } from "../controllers/health.controllers";\n\nconst router = Router();\n\nrouter.get("/", getHealth);\n\nexport default router;`, }, { path: "src/models/example.model.ts", content: `// Write your database model here`, }, { path: "src/types/example.types.ts", content: `export interface ExampleType {\n id: string;\n name: string;\n description: string;\n}`, }, { path: "src/utils/example.utils.ts", content: `export const capitalizeString = (str:string) => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();\n\nexport const generateRandomId = () => Math.random().toString(36).substring(2, 9);`, }, { path: "src/db/connection.ts", content: dbConnectionContent, }, { path: "src/utils/ApiError.ts", content: `class ApiError extends Error {\n statusCode: number;\n data: null | any;\n message: string;\n success: boolean;\n errors: any[];\n\n constructor(statusCode: number, message = "Something went wrong", errors: any[] = [], stack = "") {\n super(message);\n this.statusCode = statusCode;\n this.data = null;\n this.message = message;\n this.success = false;\n this.errors = errors;\n\n if (stack) {\n this.stack = stack;\n } else {\n Error.captureStackTrace(this, this.constructor);\n }\n }\n}\n\nexport default ApiError;`, }, { path: "src/utils/ApiResponse.ts", content: `class ApiResponse {\n statusCode: number;\n data: any;\n message: string;\n success: boolean;\n\n constructor(statusCode: number, data: any, message = "This is a success response") {\n this.statusCode = statusCode;\n this.data = data;\n this.message = message;\n this.success = statusCode < 400;\n }\n}\n\nexport default ApiResponse;`, }, { path: "src/utils/AsyncHandler.ts", content: `import { Request, Response, NextFunction } from "express";\n\ntype AsyncFunction = (req: Request, res: Response, next: NextFunction) => Promise<any>;\n\nconst AsyncHandler = (execution: AsyncFunction) => (req: Request, res: Response, next: NextFunction) => {\n Promise.resolve(execution(req, res, next)).catch((error) => {\n next(error);\n });\n};\n\nexport default AsyncHandler;\n`, }, { path: "src/index.ts", content: `import express, { NextFunction, Request, Response, Application } from "express";\nimport dotenv from "dotenv";\nimport cors from "cors";\nimport healthRoutes from "./routes/health.routes";\nimport exampleRoutes from "./routes/example.routes";\nimport ApiError from "./utils/ApiError";\n${extraImports}\n\ndotenv.config({path: ".env"});\n\nconst app: Application = express();\nconst port = process.env.PORT || 5555;\nconst allowedOrigins = "*"; // Allows anyone to access the API\n\napp.use(express.json({limit: "20kb"})); // Limit the body size to 20kb\napp.use(cors({ origin: allowedOrigins }));\napp.use(express.urlencoded({ extended: true }));\n\n// API routes\napp.use("/api/health", healthRoutes);\napp.use("/api/example", exampleRoutes);\n\n${databaseConnectionCode}\n${selectedFeatures.includes("swagger") ? ` // Swagger Documentation const swaggerOptions = { definition: { openapi: "3.0.0", info: { title: "Express TypeScript API", version: "1.0.0", description: "Express TypeScript API Documentation" }, servers: [ { url: "http://localhost:" + port, description: "Development server" } ] }, apis: ["./src/routes/*.ts"] }; const swaggerDocs = swaggerJsDoc(swaggerOptions); app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocs)); ` : ""} app.listen(port, ()=> {\n console.log(\`Server is running on port \${port}\`);\n});\n\napp.use((err: ApiError, req: Request, res: Response, next: NextFunction) => {\n res.status(err.statusCode || 500).json({ message: err.message, errors: err.errors });\n});\n\n`, }, { path: ".env", content: `PORT=5555${databaseType === "mongodb" ? "\nMONGODB_URI=mongodb://localhost:27017/YOUR_DATABASE" : ""}${databaseType === "postgres" ? "\nPGUSER=postgres\nPGHOST=localhost\nPGDATABASE=postgres\nPGPASSWORD=postgres\nPGPORT=5432" : ""}${databaseType === "mysql" ? "\nDB_HOST=localhost\nDB_USER=root\nDB_PASSWORD=password\nDB_NAME=database" : ""}`, }, { path: ".env.example", content: `PORT=5555${databaseType === "mongodb" ? "\nMONGODB_URI=mongodb://localhost:27017/YOUR_DATABASE" : ""}${databaseType === "postgres" ? "\nPGUSER=postgres\nPGHOST=localhost\nPGDATABASE=postgres\nPGPASSWORD=postgres\nPGPORT=5432" : ""}${databaseType === "mysql" ? "\nDB_HOST=localhost\nDB_USER=root\nDB_PASSWORD=password\nDB_NAME=database" : ""}`, }, { path: ".gitignore", content: `# Dependency directories\nnode_modules/\n\n# Build output\ndist/\n\n# Environment variables\n.env\n\n# Logs\n*.log\nlogs/\n\n# OS generated files\n.DS_Store\nThumbs.db\n\n# Editor directories and files\n.vscode/\n.idea/\n*.swp\n*.swo\n\n# TypeScript cache\n*.tsbuildinfo\n\n# npm debug logs\nnpm-debug.log*\n\n# yarn debug logs\nyarn-debug.log*\nyarn-error.log*\n\n# pnpm debug logs\npnpm-debug.log*`, }, { path: "package.json", content: `{ "name": "${projectName === "." ? path.basename(currentDir) : projectName.toLowerCase()}", "version": "1.0.0", "description": "Express TypeScript Starter", "main": "dist/index.js", "scripts": { "start": "node dist/index.js", "build": "tsc", "dev": "nodemon --exec ts-node src/index.ts", "lint": "eslint . --ext .ts"${selectedFeatures.includes("logging") ? ',\n "logs": "tail -f ./logs/app.log"' : ""} }, "keywords": [ "${projectName === "." ? path.basename(currentDir) : projectName}" ], "author": "Your name", "dependencies": { "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1"${Object.entries(extraDependencies).map(([key, value]) => `,\n "${key}": "${value}"`).join("")} }, "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/node": "^22.7.5", "@typescript-eslint/eslint-plugin": "^8.8.1", "@typescript-eslint/parser": "^8.8.1", "eslint": "^9.12.0", "nodemon": "^3.1.7", "ts-node": "^10.9.2", "typescript": "^5.6.3"${Object.entries(extraDevDependencies).map(([key, value]) => `,\n "${key}": "${value}"`).join("")} } }`, }, { path: ".eslintrc.json", content: `{\n "parser": "@typescript-eslint/parser",\n "extends": ["plugin:@typescript-eslint/recommended"],\n "parserOptions": {\n "ecmaVersion": 2018,\n "sourceType": "module"\n },\n "rules": {}\n}`, }, { path: "README.md", content: `# Express TypeScript Starter This is a starter project for building a backend application with Express and TypeScript. ## Features - Express server setup - TypeScript configuration - Nodemon for development - Health check route - ESLint for linting - Proper project structure${databaseType !== "none" ? `\n- ${databaseType === "mongodb" ? "MongoDB" : databaseType === "postgres" ? "PostgreSQL" : "MySQL"} database integration` : ""}${selectedFeatures.length > 0 ? `${selectedFeatures.includes("authentication") ? "\n- JWT Authentication" : ""}${selectedFeatures.includes("logging") ? "\n- Winston Logger" : ""}${selectedFeatures.includes("swagger") ? "\n- Swagger API Documentation" : ""}${selectedFeatures.includes("validation") ? "\n- Request Validation with Zod" : ""}` : ""} ## Installation \`\`\`bash cd ${projectName} ${packageManager} install \`\`\` ## Getting Started 1. Create a \`.env\` file in the root directory (copy from \`.env.example\`) 2. Run \`${packageManager}${packageManager === "npm" ? " run" : ""} dev\` to start the development server 3. Visit \`http://localhost:5555/api/health\` to check if the server is running${selectedFeatures.includes("swagger") ? "\n4. Visit `http://localhost:5555/api-docs` for API documentation" : ""} ## Scripts - \`${packageManager}${packageManager === "npm" ? " run" : ""} dev\`: Start the development server with Nodemon - \`${packageManager}${packageManager === "npm" ? " run" : ""} build\`: Build the TypeScript code - \`${packageManager}${packageManager === "npm" ? " run" : ""} start\`: Start the production server - \`${packageManager}${packageManager === "npm" ? " run" : ""} lint\`: Run ESLint${selectedFeatures.includes("logging") ? `\n- \`${packageManager}${packageManager === "npm" ? " run" : ""} logs\`: View application logs` : ""} `, }, { path: "tsconfig.json", content: `{\n "compilerOptions": {\n "target": "es6",\n "module": "commonjs",\n "outDir": "./dist",\n "rootDir": "./src",\n "strict": true,\n "esModuleInterop": true,\n "skipLibCheck": true,\n "forceConsistentCasingInFileNames": true\n },\n "include": ["src/**/*"],\n "exclude": ["node_modules", "**/*.spec.ts"]\n }`, }, ]; // Add logger file if logging is selected if (selectedFeatures.includes("logging")) { files.push({ path: "src/utils/logger.ts", content: `import winston from 'winston'; import 'winston-daily-rotate-file'; import path from 'path'; import fs from 'fs'; // Create logs directory if it doesn't exist const logDir = path.join(process.cwd(), 'logs'); if (!fs.existsSync(logDir)) { fs.mkdirSync(logDir); } const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), winston.format.splat(), winston.format.json() ), defaultMeta: { service: 'api' }, transports: [ new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.printf(({ timestamp, level, message, ...meta }) => { return \`\${timestamp} [\${level}]: \${message} \${Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''}\`; }) ) }), new winston.transports.DailyRotateFile({ filename: path.join(logDir, 'app-%DATE%.log'), datePattern: 'YYYY-MM-DD', zippedArchive: true, maxSize: '20m', maxFiles: '14d' }) ] }); export default logger;` }); } // Add auth middleware if authentication is selected if (selectedFeatures.includes("authentication")) { fs.mkdirSync(path.join(projectDir, "src", "middleware"), { recursive: true }); files.push({ path: "src/middleware/auth.middleware.ts", content: `import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; import ApiError from '../utils/ApiError'; interface JwtPayload { id: string; role: string; } interface AuthRequest extends Request { user?: JwtPayload; } export const verifyJWT = (req: AuthRequest, res: Response, next: NextFunction) => { try { // Get token from header const token = req.header('Authorization')?.replace('Bearer ', ''); if (!token) { throw new ApiError(401, 'No token, authorization denied'); } // Verify token const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret'; const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload; req.user = decoded; next(); } catch (error) { next(new ApiError(401, 'Token is not valid')); } }; export const isAdmin = (req: AuthRequest, res: Response, next: NextFunction) => { if (req.user && req.user.role === 'admin') { next(); } else { next(new ApiError(403, 'Admin access denied')); } };` }); } // Create project files files.forEach((file) => { const filePath = path.join(projectDir, file.path); const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, file.content); }); spinner.text = "Installing dependencies..."; // Initialize and install dependencies process.chdir(projectDir); let installCommand = ""; if (packageManager === "npm") { installCommand = "npm install"; } else if (packageManager === "yarn") { installCommand = "yarn"; } else if (packageManager === "pnpm") { installCommand = "pnpm install"; } execSync(installCommand, { stdio: "inherit" }); spinner.succeed("Project created successfully! 🎉"); // Print success message with a styled box console.log( boxen( chalk.green.bold("Project created successfully! 🎉") + "\n\n" + chalk.white("To get started:") + "\n\n" + chalk.cyan(` cd ${projectName}`) + "\n" + chalk.cyan(` ${packageManager}${packageManager === "npm" ? " run" : ""} dev`) + "\n\n" + chalk.white("Then visit:") + "\n" + chalk.cyan(` http://localhost:5555/api/health`) + chalk.white(" to verify the server is running") + (selectedFeatures.includes("swagger") ? "\n" + chalk.cyan(` http://localhost:5555/api-docs`) + chalk.white(" for API documentation") : ""), { padding: 1, margin: 1, borderStyle: "round", borderColor: "green" } ) ); } catch (error) { console.error(chalk.red("Error creating project:"), error); process.exit(1); } }; createProject();