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.
Files changed (37) hide show
  1. package/README.md +0 -0
  2. package/bin/cli.js +105 -0
  3. package/package.json +44 -0
  4. package/template/.biomeignore +40 -0
  5. package/template/.github/PULL_REQUEST_TEMPLATE.md +18 -0
  6. package/template/.github/workflows/check.yml +107 -0
  7. package/template/.husky/pre-commit +4 -0
  8. package/template/README.md +66 -0
  9. package/template/biome.json +65 -0
  10. package/template/bun.lock +643 -0
  11. package/template/package.json +67 -0
  12. package/template/prisma/schema.prisma +8 -0
  13. package/template/prisma.config.js +12 -0
  14. package/template/src/app.js +45 -0
  15. package/template/src/config/cookie.config.js +21 -0
  16. package/template/src/config/env.config.js +43 -0
  17. package/template/src/config/swagger.config.js +62 -0
  18. package/template/src/constants/api.constants.js +6 -0
  19. package/template/src/constants/app.constants.js +5 -0
  20. package/template/src/constants/http.constants.js +90 -0
  21. package/template/src/constants/queue.constants.js +5 -0
  22. package/template/src/constants/security.constants.js +22 -0
  23. package/template/src/constants/validation.constants.js +8 -0
  24. package/template/src/core/db/prisma.connection.js +0 -0
  25. package/template/src/core/db/prisma.js +0 -0
  26. package/template/src/core/http/api.error.js +162 -0
  27. package/template/src/core/http/api.response.js +118 -0
  28. package/template/src/core/middlewares/asyncHandler.middleware.js +7 -0
  29. package/template/src/core/middlewares/error.middleware.js +43 -0
  30. package/template/src/core/middlewares/notFound.middleware.js +8 -0
  31. package/template/src/core/middlewares/validate.middleware.js +33 -0
  32. package/template/src/core/utils/logger.utils.js +78 -0
  33. package/template/src/core/utils/zod.utils.js +74 -0
  34. package/template/src/index.js +29 -0
  35. package/template/src/routes/createRoute.js +10 -0
  36. package/template/src/routes/health.route.js +28 -0
  37. package/template/src/routes/index.route.js +38 -0
@@ -0,0 +1,7 @@
1
+ const asyncHandler = (fn) => {
2
+ return (req, res, next) => {
3
+ Promise.resolve(fn(req, res, next)).catch((err) => next(err));
4
+ };
5
+ };
6
+
7
+ export default asyncHandler;
@@ -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,8 @@
1
+ import { API_VERSION } from "../../constants/api.constants.js";
2
+ import ApiResponse from "../http/api.response.js";
3
+
4
+ const notFound = (_, res) => {
5
+ return ApiResponse.redirect(res, `/api/${API_VERSION}/docs`);
6
+ };
7
+
8
+ export default notFound;
@@ -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,10 @@
1
+ import { Router } from "express";
2
+
3
+ const createRouter = () =>
4
+ Router({
5
+ caseSensitive: true,
6
+ mergeParams: true,
7
+ strict: true,
8
+ });
9
+
10
+ export default createRouter;
@@ -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;