Skip to content

Commit e54e698

Browse files
authored
Merge pull request #10 from foyzulkarim/feature/05-configuration-management
Implement environment-based config loading with Joi validation
2 parents c0eadd5 + 7eda134 commit e54e698

14 files changed

+169
-54
lines changed

.gitignore

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,6 @@ web_modules/
7272
# Yarn Integrity file
7373
.yarn-integrity
7474

75-
# dotenv environment variable files
76-
.env
77-
.env.development.local
78-
.env.test.local
79-
.env.production.local
80-
.env.local
81-
8275
# parcel-bundler cache (https://parceljs.org/)
8376
.cache
8477
.parcel-cache
@@ -128,3 +121,11 @@ dist
128121
.yarn/build-state.yml
129122
.yarn/install-state.gz
130123
.pnp.*
124+
125+
# ignore .env and config.{}.json files
126+
.env.development
127+
.env.production
128+
.env.test
129+
config.development.json
130+
config.production.json
131+
config.test.json

src/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
MONGODB_URI=mongodb://localhost:27017/your-database-name

src/.env.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
MONGODB_URI=mongodb://localhost:27018/testdb

src/configs/config.example.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"NODE_ENV": "development",
3+
"MONGODB_URI": "mongodb://localhost:27017/mydatabase",
4+
"RATE": 40,
5+
"PORT": 4000
6+
}

src/configs/config.schema.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const Joi = require("joi");
2+
3+
const schema = Joi.object({
4+
NODE_ENV: Joi.string()
5+
.valid("development", "production", "test")
6+
.default("development"),
7+
MONGODB_URI: Joi.string().required(),
8+
RATE: Joi.number().min(0).required(),
9+
PORT: Joi.number().min(1000).default(4000),
10+
});
11+
12+
module.exports = schema;

src/configs/config.shared.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"RATE": 100
3+
}

src/configs/config.test.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

src/configs/index.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const dotenv = require("dotenv");
2+
const fs = require("fs");
3+
const path = require("path");
4+
5+
const logger = require("../libraries/log/logger");
6+
const schema = require("./config.schema");
7+
8+
class Config {
9+
constructor() {
10+
if (!Config.instance) {
11+
logger.info("Loading and validating config for the first time...");
12+
this.config = this.loadAndValidateConfig();
13+
Config.instance = this;
14+
logger.info("Config loaded and validated");
15+
}
16+
17+
return Config.instance;
18+
}
19+
20+
loadAndValidateConfig() {
21+
const environment = process.env.NODE_ENV || "development";
22+
23+
// 1. Load environment file from one level up and using __dirname
24+
const envFile = `.env.${environment}`;
25+
const envPath = path.join(__dirname, "..", envFile);
26+
if (!fs.existsSync(envPath)) {
27+
throw new Error(`Environment file not found: ${envPath}`);
28+
}
29+
dotenv.config({ path: envPath });
30+
31+
// 2. Load config file based on environment
32+
// Construct the path to the config file
33+
const configFile = path.join(__dirname, `config.${environment}.json`);
34+
35+
// Check if the file exists before trying to read it
36+
if (!fs.existsSync(configFile)) {
37+
throw new Error(`Config file not found: ${configFile}`);
38+
}
39+
40+
let config = JSON.parse(fs.readFileSync(configFile));
41+
42+
const sharedConfigFile = path.join(__dirname, "config.shared.json");
43+
if (fs.existsSync(sharedConfigFile)) {
44+
const sharedConfig = JSON.parse(fs.readFileSync(sharedConfigFile));
45+
config = { ...sharedConfig, ...config };
46+
}
47+
48+
const finalConfig = {};
49+
for (const key in schema.describe().keys) {
50+
if (process.env.hasOwnProperty(key)) {
51+
finalConfig[key] = process.env[key]; // Prioritize environment variables
52+
} else if (config.hasOwnProperty(key)) {
53+
finalConfig[key] = config[key]; // Fallback to config file value
54+
}
55+
}
56+
57+
// 4. load the schema file
58+
if (!schema) {
59+
throw new Error(`Schema file not found`);
60+
}
61+
62+
const { error, value: validatedConfig } = schema.validate(finalConfig);
63+
if (error) {
64+
const missingProperties = error.details.map((detail) => detail.path[0]);
65+
throw new Error(
66+
`Config validation error: missing properties ${missingProperties}`,
67+
);
68+
}
69+
return validatedConfig;
70+
}
71+
72+
static getInstance() {
73+
if (!Config.instance) {
74+
new Config();
75+
}
76+
return Config.instance;
77+
}
78+
}
79+
80+
module.exports = Config.getInstance().config;

src/domains/product/request.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ const mongoose = require('mongoose');
33

44
const createSchema = Joi.object().keys({
55
name: Joi.string().required(),
6-
// other properties
6+
description: Joi.string().required(),
7+
price: Joi.number().required(),
8+
inStock: Joi.boolean().optional(),
79
});
810

911
const updateSchema = Joi.object().keys({
1012
name: Joi.string(),
11-
// other properties
13+
description: Joi.string(),
14+
price: Joi.number(),
15+
inStock: Joi.boolean(),
1216
});
1317

