@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.
- package/README.md +288 -0
- package/bin/cli.js +249 -0
- package/bin/lib/microservice-config.js +89 -0
- package/bin/lib/prompts.js +159 -0
- package/bin/lib/readme-generator.js +245 -0
- package/bin/lib/service-setup.js +316 -0
- package/package.json +52 -0
- package/template/base/.env.example +5 -0
- package/template/base/.eslintrc.json +17 -0
- package/template/base/.husky/pre-commit +13 -0
- package/template/base/.prettierignore +47 -0
- package/template/base/.prettierrc +7 -0
- package/template/base/eslint.config.js +33 -0
- package/template/base/package.json +32 -0
- package/template/base/src/app.ts +9 -0
- package/template/base/src/config/db.ts +4 -0
- package/template/base/src/config/env.ts +9 -0
- package/template/base/src/config/index.ts +2 -0
- package/template/base/src/middlewares/index.ts +3 -0
- package/template/base/src/middlewares/method-not-allowed.middleware.ts +17 -0
- package/template/base/src/middlewares/not-found.middleware.ts +8 -0
- package/template/base/src/middlewares/root.middleware.ts +14 -0
- package/template/base/src/modules/index.ts +8 -0
- package/template/base/src/modules/v1/health/health.controller.ts +18 -0
- package/template/base/src/modules/v1/health/health.route.ts +9 -0
- package/template/base/src/modules/v1/health/index.ts +1 -0
- package/template/base/src/modules/v1/index.ts +8 -0
- package/template/base/src/routes.ts +15 -0
- package/template/base/src/server.ts +11 -0
- package/template/base/src/utils/http-error.ts +47 -0
- package/template/base/src/utils/index.ts +11 -0
- package/template/base/src/utils/logger.ts +34 -0
- package/template/base/tsconfig.json +22 -0
- package/template/features/auth/argon2/inject.js +25 -0
- package/template/features/auth/base/inject.js +89 -0
- package/template/features/auth/bcrypt/inject.js +18 -0
- package/template/features/auth/models/index.ts +1 -0
- package/template/features/auth/models/user.model.ts +28 -0
- package/template/features/auth/modules/auth.controller.ts +20 -0
- package/template/features/auth/modules/auth.routes.ts +11 -0
- package/template/features/auth/modules/auth.service.ts +38 -0
- package/template/features/auth/modules/index.ts +1 -0
- package/template/features/auth/utils/hash.ts +20 -0
- package/template/features/auth/utils/jwt.ts +15 -0
- package/template/features/cors/inject.js +9 -0
- package/template/features/helmet/inject.js +3 -0
- package/template/features/morgan/inject.js +3 -0
- package/template/features/rate-limit/inject.js +3 -0
- package/template/gateway/app.ts +26 -0
- package/template/gateway/inject.js +27 -0
- package/template/gateway/server.ts +19 -0
- package/template/microservice/docker/Dockerfile +5 -0
- package/template/microservice/docker/docker-compose.yml +6 -0
- package/template/microservice/nodocker/pm2.config.js +3 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import prompts from "prompts";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
export const setupService = async (
|
|
8
|
+
res,
|
|
9
|
+
serviceName,
|
|
10
|
+
serviceRoot,
|
|
11
|
+
shouldIncludeAuth,
|
|
12
|
+
allServices = []
|
|
13
|
+
) => {
|
|
14
|
+
let imports = [];
|
|
15
|
+
let middlewares = [];
|
|
16
|
+
let deps = [];
|
|
17
|
+
let v1Imports = [];
|
|
18
|
+
let v1Routes = [];
|
|
19
|
+
|
|
20
|
+
// Special handling for gateway service
|
|
21
|
+
if (serviceName === "gateway") {
|
|
22
|
+
const gatewayModule = await import("../../template/gateway/inject.js");
|
|
23
|
+
deps.push(...gatewayModule.gatewayDeps);
|
|
24
|
+
|
|
25
|
+
// Copy gateway-specific files
|
|
26
|
+
const gatewayAppPath = path.join(serviceRoot, "src/app.ts");
|
|
27
|
+
const gatewayServerPath = path.join(serviceRoot, "src/server.ts");
|
|
28
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
|
|
30
|
+
const gatewayAppContent = fs.readFileSync(
|
|
31
|
+
path.join(__dirname, "../../template/gateway/app.ts"),
|
|
32
|
+
"utf8"
|
|
33
|
+
);
|
|
34
|
+
const gatewayServerContent = fs.readFileSync(
|
|
35
|
+
path.join(__dirname, "../../template/gateway/server.ts"),
|
|
36
|
+
"utf8"
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Generate routes for all services
|
|
40
|
+
const routes = gatewayModule.generateGatewayRoutes(allServices);
|
|
41
|
+
const finalAppContent = gatewayAppContent.replace("/*__ROUTES__*/", routes);
|
|
42
|
+
|
|
43
|
+
fs.writeFileSync(gatewayAppPath, finalAppContent);
|
|
44
|
+
fs.writeFileSync(gatewayServerPath, gatewayServerContent);
|
|
45
|
+
|
|
46
|
+
// Remove unnecessary files for gateway
|
|
47
|
+
const routesPath = path.join(serviceRoot, "src/routes.ts");
|
|
48
|
+
const modulesPath = path.join(serviceRoot, "src/modules");
|
|
49
|
+
const middlewaresPath = path.join(serviceRoot, "src/middlewares");
|
|
50
|
+
|
|
51
|
+
if (fs.existsSync(routesPath)) fs.rmSync(routesPath);
|
|
52
|
+
if (fs.existsSync(modulesPath)) fs.rmSync(modulesPath, { recursive: true });
|
|
53
|
+
if (fs.existsSync(middlewaresPath))
|
|
54
|
+
fs.rmSync(middlewaresPath, { recursive: true });
|
|
55
|
+
} else {
|
|
56
|
+
// Regular service setup (existing code)
|
|
57
|
+
// Add features (only for monolith or health-service)
|
|
58
|
+
if (res.projectType === "monolith" || serviceName === "health-service") {
|
|
59
|
+
for (const f of res.features) {
|
|
60
|
+
const feature = await import(`../../template/features/${f}/inject.js`);
|
|
61
|
+
imports.push(feature.imports);
|
|
62
|
+
middlewares.push(feature.middleware);
|
|
63
|
+
deps.push(...feature.deps);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Add authentication (only for monolith or auth-service)
|
|
68
|
+
if (shouldIncludeAuth && res.auth) {
|
|
69
|
+
const baseAuth = await import(
|
|
70
|
+
"../../template/features/auth/base/inject.js"
|
|
71
|
+
);
|
|
72
|
+
deps.push(...baseAuth.deps);
|
|
73
|
+
|
|
74
|
+
for (const file in baseAuth.files) {
|
|
75
|
+
const fullPath = path.join(serviceRoot, file);
|
|
76
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
77
|
+
fs.writeFileSync(fullPath, baseAuth.files[file]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const algo = await prompts({
|
|
81
|
+
type: "select",
|
|
82
|
+
name: "hasher",
|
|
83
|
+
message: `Password hashing method${
|
|
84
|
+
serviceName ? ` for ${serviceName}` : ""
|
|
85
|
+
}`,
|
|
86
|
+
choices: [
|
|
87
|
+
{
|
|
88
|
+
title: process.platform === "win32"
|
|
89
|
+
? "bcrypt (recommended for Windows)"
|
|
90
|
+
: "argon2 (recommended)",
|
|
91
|
+
value: process.platform === "win32" ? "bcrypt" : "argon2"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
title: process.platform === "win32"
|
|
95
|
+
? "argon2 (requires build tools)"
|
|
96
|
+
: "bcrypt",
|
|
97
|
+
value: process.platform === "win32" ? "argon2" : "bcrypt"
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const hashFeature = await import(
|
|
103
|
+
`../../template/features/auth/${algo.hasher}/inject.js`
|
|
104
|
+
);
|
|
105
|
+
deps.push(...hashFeature.deps);
|
|
106
|
+
|
|
107
|
+
for (const file in hashFeature.files) {
|
|
108
|
+
const fullPath = path.join(serviceRoot, file);
|
|
109
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
110
|
+
fs.writeFileSync(fullPath, hashFeature.files[file]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
v1Imports.push(baseAuth.imports);
|
|
114
|
+
v1Routes.push(baseAuth.middleware);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Update app.ts
|
|
118
|
+
const appPath = path.join(serviceRoot, "src/app.ts");
|
|
119
|
+
let content = fs.readFileSync(appPath, "utf8");
|
|
120
|
+
content = content.replace("/*__IMPORTS__*/", imports.join("\n"));
|
|
121
|
+
// import env if cors is selected
|
|
122
|
+
if (res.features && res.features.includes("cors")) {
|
|
123
|
+
envContent = envContent.replace(
|
|
124
|
+
"/*__ENV_CORS__*/",
|
|
125
|
+
"import { ENV } from '@/config';"
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
content = content.replace("/*__MIDDLEWARE__*/", middlewares.join("\n"));
|
|
129
|
+
fs.writeFileSync(appPath, content);
|
|
130
|
+
|
|
131
|
+
// Update root endpoint middleware with project info
|
|
132
|
+
const rootMiddlewarePath = path.join(serviceRoot, "src/middlewares/root.middleware.ts");
|
|
133
|
+
if (fs.existsSync(rootMiddlewarePath)) {
|
|
134
|
+
let rootContent = fs.readFileSync(rootMiddlewarePath, "utf8");
|
|
135
|
+
rootContent = rootContent.replace("/*__PROJECT_NAME__*/", serviceName || res.sanitizedName);
|
|
136
|
+
rootContent = rootContent.replace("/*__PROJECT_TYPE__*/", res.projectType);
|
|
137
|
+
|
|
138
|
+
// Add auth endpoint if auth is enabled
|
|
139
|
+
if (shouldIncludeAuth && res.auth) {
|
|
140
|
+
rootContent = rootContent.replace(
|
|
141
|
+
"/*__AUTH_ENDPOINT__*/",
|
|
142
|
+
'auth: "/v1/auth",'
|
|
143
|
+
);
|
|
144
|
+
} else {
|
|
145
|
+
rootContent = rootContent.replace("/*__AUTH_ENDPOINT__*/", "");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fs.writeFileSync(rootMiddlewarePath, rootContent);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Update v1 index.ts if needed
|
|
152
|
+
if (v1Imports.length || v1Routes.length) {
|
|
153
|
+
const v1IndexPath = path.join(serviceRoot, "src/modules/v1/index.ts");
|
|
154
|
+
let v1Content = fs.readFileSync(v1IndexPath, "utf8");
|
|
155
|
+
|
|
156
|
+
const lastImportIndex = v1Content.lastIndexOf("import");
|
|
157
|
+
const importEndIndex = v1Content.indexOf("\n", lastImportIndex) + 1;
|
|
158
|
+
v1Content =
|
|
159
|
+
v1Content.slice(0, importEndIndex) +
|
|
160
|
+
v1Imports.join("\n") +
|
|
161
|
+
"\n" +
|
|
162
|
+
v1Content.slice(importEndIndex);
|
|
163
|
+
|
|
164
|
+
const exportIndex = v1Content.lastIndexOf("export default");
|
|
165
|
+
v1Content =
|
|
166
|
+
v1Content.slice(0, exportIndex) +
|
|
167
|
+
v1Routes.join("\n") +
|
|
168
|
+
"\n\n" +
|
|
169
|
+
v1Content.slice(exportIndex);
|
|
170
|
+
|
|
171
|
+
fs.writeFileSync(v1IndexPath, v1Content);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Update env.ts to conditionally include ALLOWED_ORIGIN and MONGO_URI
|
|
175
|
+
const envPath = path.join(serviceRoot, "src/config/env.ts");
|
|
176
|
+
if (fs.existsSync(envPath)) {
|
|
177
|
+
let envContent = fs.readFileSync(envPath, "utf8");
|
|
178
|
+
|
|
179
|
+
// Add ALLOWED_ORIGIN if CORS is selected
|
|
180
|
+
if (res.features && res.features.includes("cors")) {
|
|
181
|
+
envContent = envContent.replace(
|
|
182
|
+
"/*__ALLOWED_ORIGIN__*/",
|
|
183
|
+
'ALLOWED_ORIGIN: process.env.ALLOWED_ORIGIN!,'
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
envContent = envContent.replace("/*__ALLOWED_ORIGIN__*/", "");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Add MONGO_URI if auth is enabled
|
|
190
|
+
if (shouldIncludeAuth && res.auth) {
|
|
191
|
+
envContent = envContent.replace(
|
|
192
|
+
"/*__MONGO_URI__*/",
|
|
193
|
+
'MONGO_URI: process.env.MONGO_URI!,'
|
|
194
|
+
);
|
|
195
|
+
} else {
|
|
196
|
+
envContent = envContent.replace("/*__MONGO_URI__*/", "");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fs.writeFileSync(envPath, envContent);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Update server.ts to connect to DB if auth is enabled
|
|
203
|
+
const serverPath = path.join(serviceRoot, "src/server.ts");
|
|
204
|
+
if (fs.existsSync(serverPath)) {
|
|
205
|
+
let serverContent = fs.readFileSync(serverPath, "utf8");
|
|
206
|
+
|
|
207
|
+
if (shouldIncludeAuth && res.auth) {
|
|
208
|
+
serverContent = serverContent.replace(
|
|
209
|
+
"/*__DB_IMPORT__*/",
|
|
210
|
+
'import { connectDB } from "./config";'
|
|
211
|
+
);
|
|
212
|
+
serverContent = serverContent.replace(
|
|
213
|
+
"/*__DB_CONNECT__*/",
|
|
214
|
+
`// Connect to MongoDB
|
|
215
|
+
await connectDB();`
|
|
216
|
+
);
|
|
217
|
+
} else {
|
|
218
|
+
serverContent = serverContent.replace("/*__DB_IMPORT__*/", "");
|
|
219
|
+
serverContent = serverContent.replace("/*__DB_CONNECT__*/", "");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fs.writeFileSync(serverPath, serverContent);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Update .env.example to conditionally include environment variables
|
|
226
|
+
const envExamplePath = path.join(serviceRoot, ".env.example");
|
|
227
|
+
if (fs.existsSync(envExamplePath)) {
|
|
228
|
+
let envExampleContent = fs.readFileSync(envExamplePath, "utf8");
|
|
229
|
+
|
|
230
|
+
// Add ALLOWED_ORIGIN if CORS is selected
|
|
231
|
+
if (res.features && res.features.includes("cors")) {
|
|
232
|
+
envExampleContent = envExampleContent.replace(
|
|
233
|
+
"/*__ALLOWED_ORIGIN_ENV__*/",
|
|
234
|
+
'ALLOWED_ORIGIN=http://localhost:3000'
|
|
235
|
+
);
|
|
236
|
+
} else {
|
|
237
|
+
envExampleContent = envExampleContent.replace("/*__ALLOWED_ORIGIN_ENV__*/", "");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Add MONGO_URI and JWT_SECRET if auth is enabled
|
|
241
|
+
if (shouldIncludeAuth && res.auth) {
|
|
242
|
+
envExampleContent = envExampleContent.replace(
|
|
243
|
+
"/*__MONGO_URI_ENV__*/",
|
|
244
|
+
'MONGO_URI=mongodb://localhost:27017/your-database-name'
|
|
245
|
+
);
|
|
246
|
+
envExampleContent = envExampleContent.replace(
|
|
247
|
+
"/*__JWT_SECRET_ENV__*/",
|
|
248
|
+
'JWT_SECRET=your-super-secret-jwt-key-change-this-in-production'
|
|
249
|
+
);
|
|
250
|
+
} else {
|
|
251
|
+
envExampleContent = envExampleContent.replace("/*__MONGO_URI_ENV__*/", "");
|
|
252
|
+
envExampleContent = envExampleContent.replace("/*__JWT_SECRET_ENV__*/", "");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fs.writeFileSync(envExamplePath, envExampleContent);
|
|
256
|
+
}
|
|
257
|
+
} // End of else block for non-gateway services
|
|
258
|
+
|
|
259
|
+
// Update tsconfig.json for microservices to support @/ alias with shared folder
|
|
260
|
+
if (res.projectType === "microservice") {
|
|
261
|
+
const tsconfigPath = path.join(serviceRoot, "tsconfig.json");
|
|
262
|
+
let tsconfigContent = fs.readFileSync(tsconfigPath, "utf8");
|
|
263
|
+
|
|
264
|
+
// Remove comments from JSON (strip-json-comments approach)
|
|
265
|
+
tsconfigContent = tsconfigContent
|
|
266
|
+
.replace(/\/\/.*$/gm, '') // Remove single-line comments
|
|
267
|
+
.replace(/\/\*[\s\S]*?\*\//g, ''); // Remove multi-line comments
|
|
268
|
+
|
|
269
|
+
const tsconfig = JSON.parse(tsconfigContent);
|
|
270
|
+
|
|
271
|
+
// Update paths to include shared folder
|
|
272
|
+
tsconfig.compilerOptions.paths = {
|
|
273
|
+
"@/*": ["*"],
|
|
274
|
+
"@/config/*": ["../../shared/config/*"],
|
|
275
|
+
"@/utils/*": ["../../shared/utils/*"],
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Update package.json
|
|
282
|
+
const packageJsonPath = path.join(serviceRoot, "package.json");
|
|
283
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
284
|
+
packageJson.name = serviceName || res.sanitizedName;
|
|
285
|
+
fs.writeFileSync(
|
|
286
|
+
packageJsonPath,
|
|
287
|
+
JSON.stringify(packageJson, null, 2) + "\n"
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
// Install dependencies
|
|
291
|
+
console.log(
|
|
292
|
+
`\n📦 Installing dependencies for ${serviceName || "project"}...\n`
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
if (deps.length) {
|
|
297
|
+
execSync(`npm install ${deps.join(" ")}`, {
|
|
298
|
+
cwd: serviceRoot,
|
|
299
|
+
stdio: "inherit",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
execSync("npm install", { cwd: serviceRoot, stdio: "inherit" });
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error("\n⚠️ Warning: Some dependencies failed to install.");
|
|
305
|
+
console.error("This is usually due to native modules (like argon2) requiring build tools.\n");
|
|
306
|
+
console.error("💡 Solutions:");
|
|
307
|
+
console.error(" 1. Install build tools: npm install --global windows-build-tools");
|
|
308
|
+
console.error(" 2. Or switch to bcrypt (works better on Windows)");
|
|
309
|
+
console.error(" 3. Or manually install later: cd " + (serviceName || res.sanitizedName) + " && npm install\n");
|
|
310
|
+
|
|
311
|
+
// Don't exit - let the project be created anyway
|
|
312
|
+
console.log("⏭️ Continuing with project creation...\n");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return deps;
|
|
316
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ifecodes/backend-template",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Production-ready Express + TypeScript backend generator with optional features and microservice support",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ifecodes-template": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/ALADETAN-IFE/backend-template.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/ALADETAN-IFE/backend-template/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/ALADETAN-IFE/backend-template#readme",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"cli",
|
|
29
|
+
"template",
|
|
30
|
+
"boilerplate",
|
|
31
|
+
"typescript",
|
|
32
|
+
"backend",
|
|
33
|
+
"express",
|
|
34
|
+
"microservice",
|
|
35
|
+
"monolith",
|
|
36
|
+
"nodejs",
|
|
37
|
+
"generator",
|
|
38
|
+
"scaffold",
|
|
39
|
+
"docker",
|
|
40
|
+
"pm2",
|
|
41
|
+
"jwt",
|
|
42
|
+
"authentication"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"author": "Aladetan Fortune Ifeloju (IfeCodes) <ifecodes01@gmail.com>",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"prompts": "^2.4.2"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"parser": "@typescript-eslint/parser",
|
|
3
|
+
"plugins": ["@typescript-eslint"],
|
|
4
|
+
"extends": [
|
|
5
|
+
"eslint:recommended",
|
|
6
|
+
"plugin:@typescript-eslint/recommended",
|
|
7
|
+
"prettier"
|
|
8
|
+
],
|
|
9
|
+
"env": {
|
|
10
|
+
"node": true,
|
|
11
|
+
"es2021": true
|
|
12
|
+
},
|
|
13
|
+
"rules": {
|
|
14
|
+
"@typescript-eslint/no-unused-vars": ["warn"],
|
|
15
|
+
"@typescript-eslint/no-explicit-any": "error"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
/node_modules
|
|
5
|
+
/.pnp
|
|
6
|
+
.pnp.js
|
|
7
|
+
.yarn/install-state.gz
|
|
8
|
+
|
|
9
|
+
# testing
|
|
10
|
+
/coverage
|
|
11
|
+
|
|
12
|
+
# next.js
|
|
13
|
+
/.next/
|
|
14
|
+
/out/
|
|
15
|
+
.next/
|
|
16
|
+
|
|
17
|
+
# production
|
|
18
|
+
/build
|
|
19
|
+
|
|
20
|
+
# misc
|
|
21
|
+
.DS_Store
|
|
22
|
+
*.pem
|
|
23
|
+
|
|
24
|
+
# debug
|
|
25
|
+
npm-debug.log*
|
|
26
|
+
yarn-debug.log*
|
|
27
|
+
yarn-error.log*
|
|
28
|
+
|
|
29
|
+
# local env files
|
|
30
|
+
.env*.local
|
|
31
|
+
|
|
32
|
+
# vercel
|
|
33
|
+
.vercel
|
|
34
|
+
|
|
35
|
+
# typescript
|
|
36
|
+
*.tsbuildinfo
|
|
37
|
+
next-env.d.ts
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
./dist
|
|
41
|
+
|
|
42
|
+
/*.yml
|
|
43
|
+
/*.yaml
|
|
44
|
+
/*.md
|
|
45
|
+
/*.json
|
|
46
|
+
README.md
|
|
47
|
+
eslint.config.ts
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const tsParser = require("@typescript-eslint/parser");
|
|
2
|
+
const tsPlugin = require("@typescript-eslint/eslint-plugin");
|
|
3
|
+
|
|
4
|
+
module.exports = [
|
|
5
|
+
// Files/paths to ignore (replaces .eslintignore usage in flat config)
|
|
6
|
+
{
|
|
7
|
+
ignores: ["node_modules/**", "dist/**"],
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
// TypeScript rules for source files
|
|
11
|
+
{
|
|
12
|
+
files: ["src/**/*.{ts,tsx}"],
|
|
13
|
+
languageOptions: {
|
|
14
|
+
parser: tsParser,
|
|
15
|
+
parserOptions: {
|
|
16
|
+
project: "./tsconfig.json",
|
|
17
|
+
tsconfigRootDir: __dirname,
|
|
18
|
+
ecmaVersion: 2020,
|
|
19
|
+
sourceType: "module",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
plugins: {
|
|
23
|
+
"@typescript-eslint": tsPlugin,
|
|
24
|
+
},
|
|
25
|
+
rules: {
|
|
26
|
+
// Disallow explicit `any`
|
|
27
|
+
"@typescript-eslint/no-explicit-any": "error",
|
|
28
|
+
|
|
29
|
+
// You can add or tune more TypeScript rules here
|
|
30
|
+
"@typescript-eslint/explicit-module-boundary-types": "off",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scripts": {
|
|
3
|
+
"dev": "ts-node-dev --respawn --transpile-only -r tsconfig-paths/register src/server.ts",
|
|
4
|
+
"start": "node dist/server.js",
|
|
5
|
+
"build": "tsc",
|
|
6
|
+
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
7
|
+
"format": "prettier --write \"src/**/*.{ts,json}\"",
|
|
8
|
+
"check-format": "prettier --check \"src/**/*.{ts,json}\"",
|
|
9
|
+
"prepare": "husky"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"dotenv": "^16.3.1",
|
|
13
|
+
"express": "^5.2.1",
|
|
14
|
+
"module-alias": "^2.2.3"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/express": "^5.0.6",
|
|
18
|
+
"@types/node": "^25.0.3",
|
|
19
|
+
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
|
20
|
+
"@typescript-eslint/parser": "^8.50.1",
|
|
21
|
+
"eslint": "^9.39.2",
|
|
22
|
+
"eslint-config-prettier": "^10.1.8",
|
|
23
|
+
"husky": "^9.1.7",
|
|
24
|
+
"prettier": "^3.7.4",
|
|
25
|
+
"ts-node-dev": "^2.0.0",
|
|
26
|
+
"tsconfig-paths": "^4.2.0",
|
|
27
|
+
"typescript": "^5.9.3"
|
|
28
|
+
},
|
|
29
|
+
"_moduleAliases": {
|
|
30
|
+
"@": "dist"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// middlewares/methodNotAllowed.ts
|
|
2
|
+
import { Request, Response, NextFunction } from "express";
|
|
3
|
+
|
|
4
|
+
const methodNotAllowed =
|
|
5
|
+
(allowedMethods: string[]) => (req: Request, res: Response, next: NextFunction) => {
|
|
6
|
+
if (!allowedMethods.includes(req.method)) {
|
|
7
|
+
res.set("Allow", allowedMethods.join(", "));
|
|
8
|
+
return res.status(405).json({
|
|
9
|
+
status: "error",
|
|
10
|
+
message: `Method ${req.method} not allowed for ${req.originalUrl}`,
|
|
11
|
+
allowed: allowedMethods,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
next();
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default methodNotAllowed;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
|
|
3
|
+
export const rootHandler = (req: Request, res: Response) => {
|
|
4
|
+
res.json({
|
|
5
|
+
name: "/*__PROJECT_NAME__*/",
|
|
6
|
+
type: "/*__PROJECT_TYPE__*/",
|
|
7
|
+
version: "1.0.0",
|
|
8
|
+
status: "running",
|
|
9
|
+
endpoints: {
|
|
10
|
+
health: "/v1/health",
|
|
11
|
+
/*__AUTH_ENDPOINT__*/
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
import { logger } from "@/utils";
|
|
3
|
+
|
|
4
|
+
export const healthCheck = async (_: Request, res: Response) => {
|
|
5
|
+
logger.info("Health", "healthy");
|
|
6
|
+
|
|
7
|
+
return res.status(200).json({
|
|
8
|
+
status: "healthy",
|
|
9
|
+
uptime: process.uptime(),
|
|
10
|
+
timestamp: new Date().toISOString(),
|
|
11
|
+
services: {
|
|
12
|
+
memory: {
|
|
13
|
+
rss: process.memoryUsage().rss,
|
|
14
|
+
heapUsed: process.memoryUsage().heapUsed,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { healthCheck } from "./health.controller";
|
|
3
|
+
import { methodNotAllowedHandler } from "@/middlewares";
|
|
4
|
+
|
|
5
|
+
const router = Router();
|
|
6
|
+
router.use(methodNotAllowedHandler(["GET"]));
|
|
7
|
+
router.get("/", healthCheck);
|
|
8
|
+
|
|
9
|
+
export default router;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as healthRoutes } from "./health.route";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import modulesRouter from "./modules";
|
|
3
|
+
import { notFound, rootHandler } from "./middlewares";
|
|
4
|
+
|
|
5
|
+
const router = Router();
|
|
6
|
+
|
|
7
|
+
// Root endpoint
|
|
8
|
+
router.get("/", rootHandler);
|
|
9
|
+
|
|
10
|
+
router.use("/", modulesRouter);
|
|
11
|
+
|
|
12
|
+
// 404 handler - must be last
|
|
13
|
+
router.use(notFound);
|
|
14
|
+
|
|
15
|
+
export default router;
|