@ifecodes/backend-template 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.
Files changed (54) hide show
  1. package/README.md +288 -0
  2. package/bin/cli.js +249 -0
  3. package/bin/lib/microservice-config.js +89 -0
  4. package/bin/lib/prompts.js +159 -0
  5. package/bin/lib/readme-generator.js +245 -0
  6. package/bin/lib/service-setup.js +316 -0
  7. package/package.json +52 -0
  8. package/template/base/.env.example +5 -0
  9. package/template/base/.eslintrc.json +17 -0
  10. package/template/base/.husky/pre-commit +13 -0
  11. package/template/base/.prettierignore +47 -0
  12. package/template/base/.prettierrc +7 -0
  13. package/template/base/eslint.config.js +33 -0
  14. package/template/base/package.json +32 -0
  15. package/template/base/src/app.ts +9 -0
  16. package/template/base/src/config/db.ts +4 -0
  17. package/template/base/src/config/env.ts +9 -0
  18. package/template/base/src/config/index.ts +2 -0
  19. package/template/base/src/middlewares/index.ts +3 -0
  20. package/template/base/src/middlewares/method-not-allowed.middleware.ts +17 -0
  21. package/template/base/src/middlewares/not-found.middleware.ts +8 -0
  22. package/template/base/src/middlewares/root.middleware.ts +14 -0
  23. package/template/base/src/modules/index.ts +8 -0
  24. package/template/base/src/modules/v1/health/health.controller.ts +18 -0
  25. package/template/base/src/modules/v1/health/health.route.ts +9 -0
  26. package/template/base/src/modules/v1/health/index.ts +1 -0
  27. package/template/base/src/modules/v1/index.ts +8 -0
  28. package/template/base/src/routes.ts +15 -0
  29. package/template/base/src/server.ts +11 -0
  30. package/template/base/src/utils/http-error.ts +47 -0
  31. package/template/base/src/utils/index.ts +11 -0
  32. package/template/base/src/utils/logger.ts +34 -0
  33. package/template/base/tsconfig.json +22 -0
  34. package/template/features/auth/argon2/inject.js +25 -0
  35. package/template/features/auth/base/inject.js +89 -0
  36. package/template/features/auth/bcrypt/inject.js +18 -0
  37. package/template/features/auth/models/index.ts +1 -0
  38. package/template/features/auth/models/user.model.ts +28 -0
  39. package/template/features/auth/modules/auth.controller.ts +20 -0
  40. package/template/features/auth/modules/auth.routes.ts +11 -0
  41. package/template/features/auth/modules/auth.service.ts +38 -0
  42. package/template/features/auth/modules/index.ts +1 -0
  43. package/template/features/auth/utils/hash.ts +20 -0
  44. package/template/features/auth/utils/jwt.ts +15 -0
  45. package/template/features/cors/inject.js +9 -0
  46. package/template/features/helmet/inject.js +3 -0
  47. package/template/features/morgan/inject.js +3 -0
  48. package/template/features/rate-limit/inject.js +3 -0
  49. package/template/gateway/app.ts +26 -0
  50. package/template/gateway/inject.js +27 -0
  51. package/template/gateway/server.ts +19 -0
  52. package/template/microservice/docker/Dockerfile +5 -0
  53. package/template/microservice/docker/docker-compose.yml +6 -0
  54. package/template/microservice/nodocker/pm2.config.js +3 -0
