Code Buckets

Buckets of code

Node React

Changing configuration when deploying to different environments: comparing Node and React

React and Node at the front, perhaps with .Net and Python in the background

I’m getting into Node a bit more and I wanted to look at how you would swap config settings when deploying into different test and production environments. In this post I’m going to compare Node to React – not so much comparing apples with oranges, more like comparing oranges to tangerines.

Requirements

The requirements are

  1. I want to change config settings for a Node and a React application depending on the environment it is deployed to.
  2. I might have multiple environments – more than dev, test and production
  3. I want it to run with the npm start command or similar. Basically, it’s got to be easy to run in development and easy to deploy
  4. I want it to work on Linux, Windows and MacOS with a minimum of fuss.

Overall principal

I’m going to use environment variables to indicate which environment we are targeting. Different config will be served depending on the controlling environmental variable.

The environments we are targeting are

  1. Development
  2. QA
  3. Regression
  4. Production

We could have a lot more – as many as we wanted or as many as the test team demand.

Node

The node app is as simple as I can get it.

const express = require('express');
const cors = require('cors');
const {hostname, port, message} = require('./config');

const server = express();
server.use(cors());

server.get('/', (req, res) => {

    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/json')
    res.end(message);
});

server.listen(port, hostname, () => {
    console.log(`server running at ${hostname}:${port}`);
});

It is going to send a string of when we browse to the route url which indicates the current environment i.e.

Only thing to note here is that I’m using a very loose Cross-origin Resource Sharing constraint (let everything in). I need that Cors definition as this will be hooked to the React app that is coming later which runs on a separate port. It’s very loose because it’s only a demo and I want everything simple.

Environment variable

We will store the current environment in the standard environment variable NODE_ENV. This will control which config will be served.

Config

The config is

const development_env = "development";
const qa_env = "qa";
const regression_env = "regression";
const production_env = "production";
const current_env = process.env.NODE_ENV;

const environments = 
[
    {
        env: development_env,
        hostname: "127.0.0.1",
        port: "3010",
        message: 'node is development'
    },
    {
        env: qa_env,
        hostname: "127.0.0.1",
        port: "3011",
        message: 'node is qa'
    },
    {
        env: regression_env,
        hostname: "127.0.0.1",
        port: "3012",
        message: 'node is regression'
    },
    {
        env: production_env,
        hostname: "127.0.0.1",
        port: "3013",
        message: 'node is production'
    }
];

const config = environments.find(e => e.env === current_env) || environments.find(e => e.env === production_env);

module.exports = config;

Each config contains

  • An environmental specific message
  • The port the app runs on
  • The host the app runs on

There is config for all 4 environments. This code at the end is the important bit.

const config = environments.find(e => e.env === current_env) || environments.find(e => e.env === production_env);

It picks out the correct config based on the NODE_ENV environment variable and exports it. If there is no match, then the production config is exported – a sensible precaution, I think.

Npm

This is all glued together in package.config in the scripts section

 "start": "cross-env NODE_ENV=development nodemon app.js",
 "start-qa": "cross-env NODE_ENV=qa node app.js",
 "start-regression": "cross-env NODE_ENV=regression node app.js",
 "start-production": "cross-env NODE_ENV=production node app.js",

Each script will set up the correct environment variable then start the app. So if we type

npm run start-regression

into a terminal then the environmental variable NODE_ENV is set to regression and the app starts. When I browse to the endpoint

http://127.0.0.1:3012

I see the message ‘node is regression’ in the browser.

The development environment is a slight outlier here. It runs nodemon rather than node as I want hot reloading for my development environment. Other than that it works in the same way as the rest of the environments.

Cross-env

Setting environmental variables through a terminal differs depending on what environment you are working on

For Linux it is

NODE_ENV=regression

But windows it is

set NODE_ENV=regression

This prevents my solution being environment agnostic. To circumvent this I’m using the npm package cross-env which standardizes this across all environments to

cross-env NODE_ENV=regression

Therefore my application will run on windows, Linux, macOS and so forth which is what I want.

React

The React app is created using create-react-app which constrains the implementation a bit– but the solution is generic enough to be useful, even if you are using another package to create the app or are starting from scratch.

The react app is written in TypeScript unlike the node app which was straight JavaScript.

App

The app is still simple but a bit more complex than last time

