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,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,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,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,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 };
|
|
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;
|