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,67 @@
1
+ {
2
+ "name": "backend",
3
+ "version": "0.1.0",
4
+ "description": "Production-ready backend using Bun",
5
+ "type": "module",
6
+ "private": true,
7
+ "license": "MIT",
8
+ "author": {
9
+ "name": "Harshit Ostwal",
10
+ "email": "harshitostwal1234@gmail.com",
11
+ "url": "https://harshitostwal.com"
12
+ },
13
+ "contributors": [
14
+ {
15
+ "name": "Harshit Ostwal",
16
+ "email": "harshitostwal1234@gmail.com",
17
+ "url": "https://harshitostwal.com"
18
+ }
19
+ ],
20
+ "main": "src/index.js",
21
+ "module": "src/index.js",
22
+ "scripts": {
23
+ "dev": "bun --hot src/index.js",
24
+ "start": "bun src/index.js",
25
+ "biome:format": "bunx biome format --write",
26
+ "biome:lint": "bunx biome lint --write",
27
+ "biome:check": "bunx biome check --write",
28
+ "biome:clean": "bunx biome clean",
29
+ "prisma:generate": "prisma generate",
30
+ "prisma:migrate": "prisma migrate dev --name",
31
+ "prisma:migrate:deploy": "prisma migrate deploy",
32
+ "prisma:migrate:reset": "prisma migrate reset",
33
+ "prisma:migrate:status": "prisma migrate status",
34
+ "prisma:studio": "prisma studio",
35
+ "prisma:db:push": "prisma db push",
36
+ "prisma:db:pull": "prisma db pull",
37
+ "prepare": "husky"
38
+ },
39
+ "lint-staged": {
40
+ "*": [
41
+ "bunx biome check --write --no-errors-on-unmatched --files-ignore-unknown=true"
42
+ ]
43
+ },
44
+ "dependencies": {
45
+ "@prisma/adapter-pg": "^7.3.0",
46
+ "@prisma/client": "^7.3.0",
47
+ "cookie-parser": "^1.4.7",
48
+ "cors": "^2.8.6",
49
+ "dotenv": "^17.2.3",
50
+ "express": "^5.2.1",
51
+ "helmet": "^8.1.0",
52
+ "jsonwebtoken": "^9.0.3",
53
+ "morgan": "^1.10.1",
54
+ "pg": "^8.18.0",
55
+ "swagger-jsdoc": "^6.2.8",
56
+ "swagger-ui-express": "^5.0.1",
57
+ "uuid": "^13.0.0",
58
+ "winston": "^3.19.0",
59
+ "zod": "^4.3.6"
60
+ },
61
+ "devDependencies": {
62
+ "@biomejs/biome": "2.3.13",
63
+ "husky": "^9.1.7",
64
+ "lint-staged": "^16.2.7",
65
+ "prisma": "^7.3.0"
66
+ }
67
+ }
@@ -0,0 +1,8 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ output = "../src/generated/prisma"
4
+ }
5
+
6
+ datasource db {
7
+ provider = "postgresql"
8
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "prisma/config";
2
+ import { NODE_ENV } from "./src/config/env.config.js";
3
+
4
+ export default defineConfig({
5
+ schema: "prisma/schema.prisma",
6
+ migrations: {
7
+ path: "prisma/migrations",
8
+ },
9
+ datasource: {
10
+ url: NODE_ENV,
11
+ },
12
+ });
@@ -0,0 +1,45 @@
1
+ import cookieParser from "cookie-parser";
2
+ import cors from "cors";
3
+ import express from "express";
4
+ import helmet from "helmet";
5
+ import morgan from "morgan";
6
+ import { NODE_ENV } from "./config/env.config.js";
7
+ import { API_VERSION, REQUEST_SIZE_LIMIT } from "./constants/api.constants.js";
8
+ import { APP_NAME } from "./constants/app.constants.js";
9
+ import { CORS_OPTIONS } from "./constants/security.constants.js";
10
+ import errorHandler from "./core/middlewares/error.middleware.js";
11
+ import notFound from "./core/middlewares/notFound.middleware.js";
12
+ import { logger } from "./core/utils/logger.utils.js";
13
+ import routes from "./routes/index.route.js";
14
+
15
+ const app = express();
16
+
17
+ app.set("title", `${APP_NAME} API Server (${API_VERSION})`);
18
+ app.set("env", NODE_ENV || "development");
19
+
20
+ app.set("trust proxy");
21
+ app.set("json spaces", 2);
22
+
23
+ app.use(
24
+ morgan(":method :url :status :res[content-length] - :response-time ms", {
25
+ stream: logger.stream,
26
+ }),
27
+ );
28
+
29
+ app.use(express.json({ limit: REQUEST_SIZE_LIMIT }));
30
+ app.use(express.urlencoded({ extended: true, limit: REQUEST_SIZE_LIMIT }));
31
+ app.use(express.static("public"));
32
+ app.use(cookieParser());
33
+ app.use(cors(CORS_OPTIONS));
34
+
35
+ // Helmet for security headers
36
+ // -- Disable contentSecurityPolicy for development to avoid issues with Swagger UI
37
+ app.use(helmet({ contentSecurityPolicy: false }));
38
+
39
+ // API routes
40
+ app.use(`/api/${API_VERSION}`, routes);
41
+
42
+ app.use(notFound);
43
+ app.use(errorHandler);
44
+
45
+ export { app };
@@ -0,0 +1,21 @@
1
+ import { NODE_ENV } from "../config/env.config.js";
2
+
3
+ const sharedCookieBase = {
4
+ httpOnly: true,
5
+ secure: NODE_ENV === "production",
6
+ sameSite:
7
+ NODE_ENV === "production" ? "none" : NODE_ENV === "testing" ? "" : "",
8
+ path: "/",
9
+ };
10
+
11
+ const cookieAccessOptions = {
12
+ ...sharedCookieBase,
13
+ maxAge: 15 * 60 * 1000, // 15 minutes
14
+ };
15
+
16
+ const cookieRefreshOptions = {
17
+ ...sharedCookieBase,
18
+ maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
19
+ };
20
+
21
+ export { cookieAccessOptions, cookieRefreshOptions };
@@ -0,0 +1,43 @@
1
+ import { config } from "dotenv";
2
+
3
+ config({
4
+ path: `.env.${process.env.NODE_ENV || "development"}`,
5
+ debug: process.env.NODE_ENV === "development",
6
+ encoding: "utf8",
7
+ override: false,
8
+ quiet: false,
9
+ });
10
+
11
+ export const {
12
+ NODE_ENV,
13
+ PORT,
14
+
15
+ // CORS
16
+ ALLOWED_ORIGINS,
17
+
18
+ // URL's
19
+ BACKEND_URL,
20
+ FRONTEND_URL,
21
+
22
+ // Database
23
+ DATABASE_URL,
24
+
25
+ // Redis
26
+ REDIS_HOST,
27
+ REDIS_PORT,
28
+ REDIS_PASSWORD,
29
+
30
+ // JWT
31
+ ACCESS_TOKEN_SECRET,
32
+ ACCESS_TOKEN_EXPIRY,
33
+ REFRESH_TOKEN_SECRET,
34
+ REFRESH_TOKEN_EXPIRY,
35
+
36
+ // Mail
37
+ SMPT_HOST,
38
+ SMPT_PORT,
39
+ SMPT_USER,
40
+ SMPT_PASSWORD,
41
+ SMPT_SECURE,
42
+ SMPT_SERVICE,
43
+ } = process.env;
@@ -0,0 +1,62 @@
1
+ import swaggerJsdoc from "swagger-jsdoc";
2
+ import swaggerUi from "swagger-ui-express";
3
+ import { API_VERSION } from "../constants/api.constants.js";
4
+ import { APP_NAME } from "../constants/app.constants.js";
5
+ import { BACKEND_URL } from "./env.config.js";
6
+
7
+ const options = {
8
+ definition: {
9
+ openapi: "3.0.0",
10
+ info: {
11
+ contact: {
12
+ name: "API Support",
13
+ url: "https://www.example.com/support",
14
+ email: "support@example.com",
15
+ },
16
+ title: `${APP_NAME} API Documentation`,
17
+ version: "1.0.0",
18
+ description: `API documentation for the ${APP_NAME} application`,
19
+ license: {
20
+ name: "MIT",
21
+ url: "https://opensource.org/licenses/MIT",
22
+ },
23
+ termsOfService: "https://www.example.com/terms",
24
+ },
25
+ basePath: `/api/${API_VERSION}`,
26
+ consumes: ["application/json"],
27
+ produces: ["application/json"],
28
+ externalDocs: {
29
+ description: "Find out more",
30
+ url: "https://www.example.com/docs",
31
+ },
32
+ host: `${BACKEND_URL.replace(/^https?:\/\//, "")}`,
33
+ schemes: ["http", "https"],
34
+ tags: [{ name: "System", description: "System routes" }],
35
+ servers: [
36
+ {
37
+ url: `${BACKEND_URL}/api/${API_VERSION}`,
38
+ description: "Development server",
39
+ },
40
+ ],
41
+ },
42
+ apis: ["./src/routes/*.js"],
43
+ };
44
+
45
+ const theme = {
46
+ customFavIcon: "/favicon.ico",
47
+ customSiteTitle: `${APP_NAME} API`,
48
+ swaggerOptions: {
49
+ layout: "BaseLayout",
50
+ docExpansion: "none",
51
+ persistAuthorization: false,
52
+ },
53
+ customCss: `
54
+ .topbar { display: none !important; }
55
+ body { background: #ffffff !important; }
56
+ .swagger-ui { background: #ffffff !important; }
57
+ `,
58
+ };
59
+
60
+ const specs = swaggerJsdoc(options);
61
+
62
+ export { swaggerUi, specs, theme };
@@ -0,0 +1,6 @@
1
+ const API_VERSION = "v1";
2
+ const API_PREFIX = `/api/${API_VERSION}`;
3
+
4
+ const REQUEST_SIZE_LIMIT = "16kb";
5
+
6
+ export { API_VERSION, API_PREFIX, REQUEST_SIZE_LIMIT };
@@ -0,0 +1,5 @@
1
+ const APP_NAME = "Backend App";
2
+ const APP = "backend-app";
3
+ const DEVELOPER_NAME = "Harshit Ostwal";
4
+
5
+ export { APP_NAME, APP, DEVELOPER_NAME };
@@ -0,0 +1,90 @@
1
+ const HTTP_METHODS = {
2
+ GET: "GET",
3
+ POST: "POST",
4
+ PUT: "PUT",
5
+ PATCH: "PATCH",
6
+ DELETE: "DELETE",
7
+ OPTIONS: "OPTIONS",
8
+ HEAD: "HEAD",
9
+ };
10
+
11
+ const HTTP_STATUS = {
12
+ // Success
13
+ OK: 200,
14
+ CREATED: 201,
15
+ ACCEPTED: 202,
16
+ NO_CONTENT: 204,
17
+ PARTIAL_CONTENT: 206,
18
+
19
+ // Redirection (Optional but good to have)
20
+ MOVED_PERMANENTLY: 301,
21
+ FOUND: 302,
22
+ NOT_MODIFIED: 304,
23
+ TEMPORARY_REDIRECT: 307,
24
+ PERMANENT_REDIRECT: 308,
25
+
26
+ // Client Errors
27
+ BAD_REQUEST: 400,
28
+ UNAUTHORIZED: 401,
29
+ FORBIDDEN: 403,
30
+ NOT_FOUND: 404,
31
+ METHOD_NOT_ALLOWED: 405,
32
+ NOT_ACCEPTABLE: 406,
33
+ REQUEST_TIMEOUT: 408,
34
+ CONFLICT: 409,
35
+ GONE: 410,
36
+ PAYLOAD_TOO_LARGE: 413,
37
+ UNSUPPORTED_MEDIA_TYPE: 415,
38
+ UNPROCESSABLE_ENTITY: 422,
39
+ TOO_MANY_REQUESTS: 429,
40
+
41
+ // Server Errors
42
+ INTERNAL_SERVER_ERROR: 500,
43
+ NOT_IMPLEMENTED: 501,
44
+ BAD_GATEWAY: 502,
45
+ SERVICE_UNAVAILABLE: 503,
46
+ GATEWAY_TIMEOUT: 504,
47
+ HTTP_VERSION_NOT_SUPPORTED: 505,
48
+ };
49
+
50
+ const HTTP_MESSAGES = {
51
+ // Success
52
+ OK: "Success",
53
+ CREATED: "Resource created successfully",
54
+ ACCEPTED: "Request accepted",
55
+ NO_CONTENT: "No content",
56
+ PARTIAL_CONTENT: "Partial content",
57
+
58
+ // Redirection
59
+ MOVED_PERMANENTLY: "Moved permanently",
60
+ FOUND: "Found",
61
+ NOT_MODIFIED: "Not modified",
62
+ TEMPORARY_REDIRECT: "Temporary redirect",
63
+ PERMANENT_REDIRECT: "Permanent redirect",
64
+
65
+ // Client Errors
66
+ BAD_REQUEST: "Bad request",
67
+ UNAUTHORIZED: "Unauthorized",
68
+ FORBIDDEN: "Forbidden",
69
+ NOT_FOUND: "Resource not found",
70
+ METHOD_NOT_ALLOWED: "Method not allowed",
71
+ NOT_ACCEPTABLE: "Not acceptable",
72
+ REQUEST_TIMEOUT: "Request timeout",
73
+ CONFLICT: "Resource already exists",
74
+ GONE: "Resource no longer available",
75
+ PAYLOAD_TOO_LARGE: "Payload too large",
76
+ UNSUPPORTED_MEDIA_TYPE: "Unsupported media type",
77
+ UNPROCESSABLE_ENTITY: "Unprocessable entity",
78
+ VALIDATION_ERROR: "Validation error",
79
+ TOO_MANY_REQUESTS: "Too many requests",
80
+
81
+ // Server Errors
82
+ INTERNAL_SERVER_ERROR: "Internal server error",
83
+ NOT_IMPLEMENTED: "Not implemented",
84
+ BAD_GATEWAY: "Bad gateway",
85
+ SERVICE_UNAVAILABLE: "Service unavailable",
86
+ GATEWAY_TIMEOUT: "Gateway timeout",
87
+ HTTP_VERSION_NOT_SUPPORTED: "HTTP version not supported",
88
+ };
89
+
90
+ export { HTTP_METHODS, HTTP_STATUS, HTTP_MESSAGES };
@@ -0,0 +1,5 @@
1
+ import { APP } from "./api.constants.js";
2
+
3
+ const MAIL_QUEUE_NAME = `${APP}-mail-queue`;
4
+
5
+ export { MAIL_QUEUE_NAME };
@@ -0,0 +1,22 @@
1
+ import { ALLOWED_ORIGINS } from "../config/env.config.js";
2
+
3
+ const CORS_OPTIONS = {
4
+ origin: ALLOWED_ORIGINS?.split(",") || ["*"],
5
+ credentials: true,
6
+ methods: ["GET", "POST", "PATCH", "DELETE"],
7
+ allowedHeaders: ["Content-Type", "Authorization"],
8
+ };
9
+
10
+ const RATE_LIMIT = {
11
+ WINDOW_MS: 60 * 60 * 1000,
12
+ MAX_REQUESTS: 500,
13
+ };
14
+
15
+ const SALT_ROUNDS = 10;
16
+
17
+ const TOKEN_TYPE = {
18
+ ACCESS: "access",
19
+ REFRESH: "refresh",
20
+ };
21
+
22
+ export { CORS_OPTIONS, RATE_LIMIT, SALT_ROUNDS, TOKEN_TYPE };
@@ -0,0 +1,8 @@
1
+ const ValidationSource = {
2
+ BODY: "body",
3
+ QUERY: "query",
4
+ HEADER: "header",
5
+ PARAMS: "params",
6
+ };
7
+
8
+ export { ValidationSource };
File without changes
File without changes
@@ -0,0 +1,162 @@
1
+ import { HTTP_MESSAGES, HTTP_STATUS } from "../../constants/http.constants.js";
2
+
3
+ class ApiError extends Error {
4
+ constructor(
5
+ statusCode = HTTP_STATUS.INTERNAL_SERVER_ERROR,
6
+ message = HTTP_MESSAGES.INTERNAL_SERVER_ERROR,
7
+ errors = [],
8
+ isOperational = true,
9
+ options = {}
10
+ ) {
11
+ const { cause, code, stack } = options ?? {};
12
+
13
+ super(message, cause ? { cause } : undefined);
14
+
15
+ Object.setPrototypeOf(this, new.target.prototype);
16
+
17
+ this.name = "ApiError";
18
+ this.statusCode = statusCode;
19
+ this.success = false;
20
+ this.errors = Array.isArray(errors) ? errors : [];
21
+ this.isOperational = isOperational;
22
+
23
+ if (code !== undefined) this.code = code;
24
+ if (cause !== undefined) this.cause = cause;
25
+
26
+ if (stack) {
27
+ this.stack = stack;
28
+ } else if (typeof Error.captureStackTrace === "function") {
29
+ Error.captureStackTrace(this, this.constructor);
30
+ } else {
31
+ this.stack = new Error(message).stack ?? "";
32
+ }
33
+ }
34
+
35
+ static from(error, statusCode = HTTP_STATUS.INTERNAL_SERVER_ERROR) {
36
+ if (!error) return new ApiError(statusCode);
37
+ if (error instanceof ApiError) return error;
38
+
39
+ const message = error instanceof Error ? error.message : String(error);
40
+
41
+ return new ApiError(statusCode, message, error.errors ?? [], false, {
42
+ cause: error,
43
+ code: error instanceof Error ? error.code : undefined,
44
+ });
45
+ }
46
+
47
+ // 400
48
+ static badRequest(message = HTTP_MESSAGES.BAD_REQUEST, errors = []) {
49
+ return new ApiError(HTTP_STATUS.BAD_REQUEST, message, errors);
50
+ }
51
+
52
+ // 401
53
+ static unauthorized(message = HTTP_MESSAGES.UNAUTHORIZED, errors = []) {
54
+ return new ApiError(HTTP_STATUS.UNAUTHORIZED, message, errors);
55
+ }
56
+
57
+ // 403
58
+ static forbidden(message = HTTP_MESSAGES.FORBIDDEN, errors = []) {
59
+ return new ApiError(HTTP_STATUS.FORBIDDEN, message, errors);
60
+ }
61
+
62
+ // 404
63
+ static notFound(message = HTTP_MESSAGES.NOT_FOUND, errors = []) {
64
+ return new ApiError(HTTP_STATUS.NOT_FOUND, message, errors);
65
+ }
66
+
67
+ // 409
68
+ static conflict(message = HTTP_MESSAGES.CONFLICT, errors = []) {
69
+ return new ApiError(HTTP_STATUS.CONFLICT, message, errors);
70
+ }
71
+
72
+ // 410
73
+ static gone(message = HTTP_MESSAGES.GONE ?? "Gone", errors = []) {
74
+ return new ApiError(HTTP_STATUS.GONE ?? 410, message, errors);
75
+ }
76
+
77
+ // 422
78
+ static validationError(
79
+ message = HTTP_MESSAGES.VALIDATION_ERROR,
80
+ errors = []
81
+ ) {
82
+ return new ApiError(HTTP_STATUS.UNPROCESSABLE_ENTITY, message, errors);
83
+ }
84
+
85
+ // 429
86
+ static tooManyRequests(
87
+ message = HTTP_MESSAGES.TOO_MANY_REQUESTS,
88
+ errors = []
89
+ ) {
90
+ return new ApiError(HTTP_STATUS.TOO_MANY_REQUESTS, message, errors);
91
+ }
92
+
93
+ // 500
94
+ static internalServerError(
95
+ message = HTTP_MESSAGES.INTERNAL_SERVER_ERROR,
96
+ errors = []
97
+ ) {
98
+ return new ApiError(HTTP_STATUS.INTERNAL_SERVER_ERROR, message, errors);
99
+ }
100
+
101
+ // 501
102
+ static notImplemented(
103
+ message = HTTP_MESSAGES.NOT_IMPLEMENTED,
104
+ errors = []
105
+ ) {
106
+ return new ApiError(HTTP_STATUS.NOT_IMPLEMENTED, message, errors);
107
+ }
108
+
109
+ // 502
110
+ static badGateway(message = HTTP_MESSAGES.BAD_GATEWAY, errors = []) {
111
+ return new ApiError(HTTP_STATUS.BAD_GATEWAY, message, errors);
112
+ }
113
+
114
+ // 503
115
+ static serviceUnavailable(
116
+ message = HTTP_MESSAGES.SERVICE_UNAVAILABLE,
117
+ errors = []
118
+ ) {
119
+ return new ApiError(HTTP_STATUS.SERVICE_UNAVAILABLE, message, errors);
120
+ }
121
+
122
+ // 504
123
+ static gatewayTimeout(
124
+ message = HTTP_MESSAGES.GATEWAY_TIMEOUT,
125
+ errors = []
126
+ ) {
127
+ return new ApiError(HTTP_STATUS.GATEWAY_TIMEOUT, message, errors);
128
+ }
129
+
130
+ toJSON() {
131
+ return {
132
+ name: this.name,
133
+ success: false,
134
+ statusCode: this.statusCode,
135
+ message: this.message,
136
+ errors: this.errors,
137
+ ...(this.code !== undefined && { code: this.code }),
138
+ };
139
+ }
140
+
141
+ toDebugJSON() {
142
+ return {
143
+ ...this.toJSON(),
144
+ isOperational: this.isOperational,
145
+ stack: this.stack,
146
+ cause:
147
+ this.cause instanceof Error
148
+ ? { message: this.cause.message, stack: this.cause.stack }
149
+ : this.cause,
150
+ };
151
+ }
152
+
153
+ isClientError() {
154
+ return this.statusCode >= 400 && this.statusCode < 500;
155
+ }
156
+
157
+ isServerError() {
158
+ return this.statusCode >= 500;
159
+ }
160
+ }
161
+
162
+ export default ApiError;
@@ -0,0 +1,118 @@
1
+ import { HTTP_MESSAGES, HTTP_STATUS } from "../../constants/http.constants.js";
2
+
3
+ class ApiResponse {
4
+ constructor(
5
+ statusCode = HTTP_STATUS.OK,
6
+ data = null,
7
+ message = HTTP_MESSAGES.OK ?? "Success",
8
+ meta
9
+ ) {
10
+ if (
11
+ !Number.isInteger(statusCode) ||
12
+ statusCode < 100 ||
13
+ statusCode > 599
14
+ ) {
15
+ throw new TypeError(`Invalid HTTP status code: ${statusCode}`);
16
+ }
17
+
18
+ this.success = statusCode < 400;
19
+ this.statusCode = statusCode;
20
+ this.message =
21
+ typeof message === "string" && message.trim()
22
+ ? message.trim()
23
+ : "Success";
24
+ this.data = data !== undefined ? data : null;
25
+ this.timestamp = new Date().toISOString();
26
+
27
+ if (
28
+ meta !== null &&
29
+ meta !== undefined &&
30
+ typeof meta === "object" &&
31
+ !Array.isArray(meta)
32
+ ) {
33
+ this.meta = meta;
34
+ }
35
+ }
36
+
37
+ static ok(data, message = HTTP_MESSAGES.OK ?? "OK") {
38
+ return new ApiResponse(HTTP_STATUS.OK, data, message);
39
+ }
40
+
41
+ static created(data, message = HTTP_MESSAGES.CREATED ?? "Created") {
42
+ return new ApiResponse(HTTP_STATUS.CREATED, data, message);
43
+ }
44
+
45
+ static accepted(data, message = HTTP_MESSAGES.ACCEPTED ?? "Accepted") {
46
+ return new ApiResponse(HTTP_STATUS.ACCEPTED, data, message);
47
+ }
48
+
49
+ static noContent() {
50
+ return new ApiResponse(HTTP_STATUS.NO_CONTENT, null, null);
51
+ }
52
+
53
+ static paginated(
54
+ items,
55
+ { page, limit, total },
56
+ message = HTTP_MESSAGES.OK ?? "OK"
57
+ ) {
58
+ if (!Number.isInteger(limit) || limit < 1)
59
+ throw new TypeError(
60
+ `Pagination "limit" must be a positive integer, got: ${limit}`
61
+ );
62
+ if (!Number.isInteger(page) || page < 1)
63
+ throw new TypeError(
64
+ `Pagination "page" must be a positive integer, got: ${page}`
65
+ );
66
+ if (!Number.isInteger(total) || total < 0)
67
+ throw new TypeError(
68
+ `Pagination "total" must be a non-negative integer, got: ${total}`
69
+ );
70
+
71
+ const totalPages = total === 0 ? 0 : Math.ceil(total / limit);
72
+ const meta = {
73
+ page,
74
+ limit,
75
+ total,
76
+ totalPages,
77
+ hasNext: page < totalPages,
78
+ hasPrev: page > 1,
79
+ };
80
+
81
+ return new ApiResponse(HTTP_STATUS.OK, items, message, meta);
82
+ }
83
+
84
+ toJSON() {
85
+ return {
86
+ success: this.success,
87
+ statusCode: this.statusCode,
88
+ message: this.message,
89
+ data: this.data,
90
+ ...(this.meta !== undefined && { meta: this.meta }),
91
+ timestamp: this.timestamp,
92
+ };
93
+ }
94
+
95
+ send(res) {
96
+ if (this.statusCode === HTTP_STATUS.NO_CONTENT) {
97
+ return res.status(this.statusCode).end();
98
+ }
99
+
100
+ return res.status(this.statusCode).json(this.toJSON());
101
+ }
102
+
103
+ static redirect(res, url, statusCode = HTTP_STATUS.FOUND) {
104
+ if (
105
+ !Number.isInteger(statusCode) ||
106
+ statusCode < 300 ||
107
+ statusCode > 399
108
+ ) {
109
+ throw new TypeError(
110
+ `Invalid HTTP redirect status code: ${statusCode}`
111
+ );
112
+ }
113
+
114
+ return res.redirect(statusCode, url);
115
+ }
116
+ }
117
+
118
+ export default ApiResponse;