Today I was trying to find a way to separate global variable between the modes: prod and dev.
I've hidden sensitive information in "process.env" using a third party module "dotenv" but it would still be very comfortable to have proper information there whether I am in a development mode or production. For instance, if I am working locally I am using my local or cloud test DB and when I am in a prod mode I'd like to have proper credentials for a real db. So it switches automatically depending on the current mode.
Below you can see what I have come up with so far. I would appreciate any recommendations or suggestions on the structure issue or practice, experience.
Thank you in advance!
server.js
import { environment } from "./environment";
import { apiExplorer } from "./graphql";
import express from "express";
import { ApolloServer } from "apollo-server-express";
import { database } from "./utils/database";
import { logger } from "./utils/logging";
import { verify } from "./utils/jwt";
database.connect();
apiExplorer.getSchema().then((schema) => {
// Configure express
const port = environment.port;
const app = express();
// Configure apollo
const apolloServer = new ApolloServer({
schema,
context: ({ req, res }) => {
const context = [];
// verify jwt token
context.authUser = verify(req, res);
return context;
},
formatError: (error) => {
logger.error(error);
return error;
},
debug: true
});
apolloServer.applyMiddleware({ app });
app.listen({ port }, () => {
logger.info(`🚀Server ready at http://localhost:${port}${apolloServer.graphqlPath}`);
});
})
.catch((err) => {
logger.error('Failed to load api', err);
})
db class
import mongoose from 'mongoose';
import { environment } from "../environment";
import { logger } from './logging';
class Database {
constructor() {
this._database = 'MongoDB';
this._username = environment.db.username;
this._password = environment.db.password;
this._dbName = environment.db.name;
}
connect() {
mongoose.Promise = global.Promise;
const url = `mongodb+srv://${this._username}:${this._password}@cocoondb-qx9lu.mongodb.net/${this._dbName}?retryWrites=true&w=majority`;
try {
mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true });
mongoose.connection.once('open', () => logger.info(`Connected to ${this._database}`));
mongoose.connection.on('error', err => logger.error(err));
} catch (e) {
logger.error(`Something went wrong trying to connect to the database: ${this._database}`)
}
}
}
export const database = new Database();
environment/index.js
import { development } from './develepment';
import { production } from './production';
import { merge } from "lodash"
const mode = process.env.NODE_ENV ? 'production' : 'development';
const values = process.env.NODE_ENV ? production : development;
export const environment = merge(values, { mode });
development.js
import dotenv from 'dotenv';
dotenv.config();
export const development = {
port: 8080,
db: {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
name: process.env.DB_NAME
}
};
production.js
import dotenv from 'dotenv';
dotenv.config();
export const production = {
port: process.env.PORT,
newfromproduction: 'jkdl',
db: {
test: 'test'
}
};
file structure
src
-environment
- index.js
- development.js
- production.js
-graphql
-models
-utils
server.js
.babelrc
.env
.gitignore
package.json
I think you're on the right path. Abstracting away the environment-specific configuration is the way to go in my opinion.
Here are a couple of remarks to enhance your code:
dotenv
or even merge
from lodash
to make sure that your application code runs regardless of the environment it is in.environment/index.js
should have the same shape for all environments to avoid errors that occur only in one environment, which is not the case in the snippets you provided.NODE_ENV
(or whatever single environment variable name you want) and make sure that it is defined when you run your npm start
script.Here is the code of what I would recommend to do (where ALL_CAPS strings should be replaced by the actual values you need for your app to run in that environment):
development.json
{
"port": 8080,
"db": {
"username": "DEVELOPMENT_USERNAME",
"password": "DEVELOPMENT_PASSWORD",
"name": "DEVELOPMENT_DATABASE_NAME"
},
"newfromproduction": ""
}
production.json
{
"port": 8080,
"db": {
"username": "PRODUCTION_USERNAME",
"password": "PRODUCTION_PASSWORD",
"name": "PRODUCTION_DATABASE_NAME"
},
"newfromproduction": "jkdl"
}
environment/index.js
import development from './development.json';
import production from './production.json';
const { NODE_ENV: mode } = process.env;
const configuration = {
development,
production
};
// using a little bit of destructuring magic but that is not necessary
export default { ...configuration[mode], mode };
package.json
"scripts": {
"start": "NODE_ENV=production node server",
"start:dev": "NODE_ENV=development nodemon server"
}
And you can keep the code in server.js
the same.
A couple benefits for this approach:
development.json
and production.json
in your repository, so your password is safe from developers who don't need to know what it is. If a developer needs the config file to work on the app, simply share an encrypted version of development.json
with them. It's not ideal but at least your password is not stored in plaintext in GitHub.test
or stage
), you only need to create a JSON file in the environment
folder and add a script in package.json
(for example "start:stage": "NODE_ENV=stage node server"
or "test": "NODE_ENV=test mocha"
). No need to add if
statements in environment/index.js
.A small downside:
NODE_ENV
is a string for an environment that is not expected (such as NODE_ENV=abc123
) then the app will crash because configuration.abc123
is undefined
, but that is not necessarily a bad thing (having unexpected environments is not a good idea) and you can also ameliorate the code I provided to handle that case.