@saiteja13/express-template
Version:
530 lines (475 loc) • 23.7 kB
JavaScript
// 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: `
},
{
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: `
This is a starter project for building a backend application with Express and TypeScript.
- 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" : ""}` : ""}
\`\`\`bash
cd ${projectName}
${packageManager} install
\`\`\`
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" : ""}
- \`${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();