@@ -0,0 +1,11 @@
1
+ import app from "./app";
2
+ import { ENV } from "./config";
3
+ /*__DB_IMPORT__*/
4
+
5
+ const PORT = ENV.PORT || 3000;
6
+
7
+ /*__DB_CONNECT__*/
8
+
9
+ app.listen(PORT, () => {
10
+ console.log(`Server is running on port ${PORT}`);
11
+ });
@@ -0,0 +1,47 @@
1
+ export class HttpError extends Error {
2
+ public status: number;
3
+
4
+ constructor(status: number, message: string) {
5
+ super(message);
6
+ this.status = status;
7
+ Object.setPrototypeOf(this, new.target.prototype);
8
+ }
9
+ }
10
+
11
+ export class BadRequestError extends HttpError {
12
+ constructor(message = "Bad Request") {
13
+ super(400, message);
14
+ }
15
+ }
16
+
17
+ export class UnauthorizedError extends HttpError {
18
+ constructor(message = "Unauthorized") {
19
+ super(401, message);
20
+ }
21
+ }
22
+
23
+ export class ForbiddenError extends HttpError {
24
+ constructor(message = "Forbidden") {
25
+ super(403, message);
26
+ }
27
+ }
28
+
29
+ export class NotFoundError extends HttpError {
30
+ constructor(message = "Not Found") {
31
+ super(404, message);
32
+ }
33
+ }
34
+
35
+ export class ConflictError extends HttpError {
36
+ constructor(message = "Conflict") {
37
+ super(409, message);
38
+ }
39
+ }
40
+
41
+ export class InternalServerError extends HttpError {
42
+ constructor(message = "Internal Server Error") {
43
+ super(500, message);
44
+ }
45
+ }
46
+
47
+ export default HttpError;
@@ -0,0 +1,11 @@
1
+ export {
2
+ HttpError,
3
+ BadRequestError,
4
+ UnauthorizedError,
5
+ ForbiddenError,
6
+ NotFoundError,
7
+ ConflictError,
8
+ InternalServerError,
9
+ } from "./http-error";
10
+
11
+ export { default as logger } from "./logger";
@@ -0,0 +1,34 @@
1
+ import { ENV } from "@/config";
2
+
3
+ function format(tag: string, args: unknown[]) {
4
+ return [`%c[${tag}]`, "color:#6366f1;font-weight:600", ...args];
5
+ }
6
+
7
+ const environment =
8
+ ENV.NODE_ENV === "development" || ENV.NODE_ENV === "staging"
9
+ ? "development"
10
+ : ENV.NODE_ENV;
11
+
12
+ const logger = {
13
+ log(tag: string, ...args: unknown[]) {
14
+ if (environment !== "development") return;
15
+ console.log(...format(tag, args));
16
+ },
17
+
18
+ info(tag: string, ...args: unknown[]) {
19
+ if (environment !== "development") return;
20
+ console.info(...format(tag, args));
21
+ },
22
+
23
+ warn(tag: string, ...args: unknown[]) {
24
+ if (environment !== "development") return;
25
+ console.warn(...format(tag, args));
26
+ },
27
+
28
+ error(tag: string, ...args: unknown[]) {
29
+ if (environment !== "development") return;
30
+ console.error(...format(tag, args));
31
+ },
32
+ };
33
+
34
+ export default logger;
@@ -0,0 +1,22 @@
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ "target": "ES2020",
5
+ "module": "CommonJS",
6
+ "rootDir": "src",
7
+ "outDir": "dist",
8
+
9
+ "strict": true,
10
+ "noImplicitAny": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+
14
+ "baseUrl": "src",
15
+ "paths": {
16
+ "@/*": ["*"]
17
+ },
18
+ "types": ["node"]
19
+ },
20
+ "include": ["src"],
21
+ "exclude": ["node_modules", "dist"]
22
+ }
@@ -0,0 +1,25 @@
1
+ export const deps = ["argon2"];
2
+
3
+ export const files = {
4
+ "src/utils/hash.ts": `
5
+ import argon2 from "argon2";
6
+
7
+ const HASH_OPTIONS = {
8
+ type: argon2.argon2id,
9
+ memoryCost: 2 ** 16,
10
+ timeCost: 3,
11
+ parallelism: 1,
12
+ };
13
+
14
+ export const hashPassword = async (password: string): Promise<string> => {
15
+ return argon2.hash(password, HASH_OPTIONS);
16
+ };
17
+
18
+ export const verifyPassword = async (
19
+ hash: string,
20
+ password: string,
21
+ ): Promise<boolean> => {
22
+ return argon2.verify(hash, password);
23
+ };
24
+ `
25
+ };
@@ -0,0 +1,89 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+
7
+ // Read all module files
8
+ const modulesDir = path.join(__dirname, "../modules");
9
+ const modelsDir = path.join(__dirname, "../models");
10
+ const utilsDir = path.join(__dirname, "../utils");
11
+
12
+ const readDirFiles = (dir, prefix = "") => {
13
+ const files = {};
14
+ const items = fs.readdirSync(dir);
15
+
16
+ for (const item of items) {
17
+ const fullPath = path.join(dir, item);
18
+ const relativePath = path.join(prefix, item);
19
+
20
+ if (fs.statSync(fullPath).isDirectory()) {
21
+ Object.assign(files, readDirFiles(fullPath, relativePath));
22
+ } else {
23
+ files[relativePath] = fs.readFileSync(fullPath, "utf8");
24
+ }
25
+ }
26
+
27
+ return files;
28
+ };
29
+
30
+ const moduleFiles = readDirFiles(modulesDir, "src/modules/v1/auth");
31
+ const modelFiles = readDirFiles(modelsDir, "src/models");
32
+ const utilFiles = readDirFiles(utilsDir, "src/utils");
33
+
34
+ // Add MongoDB-enabled db.ts
35
+ const dbContent = `import mongoose from "mongoose";
36
+ import { ENV } from "./env";
37
+ import { logger } from "@/utils";
38
+
39
+ export const connectDB = async () => {
40
+ await mongoose.connect(ENV.MONGO_URI);
41
+ logger.log("db", "MongoDB connected");
42
+ };
43
+ `;
44
+
45
+ // Add MongoDB health check
46
+ const healthControllerContent = `import { Request, Response } from "express";
47
+ import mongoose from "mongoose";
48
+ import { logger } from "@/utils";
49
+
50
+ export const healthCheck = async (_: Request, res: Response) => {
51
+ const mongoState = mongoose.connection.readyState;
52
+ const healthy = mongoState === 1;
53
+
54
+ const failed: string[] = [];
55
+ if (mongoState !== 1) failed.push("mongodb");
56
+
57
+ logger.info("Health", healthy ? "healthy" : "unhealthy");
58
+
59
+ return res.status(healthy ? 200 : 503).json({
60
+ status: healthy ? "healthy" : "unhealthy",
61
+ uptime: process.uptime(),
62
+ timestamp: new Date().toISOString(),
63
+ services: {
64
+ mongodb: mongoState === 1 ? "connected" : "disconnected",
65
+ memory: {
66
+ rss: process.memoryUsage().rss,
67
+ heapUsed: process.memoryUsage().heapUsed,
68
+ },
69
+ },
70
+ failed,
71
+ });
72
+ };
73
+ `;
74
+
75
+ export const files = {
76
+ ...moduleFiles,
77
+ ...modelFiles,
78
+ ...utilFiles,
79
+ "src/config/db.ts": dbContent,
80
+ "src/modules/v1/health/health.controller.ts": healthControllerContent,
81
+ };
82
+
83
+ export const imports = `import { authRoutes } from "./auth";`;
84
+
85
+ export const middleware = `router.use("/auth", authRoutes);`;
86
+
87
+ export const deps = ["jsonwebtoken", "@types/jsonwebtoken", "mongoose"];
88
+
89
+ export const targetFile = "src/modules/v1/index.ts";
@@ -0,0 +1,18 @@
1
+ export const deps = ["bcrypt"];
2
+
3
+ export const files = {
4
+ "src/utils/hash.ts": `
5
+ import bcrypt from "bcrypt";
6
+
7
+ export const hashPassword = async (password: string): Promise<string> => {
8
+ return bcrypt.hash(password, 12);
9
+ };
10
+
11
+ export const verifyPassword = async (
12
+ hash: string,
13
+ password: string,
14
+ ): Promise<boolean> => {
15
+ return bcrypt.compare(password, hash);
16
+ };
17
+ `
18
+ };
@@ -0,0 +1 @@
1
+ export { UserModel, IUser } from "./user.model";
@@ -0,0 +1,28 @@
1
+ import { Schema, model, Document } from "mongoose";
2
+
3
+ export interface IUser extends Document {
4
+ email: string;
5
+ password: string;
6
+ fullName: string;
7
+ }
8
+
9
+ const userSchema = new Schema<IUser>(
10
+ {
11
+ email: {
12
+ type: String,
13
+ required: true,
14
+ unique: true,
15
+ },
16
+ password: {
17
+ type: String,
18
+ required: true,
19
+ },
20
+ fullName: {
21
+ type: String,
22
+ required: true,
23
+ },
24
+ },
25
+ { timestamps: true },
26
+ );
27
+
28
+ export const UserModel = model<IUser>("User", userSchema);
@@ -0,0 +1,20 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ import { loginService, registerService } from "./auth.service";
3
+
4
+ export const login = async (req: Request, res: Response, next: NextFunction) => {
5
+ try {
6
+ const token = await loginService(req.body);
7
+ return res.status(200).json({ token });
8
+ } catch (err) {
9
+ return next(err);
10
+ }
11
+ };
12
+
13
+ export const register = async (req: Request, res: Response, next: NextFunction) => {
14
+ try {
15
+ const token = await registerService(req.body);
16
+ return res.status(201).json({ token });
17
+ } catch (err) {
18
+ return next(err);
19
+ }
20
+ };
@@ -0,0 +1,11 @@
1
+ import { Router } from "express";
2
+ import { register, login } from "./auth.controller";
3
+ import { methodNotAllowedHandler } from "@/middlewares";
4
+
5
+ const router = Router();
6
+ router.use(methodNotAllowedHandler(["POST"]));
7
+ router.post("/login", login);
8
+ router.post("/register", register);
9
+
10
+ export default router;
11
+
@@ -0,0 +1,38 @@
1
+ import { generateToken } from "@/utils";
2
+ import { UserModel } from "@/models/user.model";
3
+ import { hashPassword, verifyPassword } from "@/utils";
4
+ import { UnauthorizedError, ConflictError } from "@/utils";
5
+
6
+ export const loginService = async ({
7
+ email,
8
+ password,
9
+ }: {
10
+ email: string;
11
+ password: string;
12
+ }) => {
13
+ const user = await UserModel.findOne({ email }).exec();
14
+ if (!user) throw new UnauthorizedError("User not found");
15
+
16
+ const match = await verifyPassword(user.password, password);
17
+ if (!match) throw new UnauthorizedError("Invalid credentials");
18
+
19
+ return generateToken({ email: user.email });
20
+ };
21
+
22
+ export const registerService = async ({
23
+ email,
24
+ password,
25
+ fullName,
26
+ }: {
27
+ email: string;
28
+ password: string;
29
+ fullName: string;
30
+ }) => {
31
+ const exists = await UserModel.findOne({ email }).exec();
32
+ if (exists) throw new ConflictError("Email already registered");
33
+
34
+ const hashed = await hashPassword(password);
35
+ const user = await UserModel.create({ email, password: hashed, fullName });
36
+
37
+ return generateToken({ email: user.email });
38
+ };
@@ -0,0 +1 @@
1
+ export { default as authRoutes } from "./auth.routes";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * This base hash utility defines generic hashing function signatures.
3
+ * The actual implementation will be replaced by bcrypt or argon2 depending
4
+ * on user selection during template generation.
5
+ */
6
+
7
+ export async function hashPassword(_password: string): Promise<string> {
8
+ throw new Error(
9
+ "hashPassword() not implemented — hashing method not selected (bcrypt or argon2)"
10
+ );
11
+ }
12
+
13
+ export async function verifyPassword(
14
+ _hashed: string,
15
+ _password: string
16
+ ): Promise<boolean> {
17
+ throw new Error(
18
+ "verifyPassword() not implemented — hashing method not selected (bcrypt or argon2)"
19
+ );
20
+ }
@@ -0,0 +1,15 @@
1
+ import jwt from "jsonwebtoken";
2
+ import { ENV } from "@/config";
3
+
4
+ // JwtPayload mirrors the global JwtPayload declared in `src/types/express.d.ts`.
5
+ export type JwtPayload = {
6
+ email?: string;
7
+ iat?: number;
8
+ exp?: number;
9
+ };
10
+
11
+ export const generateToken = (payload: JwtPayload) =>
12
+ jwt.sign(payload, ENV.JWT_SECRET, { expiresIn: "7d" });
13
+
14
+ export const verifyToken = (token: string): JwtPayload =>
15
+ jwt.verify(token, ENV.JWT_SECRET) as JwtPayload;
@@ -0,0 +1,9 @@
1
+ export const deps = ["cors", "@types/cors"];
2
+ export const imports = `import cors from "cors";`;
3
+ export const middleware = `
4
+ const corsOptions = {
5
+ origin: ENV.ALLOWED_ORIGIN,
6
+ credentials: true,
7
+ };
8
+
9
+ app.use(cors(corsOptions));`;
@@ -0,0 +1,3 @@
1
+ export const deps = ["helmet"];
2
+ export const imports = `import helmet from "helmet";`;
3
+ export const middleware = `app.use(helmet());`;
@@ -0,0 +1,3 @@
1
+ export const deps = ["morgan", "@types/morgan"];
2
+ export const imports = `import morgan from "morgan";`;
3
+ export const middleware = `app.use(morgan("dev"));`;
@@ -0,0 +1,3 @@
1
+ export const deps = ["express-rate-limit"];
2
+ export const imports = `import rateLimit from "express-rate-limit";`;
3
+ export const middleware = `app.use(rateLimit({ windowMs: 15*60*1000, max: 100 }));`;
@@ -0,0 +1,26 @@
1
+ import express, { Request, Response, NextFunction } from "express";
2
+ import { createProxyMiddleware } from "http-proxy-middleware";
3
+ import { logger } from "@/utils";
4
+
5
+ const app = express();
6
+
7
+ // Health check for the gateway itself
8
+ app.get("/health", (_req: Request, res: Response) => {
9
+ res.json({ status: "ok", service: "gateway" });
10
+ });
11
+
12
+ // Service routes - will be dynamically configured
13
+ /*__ROUTES__*/
14
+
15
+ // 404 handler
16
+ app.use((req: Request, res: Response) => {
17
+ res.status(404).json({ error: "Route not found" });
18
+ });
19
+
20
+ // Error handler
21
+ app.use((err: any, req: Request, res: Response, next: NextFunction) => {
22
+ logger.error("Gateway error:", err);
23
+ res.status(500).json({ error: "Internal gateway error" });
24
+ });
25
+
26
+ export default app;
@@ -0,0 +1,27 @@
1
+ export const gatewayDeps = ["http-proxy-middleware"];
2
+
3
+ export const generateGatewayRoutes = (services) => {
4
+ const routes = services
5
+ .filter(s => s !== "gateway")
6
+ .map((service, index) => {
7
+ const port = 4001 + index; // Gateway is 4000, services start at 4001
8
+ const routePath = service.replace("-service", "");
9
+
10
+ return `
11
+ // Route ${service} to port ${port}
12
+ app.use("/${routePath}", createProxyMiddleware({
13
+ target: "http://localhost:${port}",
14
+ changeOrigin: true,
15
+ pathRewrite: {
16
+ "^/${routePath}": "",
17
+ },
18
+ onError: (err, req, res) => {
19
+ logger.error(\`Proxy error for ${service}:\`, err);
20
+ res.status(503).json({ error: "Service unavailable" });
21
+ },
22
+ }));`;
23
+ })
24
+ .join("\n");
25
+
26
+ return routes;
27
+ };
@@ -0,0 +1,19 @@
1
+ import http from "http";
2
+ import app from "./app";
3
+ import { logger } from "@/utils";
4
+ import { ENV } from "@/config";
5
+
6
+ const PORT = ENV.PORT || 4000;
7
+
8
+ const server = http.createServer(app);
9
+
10
+ server.listen(PORT, () => {
11
+ logger.info(`🚀 API Gateway running on port ${PORT}`);
12
+ });
13
+
14
+ process.on("SIGTERM", () => {
15
+ logger.info("SIGTERM signal received: closing HTTP server");
16
+ server.close(() => {
17
+ logger.info("HTTP server closed");
18
+ });
19
+ });
@@ -0,0 +1,5 @@
1
+ FROM node:20
2
+ WORKDIR /app
3
+ COPY . .
4
+ RUN npm install
5
+ CMD ["npm","run","dev"]
@@ -0,0 +1,6 @@
1
+ version: "3"
2
+ services:
3
+ api:
4
+ build: .
5
+ ports:
6
+ - "4000:4000"
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ apps: [{ name: "api", script: "src/server.ts" }]
3
+ };