@misterzik/espressojs 3.2.6 → 3.3.1

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.
@@ -0,0 +1,149 @@
1
+ /*
2
+ * EspressoJS - MongoDB Integration Example
3
+ * This file demonstrates how to use MongoDB with EspressoJS
4
+ */
5
+
6
+ const express = require('express');
7
+ const router = express.Router();
8
+ const mongoose = require('mongoose');
9
+ const { asyncHandler, AppError } = require('../server/middleware/errorHandler');
10
+
11
+ // Example Schema
12
+ const UserSchema = new mongoose.Schema({
13
+ name: {
14
+ type: String,
15
+ required: [true, 'Name is required'],
16
+ trim: true,
17
+ maxlength: [50, 'Name cannot exceed 50 characters']
18
+ },
19
+ email: {
20
+ type: String,
21
+ required: [true, 'Email is required'],
22
+ unique: true,
23
+ lowercase: true,
24
+ match: [/^\S+@\S+\.\S+$/, 'Please provide a valid email']
25
+ },
26
+ age: {
27
+ type: Number,
28
+ min: [0, 'Age must be positive']
29
+ },
30
+ createdAt: {
31
+ type: Date,
32
+ default: Date.now
33
+ },
34
+ updatedAt: {
35
+ type: Date,
36
+ default: Date.now
37
+ }
38
+ });
39
+
40
+ // Update timestamp on save
41
+ UserSchema.pre('save', function(next) {
42
+ this.updatedAt = Date.now();
43
+ next();
44
+ });
45
+
46
+ const User = mongoose.model('User', UserSchema);
47
+
48
+ // GET all users with pagination
49
+ router.get('/users', asyncHandler(async (req, res) => {
50
+ const page = parseInt(req.query.page) || 1;
51
+ const limit = parseInt(req.query.limit) || 10;
52
+ const skip = (page - 1) * limit;
53
+
54
+ const users = await User.find()
55
+ .select('-__v')
56
+ .limit(limit)
57
+ .skip(skip)
58
+ .sort({ createdAt: -1 });
59
+
60
+ const total = await User.countDocuments();
61
+
62
+ res.status(200).json({
63
+ status: 'success',
64
+ results: users.length,
65
+ pagination: {
66
+ page,
67
+ limit,
68
+ total,
69
+ pages: Math.ceil(total / limit)
70
+ },
71
+ data: { users }
72
+ });
73
+ }));
74
+
75
+ // GET user by ID
76
+ router.get('/users/:id', asyncHandler(async (req, res) => {
77
+ const user = await User.findById(req.params.id).select('-__v');
78
+
79
+ if (!user) {
80
+ throw new AppError('User not found', 404);
81
+ }
82
+
83
+ res.status(200).json({
84
+ status: 'success',
85
+ data: { user }
86
+ });
87
+ }));
88
+
89
+ // POST create new user
90
+ router.post('/users', asyncHandler(async (req, res) => {
91
+ const user = await User.create(req.body);
92
+
93
+ res.status(201).json({
94
+ status: 'success',
95
+ data: { user }
96
+ });
97
+ }));
98
+
99
+ // PUT update user
100
+ router.put('/users/:id', asyncHandler(async (req, res) => {
101
+ const user = await User.findByIdAndUpdate(
102
+ req.params.id,
103
+ req.body,
104
+ {
105
+ new: true,
106
+ runValidators: true
107
+ }
108
+ ).select('-__v');
109
+
110
+ if (!user) {
111
+ throw new AppError('User not found', 404);
112
+ }
113
+
114
+ res.status(200).json({
115
+ status: 'success',
116
+ data: { user }
117
+ });
118
+ }));
119
+
120
+ // DELETE user
121
+ router.delete('/users/:id', asyncHandler(async (req, res) => {
122
+ const user = await User.findByIdAndDelete(req.params.id);
123
+
124
+ if (!user) {
125
+ throw new AppError('User not found', 404);
126
+ }
127
+
128
+ res.status(204).send();
129
+ }));
130
+
131
+ // GET users by query
132
+ router.get('/users/search/:query', asyncHandler(async (req, res) => {
133
+ const { query } = req.params;
134
+
135
+ const users = await User.find({
136
+ $or: [
137
+ { name: { $regex: query, $options: 'i' } },
138
+ { email: { $regex: query, $options: 'i' } }
139
+ ]
140
+ }).select('-__v');
141
+
142
+ res.status(200).json({
143
+ status: 'success',
144
+ results: users.length,
145
+ data: { users }
146
+ });
147
+ }));
148
+
149
+ module.exports = router;
@@ -0,0 +1,149 @@
1
+ /*
2
+ * EspressoJS - Multiple API Endpoints Example
3
+ * This example demonstrates how to use multiple API configurations
4
+ */
5
+
6
+ const express = require('express');
7
+ const router = express.Router();
8
+ const { asyncHandler, AppError } = require('../server/middleware/errorHandler');
9
+ const { apiManager } = require('../index');
10
+
11
+ // Example 1: Using the API Manager directly
12
+ router.get('/news', asyncHandler(async (req, res) => {
13
+ // Make a request to api2 endpoint
14
+ const newsData = await apiManager.request('api2', '');
15
+
16
+ res.status(200).json({
17
+ status: 'success',
18
+ source: 'api2',
19
+ data: newsData
20
+ });
21
+ }));
22
+
23
+ // Example 2: Using multiple APIs in a single endpoint
24
+ router.get('/combined-data', asyncHandler(async (req, res) => {
25
+ // Check which APIs are available
26
+ const availableAPIs = apiManager.getAllAPIs();
27
+
28
+ // Make parallel requests to multiple APIs
29
+ const results = await Promise.allSettled([
30
+ apiManager.request('api', '/endpoint1'),
31
+ apiManager.request('api2', ''),
32
+ apiManager.request('api3', '/data')
33
+ ]);
34
+
35
+ const data = {
36
+ api1: results[0].status === 'fulfilled' ? results[0].value : null,
37
+ api2: results[1].status === 'fulfilled' ? results[1].value : null,
38
+ api3: results[2].status === 'fulfilled' ? results[2].value : null,
39
+ };
40
+
41
+ res.status(200).json({
42
+ status: 'success',
43
+ availableAPIs: Object.keys(availableAPIs),
44
+ data
45
+ });
46
+ }));
47
+
48
+ // Example 3: Using specific API with custom options
49
+ router.get('/custom-request', asyncHandler(async (req, res) => {
50
+ const data = await apiManager.request('api3', '/custom-endpoint', {
51
+ method: 'POST',
52
+ data: { query: 'example' },
53
+ headers: {
54
+ 'Custom-Header': 'value'
55
+ },
56
+ timeout: 5000,
57
+ retries: 2
58
+ });
59
+
60
+ res.status(200).json({
61
+ status: 'success',
62
+ data
63
+ });
64
+ }));
65
+
66
+ // Example 4: Creating a custom Axios instance for an API
67
+ router.get('/axios-instance', asyncHandler(async (req, res) => {
68
+ // Create a custom axios instance for api2
69
+ const api2Client = apiManager.createAxiosInstance('api2');
70
+
71
+ // Use it like regular axios
72
+ const response = await api2Client.get('/specific-endpoint');
73
+
74
+ res.status(200).json({
75
+ status: 'success',
76
+ data: response.data
77
+ });
78
+ }));
79
+
80
+ // Example 5: Conditional API usage based on availability
81
+ router.get('/smart-fetch', asyncHandler(async (req, res) => {
82
+ let data;
83
+
84
+ // Try primary API first, fallback to secondary
85
+ if (apiManager.hasAPI('api')) {
86
+ try {
87
+ data = await apiManager.request('api', '/data');
88
+ } catch (error) {
89
+ // Fallback to api3 if api fails
90
+ if (apiManager.hasAPI('api3')) {
91
+ data = await apiManager.request('api3', '/data');
92
+ } else {
93
+ throw new AppError('No API endpoints available', 503);
94
+ }
95
+ }
96
+ } else if (apiManager.hasAPI('api3')) {
97
+ data = await apiManager.request('api3', '/data');
98
+ } else {
99
+ throw new AppError('No API endpoints configured', 503);
100
+ }
101
+
102
+ res.status(200).json({
103
+ status: 'success',
104
+ data
105
+ });
106
+ }));
107
+
108
+ // Example 6: List all configured APIs
109
+ router.get('/api-info', (req, res) => {
110
+ const apis = apiManager.getAllAPIs();
111
+
112
+ const apiInfo = Object.entries(apis).map(([name, config]) => ({
113
+ name,
114
+ baseURL: config.baseURL,
115
+ method: config.method,
116
+ timeout: config.timeout,
117
+ retries: config.retries
118
+ }));
119
+
120
+ res.status(200).json({
121
+ status: 'success',
122
+ count: apiInfo.length,
123
+ apis: apiInfo
124
+ });
125
+ });
126
+
127
+ // Example 7: Proxy endpoint for any API
128
+ router.get('/proxy/:apiName/*', asyncHandler(async (req, res) => {
129
+ const { apiName } = req.params;
130
+ const endpoint = req.params[0];
131
+
132
+ if (!apiManager.hasAPI(apiName)) {
133
+ throw new AppError(`API '${apiName}' not found`, 404);
134
+ }
135
+
136
+ const data = await apiManager.request(apiName, `/${endpoint}`, {
137
+ method: req.method,
138
+ params: req.query
139
+ });
140
+
141
+ res.status(200).json({
142
+ status: 'success',
143
+ api: apiName,
144
+ endpoint,
145
+ data
146
+ });
147
+ }));
148
+
149
+ module.exports = router;
package/index.js CHANGED
@@ -17,7 +17,11 @@ const {
17
17
  readConfigFile,
18
18
  setCustomCacheControl,
19
19
  } = require("./server/utils/config.utils");