import React, {useState, useEffect} from 'react';
import axios from 'axios'
import config from './config';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css'
import './App.css';

function App() {

  const [message, setMessage] = useState('pending');

  useEffect(() => {

    const callServer = async () => {

      try{
        let response: any = await axios.get(config.api);
        setMessage(response.data);
      }
      catch {
        setMessage('unavailable');
      }
    }
    callServer();
  }, []);

  return (
    <div className="App container p-4">
      <h2>Deployment Demo</h2>
      <h5 className="mt-4">Client</h5>
      <div>client env variable: {process.env.REACT_APP_CLIENT_ENV}</div>
      <div>client config: {JSON.stringify(config)}</div>
      <h5 className="mt-4">Server</h5>
      <div>{message}</div>
    </div>
  );
}

export default App;

It will display the client config and call the corresponding node environment endpoint and display the environment specific message from the server as below.

The UI is startlingly basic even for me – a new low I’m afraid there. But it does the job.

Environmental variable

This time we aren’t using NODE_ENV. Create-react-app prevents us overriding it and sets it to development, test or production depending on how you run the application. Being limited to just three environments is insufficient for us – or for any enterprise level solution really. Therefore, we are going to use the custom environmental variable REACT_APP_CLIENT_ENV.

Config

interface IConfig 
{
    env:string,
    api:string
}

const development_env:string = "development";
const qa_env:string = "qa";
const regression_env:string = "regression";
const production_env:string = "production";
const current_env:string | undefined = process.env.REACT_APP_CLIENT_ENV;

const environments: Array<IConfig> = 
[
    {
        env: development_env,
        api : "http://127.0.0.1:3010"
    },
    {
        env: qa_env,
        api : "http://127.0.0.1:3011"
    },
    {
        env: regression_env,
        api : "http://127.0.0.1:3012"
    },
    {
        env: production_env,
        api : "http://127.0.0.1:3013"
    }
];

const config: IConfig = environments.find(e => e.env === current_env) || environments.find(e => e.env === production_env) as IConfig;

export default config;

The config is very similar to node except it’s written it TypeScript, so it’s got a defining interface. Like the node config, it will return the config that matches the environmental variable REACT_APP_CLIENT_ENV. if the client can’t find a config which matches the REACT_APP_CLIENT_ENV, then it will return the production config.

Npm

Again, this is all glued together in package.config in the scripts section

    "start": "cross-env REACT_APP_CLIENT_ENV=development react-scripts start",
    "build": "cross-env REACT_APP_CLIENT_ENV=development react-scripts build",
    "build-qa": "cross-env REACT_APP_CLIENT_ENV=qa react-scripts build",
    "build-regression": "cross-env REACT_APP_CLIENT_ENV=regression react-scripts build",
    "build-prod": "cross-env REACT_APP_CLIENT_ENV=production react-scripts build",

This time though, it is the build command not the start command where we need to set the environmental variable. Ultimately the react app is just static html and JavaScript with no knowledge of what REACT_APP_CLIENT_ENV currently set to. The environmental variable REACT_APP_CLIENT_ENV gets written into the static html output on build and that is how we can choose the correct config.

For example, to run regression for our react app we build the app with this command

npm run build-regression

then run it on a server with this

serve -s build

And now the correct value is embedded in the static JavaScript. We do still need a start method, but this is just used for development purposes and we don’t need multiple start methods like we did with node.

Limitations

Because we are using environmental variables this presupposes that you have one test instance per environment. You couldn’t host QA and regression on the same box for instance. Using Docker would be a way to go to ensure this wasn’t an issue.

If you are on-premises and you must share environments then I could envisage a solution that uses an json/xml to store the environment definition within the application or store it in the registry. Or of course roll your own solution.

Code

As ever the demo code is on my Git Hub site.

The Node app is at

https://github.com/timbrownls20/Demo/tree/master/Node/deployment-demo

and the React app is at

https://github.com/timbrownls20/Demo/tree/master/React/deployment-demo

Useful links

The npm package for cross-env is at

https://www.npmjs.com/package/cross-env

It’s in maintenance mode but still works well.

A complete discussion of how create react app deals with environmental variables is at

https://create-react-app.dev/docs/adding-custom-environment-variables/

There are more options than what I outlined in this article.

LEAVE A RESPONSE

Your email address will not be published. Required fields are marked *