backend-starter-kit 1.0.5
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 +0 -0
- package/bin/cli.js +105 -0
- package/package.json +44 -0
- package/template/.biomeignore +40 -0
- package/template/.github/PULL_REQUEST_TEMPLATE.md +18 -0
- package/template/.github/workflows/check.yml +107 -0
- package/template/.husky/pre-commit +4 -0
- package/template/README.md +66 -0
- package/template/biome.json +65 -0
- package/template/bun.lock +643 -0
- package/template/package.json +67 -0
- package/template/prisma/schema.prisma +8 -0
- package/template/prisma.config.js +12 -0
- package/template/src/app.js +45 -0
- package/template/src/config/cookie.config.js +21 -0
- package/template/src/config/env.config.js +43 -0
- package/template/src/config/swagger.config.js +62 -0
- package/template/src/constants/api.constants.js +6 -0
- package/template/src/constants/app.constants.js +5 -0
- package/template/src/constants/http.constants.js +90 -0
- package/template/src/constants/queue.constants.js +5 -0
- package/template/src/constants/security.constants.js +22 -0
- package/template/src/constants/validation.constants.js +8 -0
- package/template/src/core/db/prisma.connection.js +0 -0
- package/template/src/core/db/prisma.js +0 -0
- package/template/src/core/http/api.error.js +162 -0
- package/template/src/core/http/api.response.js +118 -0
- package/template/src/core/middlewares/asyncHandler.middleware.js +7 -0
- package/template/src/core/middlewares/error.middleware.js +43 -0
- package/template/src/core/middlewares/notFound.middleware.js +8 -0
- package/template/src/core/middlewares/validate.middleware.js +33 -0
- package/template/src/core/utils/logger.utils.js +78 -0
- package/template/src/core/utils/zod.utils.js +74 -0
- package/template/src/index.js +29 -0
- package/template/src/routes/createRoute.js +10 -0
- package/template/src/routes/health.route.js +28 -0
- package/template/src/routes/index.route.js +38 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { NODE_ENV } from "../../config/env.config.js";
|
|
2
|
+
import ApiError from "../http/api.error.js";
|
|
3
|
+
import { logger } from "../utils/logger.utils.js";
|
|
4
|
+
|
|
5
|
+
const errorHandler = (err, req, res, _next) => {
|
|
6
|
+
const error = ApiError.from(err);
|
|
7
|
+
const statusCode = error.statusCode || 500;
|
|
8
|
+
|
|
9
|
+
const isProduction = NODE_ENV === "production";
|
|
10
|
+
|
|
11
|
+
if (error.isOperational) {
|
|
12
|
+
logger.warn({
|
|
13
|
+
message: error.message,
|
|
14
|
+
statusCode,
|
|
15
|
+
method: req.method,
|
|
16
|
+
url: req.originalUrl,
|
|
17
|
+
ip: req.ip,
|
|
18
|
+
});
|
|
19
|
+
} else {
|
|
20
|
+
logger.error({
|
|
21
|
+
message: error.message,
|
|
22
|
+
stack: error.stack,
|
|
23
|
+
method: req.method,
|
|
24
|
+
url: req.originalUrl,
|
|
25
|
+
ip: req.ip,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const body =
|
|
30
|
+
isProduction && !error.isOperational
|
|
31
|
+
? {
|
|
32
|
+
success: false,
|
|
33
|
+
message: "Something went wrong",
|
|
34
|
+
}
|
|
35
|
+
: {
|
|
36
|
+
...error.toJSON(),
|
|
37
|
+
...(NODE_ENV === "development" && { stack: error.stack }),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
res.status(statusCode).json(body);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default errorHandler;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ZodError, z } from "zod/v4";
|
|
2
|
+
import ApiError from "../http/api.error.js";
|
|
3
|
+
|
|
4
|
+
const validate = (schema, source = "body") => {
|
|
5
|
+
return async (req, _res, next) => {
|
|
6
|
+
try {
|
|
7
|
+
const result = await schema.safeParseAsync(req[source]);
|
|
8
|
+
|
|
9
|
+
if (!result.success) {
|
|
10
|
+
const flattenError = z.flattenError(result.error);
|
|
11
|
+
|
|
12
|
+
return next(
|
|
13
|
+
ApiError.validationError("Validation Error", flattenError)
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
req[source] = result.data;
|
|
18
|
+
return next();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (error instanceof ZodError) {
|
|
21
|
+
const flattenError = z.flattenError(error);
|
|
22
|
+
|
|
23
|
+
return next(
|
|
24
|
+
ApiError.validationError("Validation Error", flattenError)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return next(ApiError.from(error));
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default validate;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import winston from "winston";
|
|
4
|
+
import { NODE_ENV } from "../../config/env.config.js";
|
|
5
|
+
|
|
6
|
+
const { combine, timestamp, errors, colorize, printf, prettyPrint } =
|
|
7
|
+
winston.format;
|
|
8
|
+
|
|
9
|
+
const logDir = "logs";
|
|
10
|
+
if (!fs.existsSync(logDir)) {
|
|
11
|
+
fs.mkdirSync(logDir);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const logFormat = printf(({ timestamp, level, message, stack }) => {
|
|
15
|
+
return `\n[${timestamp}] \n ${level} :- ${stack || message}\n`;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const logger = winston.createLogger({
|
|
19
|
+
level: NODE_ENV === "development" ? "debug" : "info",
|
|
20
|
+
handleExceptions: true,
|
|
21
|
+
handleRejections: true,
|
|
22
|
+
defaultMeta: { service: "backend" },
|
|
23
|
+
silent: NODE_ENV === "test",
|
|
24
|
+
format: combine(
|
|
25
|
+
timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
26
|
+
errors({ stack: true }),
|
|
27
|
+
logFormat,
|
|
28
|
+
prettyPrint()
|
|
29
|
+
),
|
|
30
|
+
transports: [
|
|
31
|
+
new winston.transports.Console({
|
|
32
|
+
handleExceptions: true,
|
|
33
|
+
handleRejections: true,
|
|
34
|
+
format: combine(
|
|
35
|
+
colorize({
|
|
36
|
+
all: true,
|
|
37
|
+
message: true,
|
|
38
|
+
colors: {
|
|
39
|
+
info: "blue",
|
|
40
|
+
warn: "yellow",
|
|
41
|
+
error: "red",
|
|
42
|
+
debug: "magenta",
|
|
43
|
+
verbose: "cyan",
|
|
44
|
+
silly: "green",
|
|
45
|
+
http: "magenta",
|
|
46
|
+
critical: "red",
|
|
47
|
+
alert: "red",
|
|
48
|
+
emergency: "red",
|
|
49
|
+
notice: "blue",
|
|
50
|
+
warning: "yellow",
|
|
51
|
+
},
|
|
52
|
+
level: true,
|
|
53
|
+
}),
|
|
54
|
+
logFormat
|
|
55
|
+
),
|
|
56
|
+
}),
|
|
57
|
+
new winston.transports.File({
|
|
58
|
+
filename: path.join(logDir, "error.log"),
|
|
59
|
+
level: "error",
|
|
60
|
+
handleExceptions: true,
|
|
61
|
+
maxsize: 5 * 1024 * 1024,
|
|
62
|
+
maxFiles: 5,
|
|
63
|
+
}),
|
|
64
|
+
new winston.transports.File({
|
|
65
|
+
filename: path.join(logDir, "combined.log"),
|
|
66
|
+
handleExceptions: true,
|
|
67
|
+
maxsize: 5 * 1024 * 1024,
|
|
68
|
+
maxFiles: 5,
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
exitOnError: false,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
logger.stream = {
|
|
75
|
+
write: (message) => {
|
|
76
|
+
logger.info(message.trim());
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import z from "zod/v4";
|
|
2
|
+
|
|
3
|
+
const zString = (fieldName, minLength = 2, maxLength = 255) => {
|
|
4
|
+
return z
|
|
5
|
+
.string({ error: "Invalid string" })
|
|
6
|
+
.min(minLength, {
|
|
7
|
+
error: `${fieldName} must be at least ${minLength} characters long`,
|
|
8
|
+
})
|
|
9
|
+
.max(maxLength, {
|
|
10
|
+
error: `${fieldName} must be at most ${maxLength} characters long`,
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const zEmail = (minLength = 2) => {
|
|
15
|
+
return z
|
|
16
|
+
.email({ pattern: z.regexes.email, error: "Invalid email address" })
|
|
17
|
+
.min(minLength, {
|
|
18
|
+
error: `Email must be at least ${minLength} characters long`,
|
|
19
|
+
})
|
|
20
|
+
.max(255, { error: "Email must be at most 255 characters long" });
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const zNumber = (fieldName, minLength = 1, maxLength = 15) => {
|
|
24
|
+
return z
|
|
25
|
+
.number({ error: `Invalid ${fieldName}` })
|
|
26
|
+
.min(minLength, `${fieldName} must be at least ${minLength}`)
|
|
27
|
+
.max(maxLength, `${fieldName} must be at most ${maxLength}`);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const zUUID = (fieldName) => {
|
|
31
|
+
return z
|
|
32
|
+
.uuid({ error: `Invalid ${fieldName} format` })
|
|
33
|
+
.min(36, `${fieldName} must be at least 36 characters long`)
|
|
34
|
+
.max(36, `${fieldName} must be at most 36 characters long`);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const zPassword = () => {
|
|
38
|
+
return z
|
|
39
|
+
.string({ error: "Invalid password" })
|
|
40
|
+
.min(6, { error: "Password must be at least 6 characters long" })
|
|
41
|
+
.max(128, { error: "Password must be at most 128 characters long" })
|
|
42
|
+
.regex(/[a-z]/, {
|
|
43
|
+
error: "Password must contain at least one lowercase letter",
|
|
44
|
+
})
|
|
45
|
+
.regex(/[A-Z]/, {
|
|
46
|
+
error: "Password must contain at least one uppercase letter",
|
|
47
|
+
})
|
|
48
|
+
.regex(/[0-9]/, {
|
|
49
|
+
error: "Password must contain at least one number",
|
|
50
|
+
})
|
|
51
|
+
.regex(/[^a-zA-Z0-9]/, {
|
|
52
|
+
error: "Password must contain at least one special character",
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const zArray = (
|
|
57
|
+
fieldName,
|
|
58
|
+
itemSchema = z.any(),
|
|
59
|
+
minLength = 1,
|
|
60
|
+
maxLength = 100,
|
|
61
|
+
defaultValue = []
|
|
62
|
+
) => {
|
|
63
|
+
return z
|
|
64
|
+
.array(itemSchema, { error: `Invalid ${fieldName}` })
|
|
65
|
+
.min(minLength, {
|
|
66
|
+
error: `${fieldName} must contain at least ${minLength} items`,
|
|
67
|
+
})
|
|
68
|
+
.max(maxLength, {
|
|
69
|
+
error: `${fieldName} must contain at most ${maxLength} items`,
|
|
70
|
+
})
|
|
71
|
+
.default(defaultValue);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export { zString, zEmail, zNumber, zUUID, zPassword, zArray };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { app } from "./app";
|
|
2
|
+
import { PORT } from "./config/env.config.js";
|
|
3
|
+
import { logger } from "./core/utils/logger.utils.js";
|
|
4
|
+
|
|
5
|
+
app.listen(PORT, async () => {
|
|
6
|
+
try {
|
|
7
|
+
app.listen(PORT, () => {
|
|
8
|
+
logger.info(`🚀 Server running on PORT: ${PORT}`);
|
|
9
|
+
});
|
|
10
|
+
} catch (error) {
|
|
11
|
+
logger.error("❌ Database connection failed:", error);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const shutdown = async (signal) => {
|
|
17
|
+
logger.info(`🛑 ${signal} received. Shutting down...`);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
logger.info("✅ Database connection closed. Server shutdown complete.");
|
|
21
|
+
} catch (err) {
|
|
22
|
+
logger.error("❌ Error during DB disconnect", err);
|
|
23
|
+
} finally {
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
29
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import ApiResponse from "../core/http/api.response.js";
|
|
2
|
+
import createRouter from "./createRoute.js";
|
|
3
|
+
|
|
4
|
+
const router = createRouter();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @swagger
|
|
8
|
+
* /health:
|
|
9
|
+
* get:
|
|
10
|
+
* summary: Health Check
|
|
11
|
+
* description: Check if the API is healthy and responsive
|
|
12
|
+
* tags:
|
|
13
|
+
* - System
|
|
14
|
+
* responses:
|
|
15
|
+
* 200:
|
|
16
|
+
* description: API is healthy
|
|
17
|
+
* content:
|
|
18
|
+
* application/json:
|
|
19
|
+
* example:
|
|
20
|
+
* status: success
|
|
21
|
+
* message: API is healthy
|
|
22
|
+
* data: null
|
|
23
|
+
*/
|
|
24
|
+
router.get("/", (_, res) => {
|
|
25
|
+
return ApiResponse.ok(null, "API is healthy").send(res);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export default router;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { specs, swaggerUi, theme } from "../config/swagger.config.js";
|
|
2
|
+
import { APP_NAME } from "../constants/app.constants.js";
|
|
3
|
+
import ApiResponse from "../core/http/api.response.js";
|
|
4
|
+
import createRouter from "./createRoute.js";
|
|
5
|
+
import healthRoute from "./health.route.js";
|
|
6
|
+
|
|
7
|
+
const router = createRouter();
|
|
8
|
+
|
|
9
|
+
// Swagger UI for API documentation
|
|
10
|
+
router.use("/docs", swaggerUi.serve, swaggerUi.setup(specs, theme));
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @swagger
|
|
14
|
+
* /:
|
|
15
|
+
* get:
|
|
16
|
+
* summary: Welcome message
|
|
17
|
+
* description: Welcome message for the API service
|
|
18
|
+
* tags:
|
|
19
|
+
* - System
|
|
20
|
+
* responses:
|
|
21
|
+
* 200:
|
|
22
|
+
* description: Welcome message
|
|
23
|
+
* content:
|
|
24
|
+
* application/json:
|
|
25
|
+
* example:
|
|
26
|
+
* status: success
|
|
27
|
+
* message: Welcome to the ${APP_NAME} API service
|
|
28
|
+
* data: null
|
|
29
|
+
*/
|
|
30
|
+
router.get("/", (_, res) => {
|
|
31
|
+
return ApiResponse.ok(null, `Welcome to the ${APP_NAME} API Service`).send(
|
|
32
|
+
res
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
router.use("/health", healthRoute);
|
|
37
|
+
|
|
38
|
+
export default router;
|