20
+ const { validateEnvVariables } = require("./server/utils/configValidator");
21
+ const logger = require("./server/utils/logger");
22
+ const APIManager = require("./server/utils/apiManager");
20
23
  const configData = readConfigFile();
24
+ const apiManager = new APIManager(configData);
21
25
 
22
26
  const Path = require("path");
23
27
  const Cors = require("cors");
@@ -25,17 +29,35 @@ const Compression = require("compression");
25
29
  const Favicon = require("serve-favicon");
26
30
  const Static = require("serve-static");
27
31
  const mongoose = require("mongoose");
32
+ const morgan = require("morgan");
28
33
  const Routes = require("./routes/index");
29
34
 
35
+ const {
36
+ helmetConfig,
37
+ rateLimiter,
38
+ } = require("./server/middleware/security");
39
+ const {
40
+ errorHandler,
41
+ notFoundHandler,
42
+ } = require("./server/middleware/errorHandler");
43
+ const {
44
+ healthCheck,
45
+ readinessCheck,
46
+ livenessCheck,
47
+ } = require("./server/middleware/healthCheck");
48
+
30
49
  const Port = configData.port || cfg.port;
31
- const mongoConfig = configData.mongo;
50
+ const mongoConfig = configData.mongoDB;
32
51
  const rootDir = process.cwd();