1418
const idSchema = Joi.object().keys({

src/libraries/db/index.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const mongoose = require("mongoose");
2+
const logger = require("../log/logger");
3+
4+
const config = require("../../configs");
5+
6+
const connectWithMongoDb = async () => {
7+
const MONGODB_URI = config.MONGODB_URI;
8+
9+
logger.info("Connecting to MongoDB...");
10+
mongoose.connection.once("open", () => {
11+
logger.info("MongoDB connection is open");
12+
});
13+
mongoose.connection.on("error", (error) => {
14+
logger.error("MongoDB connection error", error);
15+
});
16+
17+
await mongoose.connect(MONGODB_URI, {
18+
autoIndex: true,
19+
autoCreate: true,
20+
});
21+
logger.info("Connected to MongoDB");
22+
};
23+
24+
const disconnectWithMongoDb = async () => {
25+
logger.info("Disconnecting from MongoDB...");
26+
await mongoose.disconnect();
27+
logger.info("Disconnected from MongoDB");
28+
};
29+
30+
module.exports = { connectWithMongoDb, disconnectWithMongoDb };

src/server.js

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
const mongoose = require('mongoose');
2-
const http = require('http');
3-
const net = require('net');
4-
const express = require('express');
5-
const helmet = require('helmet');
6-
const defineRoutes = require('./app');
1+
const config = require("./configs");
2+
const express = require("express");
3+
const helmet = require("helmet");
74

8-
const { errorHandler } = require('./libraries/error-handling');
9-
const logger = require('./libraries/log/logger');
10-
const { addRequestIdMiddleware } = require('./middlewares/request-context');
5+
const defineRoutes = require("./app");
6+
const { errorHandler } = require("./libraries/error-handling");
7+
const logger = require("./libraries/log/logger");
8+
const { addRequestIdMiddleware } = require("./middlewares/request-context");
9+
const { connectWithMongoDb } = require("./libraries/db");
1110

1211
let connection;
1312

@@ -24,14 +23,14 @@ const createExpressApp = () => {
2423
next();
2524
});
2625

27-
logger.info('Express middlewares are set up');
26+
logger.info("Express middlewares are set up");
2827
defineRoutes(expressApp);
2928
defineErrorHandlingMiddleware(expressApp);
3029
return expressApp;
3130
};
3231

3332
async function startWebServer() {
34-
logger.info('Starting web server...');
33+
logger.info("Starting web server...");
3534
const expressApp = createExpressApp();
3635
const APIAddress = await openConnection(expressApp);
3736
logger.info(`Server is running on ${APIAddress.address}:${APIAddress.port}`);
@@ -51,7 +50,7 @@ async function stopWebServer() {
5150

5251
async function openConnection(expressApp) {
5352
return new Promise((resolve) => {
54-
const webServerPort = process.env.PORT || 4000;
53+
const webServerPort = config.PORT;
5554
logger.info(`Server is about to listen to port ${webServerPort}`);
5655

5756
connection = expressApp.listen(webServerPort, () => {
@@ -64,34 +63,15 @@ async function openConnection(expressApp) {
6463
function defineErrorHandlingMiddleware(expressApp) {
6564
expressApp.use(async (error, req, res, next) => {
6665
// Note: next is required for Express error handlers
67-
if (error && typeof error === 'object') {
66+
if (error && typeof error === "object") {
6867
if (error.isTrusted === undefined || error.isTrusted === null) {
6968
error.isTrusted = true;
7069
}
7170
}
72-
71+
7372
errorHandler.handleError(error);
7473
res.status(error?.HTTPStatus || 500).end();
7574
});
7675
}
7776

78-
const connectWithMongoDb = async () => {
79-
const MONGODB_URI =
80-
process.env.MONGODB_URI || 'mongodb://localhost:27017/express-mongoose';
81-
82-
logger.info('Connecting to MongoDB...');
83-
mongoose.connection.once('open', () => {
84-
logger.info('MongoDB connection is open');
85-
});
86-
mongoose.connection.on('error', (error) => {
87-
logger.error('MongoDB connection error', error);
88-
});
89-
90-
await mongoose.connect(MONGODB_URI, {
91-
autoIndex: true,
92-
autoCreate: true,
93-
});
94-
logger.info('Connected to MongoDB');
95-
};
96-
9777
module.exports = { createExpressApp, startWebServer, stopWebServer };

src/start.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
const { startWebServer } = require('./server');
22

33
const start = async () => {
4-
console.log('Hello World');
54
await startWebServer();
65
};
76

test/globalSetup.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ module.exports = async function globalSetup() {
55
const instance = await MongoMemoryServer.create({
66
instance: {
77
dbName: 'testdb',
8+
port: 27018,
89
},
910
});
10-
const uri = instance.getUri();
1111
global.__MONGOINSTANCE = instance;
12-
process.env.MONGODB_URL = uri.slice(0, uri.lastIndexOf('/'));
1312
};

test/setupFile.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,19 @@
1-
const { beforeAll, afterAll } = require('@jest/globals');
2-
const mongoose = require('mongoose');
1+
const { beforeAll, afterAll } = require("@jest/globals");
2+
const mongoose = require("mongoose");
3+
const { connectWithMongoDb, disconnectWithMongoDb } = require("../src/libraries/db");
34

45
beforeAll(async () => {
5-
await mongoose.connect(process.env.MONGODB_URL, {
6-
autoIndex: true,
7-
autoCreate: true,
8-
});
6+
await connectWithMongoDb();
97
});
108

119
afterAll(async () => {
12-
await mongoose.connection.close();
10+
await disconnectWithMongoDb();
1311
});
1412

1513
// test mongoose connection is open
1614

17-
describe('Mongoose connection', () => {
18-
it('should be open', () => {
15+
describe("Mongoose connection", () => {
16+
it("should be open", () => {
1917
expect(mongoose.connection.readyState).toBe(1);
2018
});
2119
});

0 commit comments

Comments
 (0)