create-node-prodkit 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/bin/create.js ADDED
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync } from "fs";
4
+ import { join, dirname } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { execSync } from "child_process";
7
+
8
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+
12
+ const green = (t) => `\x1b[32m${t}\x1b[0m`;
13
+ const cyan = (t) => `\x1b[36m${t}\x1b[0m`;
14
+ const yellow = (t) => `\x1b[33m${t}\x1b[0m`;
15
+ const bold = (t) => `\x1b[1m${t}\x1b[0m`;
16
+ const dim = (t) => `\x1b[2m${t}\x1b[0m`;
17
+
18
+ // ─── Argument parsing ─────────────────────────────────────────────────────────
19
+
20
+ const projectName = process.argv[2];
21
+
22
+ if (!projectName) {
23
+ console.error(`
24
+ ${bold("Usage:")}
25
+ npx create-node-prodkit ${cyan("<project-name>")}
26
+
27
+ ${bold("Example:")}
28
+ npx create-node-prodkit my-api
29
+ `);
30
+ process.exit(1);
31
+ }
32
+
33
+ if (!/^[a-z0-9-_]+$/i.test(projectName)) {
34
+ console.error(`\n Project name "${projectName}" contains invalid characters.\n Use only letters, numbers, hyphens, and underscores.\n`);
35
+ process.exit(1);
36
+ }
37
+
38
+ const targetDir = join(process.cwd(), projectName);
39
+
40
+ if (existsSync(targetDir)) {
41
+ console.error(`\n Directory "${projectName}" already exists. Choose a different name.\n`);
42
+ process.exit(1);
43
+ }
44
+
45
+ // ─── File copy ────────────────────────────────────────────────────────────────
46
+
47
+ const templateDir = join(__dirname, "../templates");
48
+
49
+ // Files where {{PROJECT_NAME}} should be replaced
50
+ const PLACEHOLDER_FILES = new Set(["package.json"]);
51
+
52
+ function copyDir(src, dest) {
53
+ mkdirSync(dest, { recursive: true });
54
+
55
+ for (const entry of readdirSync(src)) {
56
+ const srcPath = join(src, entry);
57
+ const destPath = join(dest, entry);
58
+
59
+ if (statSync(srcPath).isDirectory()) {
60
+ copyDir(srcPath, destPath);
61
+ } else {
62
+ let content = readFileSync(srcPath, "utf8");
63
+
64
+ if (PLACEHOLDER_FILES.has(entry)) {
65
+ content = content.replaceAll("{{PROJECT_NAME}}", projectName);
66
+ }
67
+
68
+ // Rename gitignore → .gitignore (npm strips dotfiles from tarballs)
69
+ const finalDest = entry === "gitignore"
70
+ ? join(dest, ".gitignore")
71
+ : destPath;
72
+
73
+ writeFileSync(finalDest, content, "utf8");
74
+ }
75
+ }
76
+ }
77
+
78
+ // ─── Bootstrap ────────────────────────────────────────────────────────────────
79
+
80
+ console.log(`
81
+ ${bold("create-node-prodkit")} ${dim("v" + JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf8")).version)}
82
+
83
+ Scaffolding project ${cyan(bold(projectName))} ...
84
+ `);
85
+
86
+ copyDir(templateDir, targetDir);
87
+ console.log(` ${green("✓")} Files copied`);
88
+
89
+ console.log(` ${yellow("⟳")} Installing dependencies ...`);
90
+
91
+ try {
92
+ execSync("npm install", { cwd: targetDir, stdio: "inherit" });
93
+ } catch {
94
+ console.error("\n npm install failed. Run it manually inside the project.\n");
95
+ process.exit(1);
96
+ }
97
+
98
+ console.log(`
99
+ ${green(bold("✓ Done!"))} Your project is ready.
100
+
101
+ ${dim("Next steps:")}
102
+
103
+ ${cyan(`cd ${projectName}`)}
104
+ ${cyan("cp .env.example .env")}
105
+ ${cyan("npm run dev")}
106
+
107
+ ${dim("Docs:")} README.md
108
+ `);
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "create-node-prodkit",
3
+ "version": "1.0.0",
4
+ "description": "Production-grade Node.js + Express skeleton CLI — scaffold a new project in seconds",
5
+ "license": "ISC",
6
+ "author": "vinay",
7
+ "type": "module",
8
+ "bin": {
9
+ "create-node-prodkit": "./bin/create.js"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "templates",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "dev": "nodemon src/server.js"
18
+ },
19
+ "keywords": [
20
+ "nodejs",
21
+ "express",
22
+ "scaffold",
23
+ "boilerplate",
24
+ "starter",
25
+ "cli",
26
+ "production"
27
+ ],
28
+ "nodemonConfig": {
29
+ "watch": ["src"],
30
+ "ext": "js,json",
31
+ "ignore": ["logs/*"],
32
+ "exec": "node src/server.js"
33
+ },
34
+ "dependencies": {
35
+ "axios": "^1.13.5",
36
+ "dotenv": "^17.3.1",
37
+ "express": "^5.2.1",
38
+ "express-rate-limit": "^8.2.1"
39
+ },
40
+ "devDependencies": {
41
+ "eslint": "^9.39.2",
42
+ "nodemon": "^3.1.13",
43
+ "prettier": "^3.8.1"
44
+ },
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ }
48
+ }
@@ -0,0 +1,28 @@
1
+ # Server
2
+ PORT=3000
3
+
4
+ # API
5
+ API_TIMEOUT=5000
6
+
7
+ # Logging
8
+ # internal → writes to logs/ directory
9
+ # external → POSTs to LOG_SERVICE_URL
10
+ LOG_MODE=internal
11
+ LOG_SERVICE_URL=
12
+
13
+ # LOG_TYPE controls sensitive field handling in logs
14
+ # 1 = show values as-is (development)
15
+ # 2 = mask values (staging)
16
+ # 3 = remove keys entirely (production)
17
+ LOG_TYPE=1
18
+
19
+ # Database (uncomment and fill in what you need)
20
+ # MONGO_URI=mongodb://localhost:27017/mydb
21
+ # MYSQL_HOST=localhost
22
+ # MYSQL_USER=root
23
+ # MYSQL_PASSWORD=
24
+ # MYSQL_DATABASE=mydb
25
+ # PG_HOST=localhost
26
+ # PG_USER=postgres
27
+ # PG_PASSWORD=
28
+ # PG_DATABASE=mydb
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ .env
3
+ logs/
4
+ *.log
5
+ *.backup
6
+ .DS_Store
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "license": "ISC",
6
+ "author": "",
7
+ "type": "module",
8
+ "main": "src/server.js",
9
+ "scripts": {
10
+ "dev": "nodemon src/server.js",
11
+ "start": "node src/server.js"
12
+ },
13
+ "nodemonConfig": {
14
+ "watch": ["src"],
15
+ "ext": "js,json",
16
+ "ignore": ["logs/*"],
17
+ "exec": "node src/server.js"
18
+ },
19
+ "dependencies": {
20
+ "axios": "^1.13.5",
21
+ "dotenv": "^17.3.1",
22
+ "express": "^5.2.1",
23
+ "express-rate-limit": "^8.2.1"
24
+ },
25
+ "devDependencies": {
26
+ "eslint": "^9.39.2",
27
+ "nodemon": "^3.1.13",
28
+ "prettier": "^3.8.1"
29
+ },
30
+ "engines": {
31
+ "node": ">=18.0.0"
32
+ }
33
+ }
@@ -0,0 +1,21 @@
1
+ import express from "express";
2
+ import routes from "./routes/index.route.js";
3
+ import { errorMiddleware } from "./middlewares/error.middleware.js";
4
+ import { ipMiddleware } from "./middlewares/ip.middleware.js";
5
+ import { rateLimitMiddleware } from "./middlewares/rateLimit.middleware.js";
6
+
7
+ const app = express();
8
+
9
+ app.use(express.json());
10
+ app.use(rateLimitMiddleware);
11
+ app.use(ipMiddleware);
12
+
13
+ app.get("/", (req, res) => {
14
+ res.send("Welcome to the API!");
15
+ });
16
+
17
+ app.use("/api/v1", routes);
18
+
19
+ app.use(errorMiddleware);
20
+
21
+ export default app;
@@ -0,0 +1,25 @@
1
+ import dotenv from "dotenv";
2
+ dotenv.config();
3
+
4
+ export default {
5
+ port: process.env.PORT || 3000,
6
+
7
+ api: {
8
+ timeout: Number(process.env.API_TIMEOUT) || 5000,
9
+ },
10
+
11
+ rateLimit: {
12
+ windowMs: 15 * 60 * 1000, // 15 minutes
13
+ max: 100, // max requests per window per IP
14
+ },
15
+
16
+ logging: {
17
+ mode: process.env.LOG_MODE || "internal", // internal | external
18
+ externalUrl: process.env.LOG_SERVICE_URL || "",
19
+ directory: "logs",
20
+ fileName: "app",
21
+ maxSize: 5 * 1024 * 1024,
22
+ dailyRotate: true,
23
+ logType: Number(process.env.LOG_TYPE) || 1
24
+ },
25
+ };
@@ -0,0 +1,11 @@
1
+ import { asyncHandler } from "../utils/asyncHandler.js";
2
+ import { ResponseUtil } from "../utils/response.util.js";
3
+ import { sampleService } from "../services/sample.service.js";
4
+
5
+ export const getSample = asyncHandler(async (req, res) => {
6
+
7
+ const data = await sampleService();
8
+
9
+ return ResponseUtil.success(res, "Posts fetched successfully", data);
10
+
11
+ });
@@ -0,0 +1,14 @@
1
+ // MongoDB connection setup
2
+ // Install: npm install mongoose
3
+ //
4
+ // import mongoose from "mongoose";
5
+ // import config from "../config/app.config.js";
6
+ //
7
+ // export const connectMongo = async () => {
8
+ // await mongoose.connect(config.db.uri);
9
+ // console.log("MongoDB connected");
10
+ // };
11
+ //
12
+ // Then in server.js:
13
+ // import { connectMongo } from "./db/mongo.db.js";
14
+ // connectMongo().then(() => app.listen(config.port, ...));
@@ -0,0 +1,17 @@
1
+ // MySQL connection setup
2
+ // Install: npm install mysql2
3
+ //
4
+ // import mysql from "mysql2/promise";
5
+ // import config from "../config/app.config.js";
6
+ //
7
+ // export const db = mysql.createPool({
8
+ // host: config.db.host,
9
+ // user: config.db.user,
10
+ // password: config.db.password,
11
+ // database: config.db.database,
12
+ // });
13
+ //
14
+ // export const connectMySQL = async () => {
15
+ // await db.getConnection();
16
+ // console.log("MySQL connected");
17
+ // };
@@ -0,0 +1,20 @@
1
+ // PostgreSQL connection setup
2
+ // Install: npm install pg
3
+ //
4
+ // import pg from "pg";
5
+ // import config from "../config/app.config.js";
6
+ //
7
+ // const { Pool } = pg;
8
+ //
9
+ // export const db = new Pool({
10
+ // host: config.db.host,
11
+ // user: config.db.user,
12
+ // password: config.db.password,
13
+ // database: config.db.database,
14
+ // port: config.db.port || 5432,
15
+ // });
16
+ //
17
+ // export const connectPostgres = async () => {
18
+ // await db.connect();
19
+ // console.log("PostgreSQL connected");
20
+ // };
@@ -0,0 +1,4 @@
1
+ export const encryptMiddleware = (req, res, next) => {
2
+ // Placeholder for encryption logic
3
+ next();
4
+ };
@@ -0,0 +1,23 @@
1
+ import { ResponseUtil } from "../utils/response.util.js";
2
+ import { logService } from "../utils/log.util.js";
3
+
4
+ export const errorMiddleware = async (err, req, res, next) => {
5
+ await logService("error", err.message, {
6
+ stack: err.stack
7
+ });
8
+
9
+ switch (err.status) {
10
+ case 400:
11
+ return ResponseUtil.badRequest(res, err.message);
12
+ case 401:
13
+ return ResponseUtil.unauthorized(res, err.message);
14
+ case 403:
15
+ return ResponseUtil.forbidden(res, err.message);
16
+ case 404:
17
+ return ResponseUtil.notFound(res, err.message);
18
+ case 422:
19
+ return ResponseUtil.validation(res, err.message, err.errors);
20
+ default:
21
+ return ResponseUtil.exception(res, err.message);
22
+ }
23
+ };
@@ -0,0 +1,20 @@
1
+ import { ResponseUtil } from "../utils/response.util.js";
2
+
3
+ export const ipMiddleware = (req, res, next) => {
4
+ const allowedIps = [
5
+ "127.0.0.1",
6
+ "::1",
7
+ "192.168.1.10"
8
+ ];
9
+
10
+ const clientIp =
11
+ req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
12
+ req.socket.remoteAddress;
13
+
14
+ if (!allowedIps.includes(clientIp)) {
15
+ return ResponseUtil.forbidden(res, "Access denied: Unauthorized IP address");
16
+
17
+ }
18
+
19
+ next();
20
+ };
@@ -0,0 +1,18 @@
1
+ import rateLimit from "express-rate-limit";
2
+ import config from "../config/app.config.js";
3
+ import { ResponseUtil } from "../utils/response.util.js";
4
+
5
+ export const rateLimitMiddleware = rateLimit({
6
+ windowMs: config.rateLimit.windowMs,
7
+ max: config.rateLimit.max,
8
+ standardHeaders: true,
9
+ legacyHeaders: false,
10
+
11
+ handler: (req, res) => {
12
+ return ResponseUtil.send(
13
+ res,
14
+ 429,
15
+ "Too many requests. Please try again later."
16
+ );
17
+ }
18
+ });
File without changes
@@ -0,0 +1,10 @@
1
+ import { apiCall } from "../utils/axios.util.js";
2
+
3
+ export const sampleRepository = {
4
+ getPosts: async () => {
5
+ return apiCall({
6
+ method: "GET",
7
+ url: `https://jsonplaceholder.typicode.com/posts`
8
+ });
9
+ }
10
+ };
@@ -0,0 +1,8 @@
1
+ import express from "express";
2
+ import sampleRoutes from "./sample1.route.js";
3
+
4
+ const router = express.Router();
5
+
6
+ router.use("/sample", sampleRoutes);
7
+
8
+ export default router;
@@ -0,0 +1,8 @@
1
+ import express from "express";
2
+ import { getSample } from "../controllers/sample.controller.js";
3
+
4
+ const router = express.Router();
5
+
6
+ router.get("/posts", getSample);
7
+
8
+ export default router;
@@ -0,0 +1,6 @@
1
+ import app from "./app.js";
2
+ import config from "./config/app.config.js";
3
+
4
+ app.listen(config.port, () => {
5
+ console.log(`Server running on port: ${config.port}`);
6
+ });
@@ -0,0 +1,17 @@
1
+ import { sampleRepository } from "../repositories/sample.repository.js";
2
+ import { logService } from "../utils/log.util.js";
3
+
4
+ export const sampleService = async () => {
5
+
6
+ const data = await sampleRepository.getPosts();
7
+ await logService("info", "All posts fetched from repository", { count: data.length, posts: data});
8
+
9
+ if (!data) {
10
+ const error = new Error("Data not found");
11
+ error.status = 404;
12
+ throw error;
13
+ }
14
+
15
+
16
+ return data;
17
+ };
@@ -0,0 +1,5 @@
1
+ export const asyncHandler = (fn) => {
2
+ return (req, res, next) => {
3
+ Promise.resolve(fn(req, res, next)).catch(next);
4
+ };
5
+ };
@@ -0,0 +1,19 @@
1
+ import axios from "axios";
2
+ import config from "../config/app.config.js";
3
+
4
+ export const apiCall = async (options) => {
5
+ try {
6
+ const response = await axios({
7
+ ...options,
8
+ timeout: config.api.timeout,
9
+ });
10
+
11
+ return response.data;
12
+ } catch (error) {
13
+ attempts++;
14
+
15
+ if (attempts >= config.api.retries) {
16
+ throw error;
17
+ }
18
+ }
19
+ };
@@ -0,0 +1,51 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import axios from "axios";
4
+ import config from "../config/app.config.js";
5
+ import { sanitizeLogData } from "./logSanitizer.util.js";
6
+
7
+ const logDir = config.logging.directory;
8
+
9
+ if (!fs.existsSync(logDir)) {
10
+ fs.mkdirSync(logDir);
11
+ }
12
+
13
+ const getFileName = () => {
14
+ if (config.logging.dailyRotate) {
15
+ const date = new Date().toISOString().split("T")[0];
16
+ return `${config.logging.fileName}-${date}.log`;
17
+ }
18
+ return `${config.logging.fileName}.log`;
19
+ };
20
+
21
+ export const logService = async (level, message, meta = {}) => {
22
+ const sanitizedMeta = sanitizeLogData(meta);
23
+
24
+ const logData = {
25
+ level,
26
+ message,
27
+ timestamp: new Date().toLocaleString("en-IN", { timeZone: "Asia/Kolkata" }),
28
+ meta: sanitizedMeta,
29
+ };
30
+
31
+ // External log mode
32
+ if (config.logging.mode === "external" && config.logging.externalUrl) {
33
+ try {
34
+ await axios.post(config.logging.externalUrl, logData);
35
+ return;
36
+ } catch (err) {
37
+ console.error("External logging failed");
38
+ }
39
+ }
40
+
41
+ // Internal file logging
42
+ const filePath = path.join(logDir, getFileName());
43
+ const logLine = JSON.stringify(logData) + "\n";
44
+
45
+ fs.appendFileSync(filePath, logLine);
46
+
47
+ const stats = fs.statSync(filePath);
48
+ if (stats.size > config.logging.maxSize) {
49
+ fs.renameSync(filePath, `${filePath}.backup`);
50
+ }
51
+ };
@@ -0,0 +1,50 @@
1
+ import { sensitiveKeys } from "./sensitiveKeys.util.js";
2
+ import config from "../config/app.config.js";
3
+
4
+ const maskValue = (value) => {
5
+ if (typeof value !== "string") return "******";
6
+ if (value.length <= 4) return "****";
7
+ return value.slice(0, 2) + "****" + value.slice(-2);
8
+ };
9
+
10
+ export const sanitizeLogData = (data) => {
11
+ if (!data || typeof data !== "object") return data;
12
+
13
+ const processObject = (obj) => {
14
+ if (Array.isArray(obj)) {
15
+ return obj.map(processObject);
16
+ }
17
+
18
+ const newObj = {};
19
+
20
+ for (const key in obj) {
21
+ const value = obj[key];
22
+
23
+ if (sensitiveKeys.includes(key)) {
24
+ // LOG_TYPE 1 → show everything
25
+ if (config.logging.logType === 1) {
26
+ newObj[key] = value;
27
+ }
28
+
29
+ // LOG_TYPE 2 → mask
30
+ else if (config.logging.logType === 2) {
31
+ newObj[key] = maskValue(value);
32
+ }
33
+
34
+ // LOG_TYPE 3 → remove
35
+ else if (config.logging.logType === 3) {
36
+ continue;
37
+ }
38
+ } else {
39
+ newObj[key] =
40
+ typeof value === "object" && value !== null
41
+ ? processObject(value)
42
+ : value;
43
+ }
44
+ }
45
+
46
+ return newObj;
47
+ };
48
+
49
+ return processObject(data);
50
+ };
@@ -0,0 +1,43 @@
1
+ export const ResponseUtil = {
2
+ send(res, statusCode, message, data = null, errors = null) {
3
+ return res.status(statusCode).json({
4
+ success: statusCode < 400,
5
+ statusCode,
6
+ message,
7
+ data,
8
+ errors
9
+ });
10
+ },
11
+
12
+ success(res, message = "Success", data = null) {
13
+ return this.send(res, 200, message, data);
14
+ },
15
+
16
+ created(res, message = "Created successfully", data = null) {
17
+ return this.send(res, 201, message, data);
18
+ },
19
+
20
+ badRequest(res, message = "Bad request", errors = null) {
21
+ return this.send(res, 400, message, null, errors);
22
+ },
23
+
24
+ unauthorized(res, message = "Unauthorized") {
25
+ return this.send(res, 401, message);
26
+ },
27
+
28
+ forbidden(res, message = "Forbidden") {
29
+ return this.send(res, 403, message);
30
+ },
31
+
32
+ notFound(res, message = "Resource not found") {
33
+ return this.send(res, 404, message);
34
+ },
35
+
36
+ validation(res, message = "Validation failed", errors = []) {
37
+ return this.send(res, 422, message, null, errors);
38
+ },
39
+
40
+ exception(res, message = "Internal server error") {
41
+ return this.send(res, 500, message);
42
+ }
43
+ };
@@ -0,0 +1,3 @@
1
+ export const sensitiveKeys = [
2
+ "userId", "id"
3
+ ];
File without changes