52
+ const publicDir = configData.publicDirectory || "/public";
53
+
54
+ validateEnvVariables();
33
55
 
34
- if (configData.mongo_isEnabled) {
56
+ if (configData.mongoDB.enabled) {
35
57
  const {
36
- uri: mongoUri = configData.mongo.uri || "",
37
- port: mongoPort = configData.mongo.port || "",
38
- db: mongoDb = configData.mongo.db || "",
58
+ uri: mongoUri = configData.mongoDB.uri || "",
59
+ port: mongoPort = configData.mongoDB.port || "",
60
+ db: mongoDb = configData.mongoDB.instance || "",
39
61
  } = mongoConfig;
40
62
  const hasPort = mongoPort ? `:${mongoPort}/` : "/";
41
63
  const url = `mongodb+srv://${
@@ -55,26 +77,103 @@ if (configData.mongo_isEnabled) {
55
77
  useNewUrlParser: true,
56
78
  promiseLibrary: require("bluebird"),
57
79
  })
58
- .then(() => console.log(":: DB Connection successful ::"))
59
- .catch((err) => console.error(err));
80
+ .then(() => logger.info(":: DB Connection successful ::"))
81
+ .catch((err) => logger.error(`DB Connection error: ${err.message}`));
60
82
  }
61
83
 
84
+ app.use(helmetConfig());
62
85
  app.use(Compression());
63
86
  app.use(Cors());
64
- app.use(express.urlencoded({ extended: false }));
65
- app.use(express.json());
66
- app.use(Favicon(Path.join(rootDir, "public", "favicon.ico")));
87
+ app.use(express.urlencoded({ extended: false, limit: "10mb" }));
88
+ app.use(express.json({ limit: "10mb" }));
89
+
90
+ const morganFormat = process.env.NODE_ENV === "production" ? "combined" : "dev";
91
+ app.use(
92
+ morgan(morganFormat, {
93
+ stream: {
94
+ write: (message) => logger.http(message.trim()),
95
+ },
96
+ })
97
+ );
98
+
99
+ app.use(rateLimiter);
100
+
101
+ app.get("/health", healthCheck);
102
+ app.get("/ready", readinessCheck);
103
+ app.get("/alive", livenessCheck);
104
+
105
+ app.use(Favicon(Path.join(rootDir, publicDir, "favicon.ico")));
67
106
  app.use(
68
- Static(Path.join(rootDir, "public"), {
107
+ Static(Path.join(rootDir, publicDir), {
69
108
  maxAge: "1d",
70
109
  setHeaders: setCustomCacheControl,
71
110
  etag: true,
72
111
  extensions: "error.html",
73
112
  })
74
113
  );
114
+
75
115
  Routes(app);
76
- app.listen(Port, () => {
77
- console.log(`Server is running on port ${Port}`);
116
+
117
+ app.use(notFoundHandler);
118
+ app.use(errorHandler);
119
+
120
+ let server;
121
+
122
+ const startServer = () => {
123
+ server = app.listen(Port, () => {
124
+ logger.info(`
125
+ ╔═══════════════════════════════════════════════════════╗
126
+ ║ ESPRESSO.JS ║
127
+ ║ Express Boilerplate Server ║
128
+ ╠═══════════════════════════════════════════════════════╣
129
+ ║ Environment: ${configData.instance.padEnd(39)} ║
130
+ ║ Port: ${Port.toString().padEnd(39)} ║
131
+ ║ URL: http://localhost:${Port.toString().padEnd(27)} ║
132
+ ║ MongoDB: ${(configData.mongoDB.enabled ? "Enabled" : "Disabled").padEnd(39)} ║
133
+ ║ API: ${(configData.api.enabled ? "Enabled" : "Disabled").padEnd(39)} ║
134
+ ╚═══════════════════════════════════════════════════════╝
135
+ `);
136
+ });
137
+ };
138
+
139
+ const gracefulShutdown = (signal) => {
140
+ logger.info(`${signal} received. Starting graceful shutdown...`);
141
+
142
+ server.close(() => {
143
+ logger.info("HTTP server closed");
144
+
145
+ if (mongoose.connection.readyState === 1) {
146
+ mongoose.connection.close(false, () => {
147
+ logger.info("MongoDB connection closed");
148
+ process.exit(0);
149
+ });
150
+ } else {
151
+ process.exit(0);
152
+ }
153
+ });
154
+
155
+ setTimeout(() => {
156
+ logger.error("Forced shutdown after timeout");
157
+ process.exit(1);
158
+ }, 10000);
159
+ };
160
+
161
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
162
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
163
+
164
+ process.on("unhandledRejection", (reason, promise) => {
165
+ logger.error(`Unhandled Rejection at: ${promise}, reason: ${reason}`);
166
+ });
167
+
168
+ process.on("uncaughtException", (error) => {
169
+ logger.error(`Uncaught Exception: ${error.message}`);
170
+ gracefulShutdown("UNCAUGHT_EXCEPTION");
78
171
  });
79
172
 
173
+ if (require.main === module) {
174
+ startServer();
175
+ }
176
+
80
177
  module.exports = app;
178
+ module.exports.apiManager = apiManager;
179
+ module.exports.config = configData;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterzik/espressojs",
3
- "version": "3.2.6",
3
+ "version": "3.3.1",
4
4
  "description": "EspressoJS Introducing Espresso.JS, your ultimate Express configuration starting point and boilerplate. With its simplicity and lack of opinionation, EspressoJS offers plug-and-play configurations built on top of Express.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -36,17 +36,23 @@
36
36
  },
37
37
  "homepage": "https://github.com/misterzik/Espresso.js#readme",
38
38
  "dependencies": {
39
- "axios": "^1.4.0",
39
+ "axios": "^1.6.0",
40
40
  "bluebird": "^3.7.2",
41
+ "compression": "^1.7.4",
42
+ "cors": "^2.8.5",
41
43
  "cross-env": "^7.0.3",
42
- "express": "^4.18.2",
43
- "mongoose": "^7.3.1",
44
44
  "dotenv": "^16.3.1",
45
- "cors": "^2.8.5",
45
+ "express": "^4.18.2",
46
+ "express-rate-limit": "^7.1.5",
47
+ "express-validator": "^7.0.1",
46
48
  "finalhandler": "^1.2.0",
47
- "compression": "^1.7.4",
48
- "serve-static": "^1.15.0",
49
+ "helmet": "^7.1.0",
50
+ "joi": "^17.11.0",
51
+ "mongoose": "^8.0.3",
52
+ "morgan": "^1.10.0",
49
53
  "serve-favicon": "^2.5.0",
54
+ "serve-static": "^1.15.0",
55
+ "winston": "^3.11.0",
50
56
  "yargs": "^17.7.2"
51
57
  },
52
58
  "devDependencies": {
package/routes/index.js CHANGED
@@ -10,27 +10,64 @@
10
10
  */
11
11
 
12
12
  const path = require("path");
13
+ const fs = require("fs");
13
14
  const configuration = require("../server");
15
+ const logger = require("../server/utils/logger");
16
+ const { asyncHandler } = require("../server/middleware/errorHandler");
14
17
  const rootDir = process.cwd();
15
- const api = require(path.join(rootDir, "routes", "api.js"));
16
- const db = require(path.join(rootDir, "routes", "db.js"));
17
18
 
18
19
  module.exports = (app) => {
19
- app.get("/", function (req, res) {
20
- const filePath = path.join(rootDir, "public", "index.html");
21
- res.sendFile(filePath);
22
- });
20
+ app.get(
21
+ "/",
22
+ asyncHandler(async (req, res) => {
23
+ const filePath = path.join(rootDir, "public", "index.html");
24
+
25
+ if (!fs.existsSync(filePath)) {
26
+ logger.warn("index.html not found, sending default response");
27
+ return res.status(200).json({
28
+ message: "Welcome to EspressoJS",
29
+ version: "3.2.6",
30
+ documentation: "https://github.com/misterzik/Espresso.js",
31
+ });
32
+ }
33
+
34
+ res.sendFile(filePath);
35
+ })
36
+ );
37
+
23
38
  if (configuration.api_isEnabled === true) {
24
- app.use("/api", api);
39
+ try {
40
+ const apiRoutes = require(path.join(rootDir, "routes", "api.js"));
41
+ app.use("/api", apiRoutes);
42
+ logger.info("API routes loaded successfully");
43
+ } catch (error) {
44
+ logger.warn(`API routes not found or failed to load: ${error.message}`);
45
+ }
25
46
  }
47
+
26
48
  if (configuration.mongo_isEnabled === true) {
27
- app.use("/api", db);
49
+ try {
50
+ const dbRoutes = require(path.join(rootDir, "routes", "db.js"));
51
+ app.use("/api/db", dbRoutes);
52
+ logger.info("Database routes loaded successfully");
53
+ } catch (error) {
54
+ logger.warn(`Database routes not found or failed to load: ${error.message}`);
55
+ }
28
56
  }
29
- app.get("/*", function (req, res) {
30
- const filePath = path.join(rootDir, "public", "index.html");
31
- res.sendFile(filePath);
32
- });
33
- app.use(function (req, res, next) {
34
- res.status(404).send("404 - Sorry can't find that!");
35
- });
57
+
58
+ app.get(
59
+ "/*",
60
+ asyncHandler(async (req, res) => {
61
+ const filePath = path.join(rootDir, "public", "index.html");
62
+
63
+ if (!fs.existsSync(filePath)) {
64
+ return res.status(404).json({
65
+ status: "error",
66
+ message: "Page not found",
67
+ });
68
+ }
69
+
70
+ res.sendFile(filePath);
71
+ })
72
+ );
36
73
  };
@@ -0,0 +1,73 @@
1
+ /*
2
+ * _| _| _| _| _|_|_|
3
+ * _| _| _|_| _|_| _| _|
4
+ * _| _| _| _| _| _| _|
5
+ * _| _| _| _| _| _|
6
+ * _| _| _| _|_|_|
7
+ * EspressoJS - Error Handler Middleware
8
+ */
9
+
10
+ const logger = require("../utils/logger");
11
+
12
+ class AppError extends Error {
13
+ constructor(message, statusCode) {
14
+ super(message);
15
+ this.statusCode = statusCode;
16
+ this.status = `${statusCode}`.startsWith("4") ? "fail" : "error";
17
+ this.isOperational = true;
18
+
19
+ Error.captureStackTrace(this, this.constructor);
20
+ }
21
+ }
22
+
23
+ const errorHandler = (err, req, res, next) => {
24
+ err.statusCode = err.statusCode || 500;
25
+ err.status = err.status || "error";
26
+
27
+ logger.error(
28
+ `${err.statusCode} - ${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`
29
+ );
30
+
31
+ if (process.env.NODE_ENV === "development") {
32
+ res.status(err.statusCode).json({
33
+ status: err.status,
34
+ error: err,
35
+ message: err.message,
36
+ stack: err.stack,
37
+ });
38
+ } else {
39
+ if (err.isOperational) {
40
+ res.status(err.statusCode).json({
41
+ status: err.status,
42
+ message: err.message,
43
+ });
44
+ } else {
45
+ logger.error("ERROR 💥", err);
46
+ res.status(500).json({
47
+ status: "error",
48
+ message: "Something went wrong!",
49
+ });
50
+ }
51
+ }
52
+ };
53
+
54
+ const notFoundHandler = (req, res, next) => {
55
+ const err = new AppError(
56
+ `Cannot find ${req.originalUrl} on this server!`,
57
+ 404
58
+ );
59
+ next(err);
60
+ };
61
+
62
+ const asyncHandler = (fn) => {
63
+ return (req, res, next) => {
64
+ Promise.resolve(fn(req, res, next)).catch(next);
65
+ };
66
+ };
67
+
68
+ module.exports = {
69
+ AppError,
70
+ errorHandler,
71
+ notFoundHandler,
72
+ asyncHandler,
73
+ };