Search code examples
node.jsexpressstructurebackendapollo-server

What is the best way of separating "dev" and "prod" modes in the app?


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


Solution

  • 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:

    • I don't think you necessarily need dotenv or even merge from lodash to make sure that your application code runs regardless of the environment it is in.
    • The object you export from 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.
    • I would suggest using JSON instead of JS for your config, but that's just a preference.
    • I would advise to use as little environment variables as possible. You can simply rely on 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:

    • You don't have to commit 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.
    • If you need to add other environments (such as 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:

    • If the value of 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.