Using INI Files in Your Node.js Applications for Health and Profit

ini files
Think about your worst nightmare becoming reality: you are running a high-flying startup and launching a multi-billion-dollar online business, and betting the farm on a Node.js ecosystem and JSON configuration files. Your sysadmin is asked to temporarily comment out a portion of a JSON configuration file. He should know better, but in the heat of the moment, he forgets that JSON does not support comments. The configuration file is changed and saved, and the entire system crashes and burns🔥, shaking the trust of customers and diverting their allegiance to other competitors. Bankruptcy ensues.

Or how about that internal company developer who builds her configuration files using pure JavaScript? She makes that one last change to the production JS configuration file late Friday afternoon. Unknowingly, she accidentally deletes the closing quotation mark when modifying a property. Her phone ☎️ rings Friday night with weekend workers who can't get to the internal website since it is unavailable. She suffers a heart attack on-the-spot, resulting in a hospital stay for three weeks.

Could these incidents have been avoided? Yes, if INI configuration files had been used in these scenarios, health and wealth would have been the theme instead of (hospital) beds and bankruptcy. Friends don't let friends build Node.js apps without INI files.😜

Some have called the INI file the wonder drug of our time. Invented back in the 80s, we are seeing a resurgence of this popular format that once graced the boot.ini file in Windows before the boot.ini file was eliminated with the dawn of Windows Vista.

Article contents

What is the INI file format?

INI files utilize an extremely simple format for configuration files consisting of sections, properties, and values. Let's consider a sample INI configuration file for retrieving weather data:

; Weather configuration data
degreeType=F

[cities]
San Diego, CA
Chicago, IL
Tokyo, Japan

[fields]
temperature=Temp
skytext=Text
windspeed=Wind Speed

Comments begin with a semicolon as shown in line 1.

In line 2, we declare a property and its associated value. I did say this format was simple, didn't I?

Section declarations are surrounded by square brackets as shown in line 4. Sections enable us to combine similar configuration items together in their own groups.

Why use INI files for configuration?

I hope the introduction above has thoroughly convinced you of the inherent dangers of not using INI files.😄 Seriously, I am very much an advocate of JS and JSON configuration files; however, I also believe there are contexts where INI files can be extremely valuable too. Let's consider an identical modeling of our weather INI configuration above using a JSON format:

{
  "degreeType": "F",
  "cities": ["San Diego, CA", "Chicago, IL", "Tokyo, Japan"],
  "fields": [
    {
      "temperature": "Temp"
    },
    {
      "skytext": "Text"
    },
    {
      "windspeed": "Wind Speed"
    }
  ]
}

What if we want to update the configuration file temporarily so we don't receive Chicago data? This poses a challenge since JSON does not support comments. It looks like we'll need to copy and paste "Chicago" and move it into a temporary file, or create duplicate configuration files.

We could also employ a pure JavaScript configuration format instead:

module.exports = {
  degreeType: 'F',
  cities: ['San Diego, CA', 'Chicago, IL', 'Tokyo, Japan'],
  fields: [
    {
      temperature: 'Temp'
    },
    {
      skytext: 'Text'
    },
    {
      windspeed: 'Wind Speed'
    }
  ]
};

The situation has not improved much in terms of commenting out a city in the array. How about adding a city? We run the risk of forgetting a comma or failing to include an opening or closing quotation mark. Has this ever happened to you as a JavaScript developer? It has certainly happened to me.😢

Furthermore, what if we must rely on others who are less intimate with JavaScript to update our configuration files? We run a high risk of corrupting the JS/JSON configuration syntax, so the configuration file cannot be parsed. Although corruption is still possible with INI files, the likelihood is greatly reduced!

Oh, and let's not kid ourselves thinking XML configuration files will come save the day either:

<weather>
    <degreeType>F</degreeType>
    <cities>
        <city>San Diego, CA</city>
        <city>Chicago, IL</city>
        <city>Tokyo, Japan</city>
    </cities>
    <fields>
        <field>
            <name>temperature</name>
            <colName>Temp</colName>
        </field>
        <field>
            <name>temperature</name>
            <colName>Temp</colName>
        </field>
        <field>
            <name>skytext</name>
            <colName>Text</colName>
        </field>
        <field>
            <name>windspeed</name>
            <colName>Wind Speed</colName>
        </field>
    </fields>
</weather>

The angle bracket tax is very evident here.

Let's look again at our simple INI file and bask in the beauty of its simple, malleable syntax where items can easily be commented out and added:

; Weather configuration data
degreeType=F

[cities]
San Diego, CA
Chicago, IL
Tokyo, Japan

[fields]
temperature=Temp
skytext=Text
windspeed=Wind Speed

There are definite benefits with the INI file format! Let's get started using the INI syntax and write some Node.js code.

Initial Setup

To get started, we'll install the excellent ini package created by Isaac Schlueter, the inventor and CEO of NPM.

First, create a default package.json file, if you don't already have one:

$ npm init -y

Next, install the INI package and save it as a dependency in the package.json file:

$ npm install --save ini

Creating and reading our first INI file

We're planning to retrieve data from a weather service and we'll start by creating a configuration file we can use to specify if we want the temperature in degrees Fahrenheit or degrees Celsius. Let's call it config.ini:

; Weather configuration data
degreeType=F

Our very basic configuration file includes a comment and a property for degreeType.

Let's read and parse the INI configuration file and retrieve the value of degreeType:

const fs = require('fs');
const ini = require('ini');

const config = ini.parse(fs.readFileSync('./config.ini', 'utf-8'));
const degreeType = config.degreeType;

console.log(degreeType);

We use the require function in lines 1-2 to import the fs and ini modules and make them available to our program.

In line 4, we read our INI file and parse it into an object named config.

Voilà! We now have a config object containing the information we stored in our configuration file. Retrieving the degreeType in line 5 is trivial.

Let's get the current weather!

Let's make it more interesting and use our INI configuration to retrieve the current weather. Here we go!

First, we'll get the weather-js module to help us:

$ npm install --save weather-js

Next, we'll create a file called weather.js to make our own weather module to use for the next exercises:

const weather = require('weather-js');

const getCurrent = (city, degreeType) =>
  new Promise((resolve, reject) => {
    weather.find({ search: city, degreeType }, (err, result) => {
      if (err) {
        reject(err);
      } else {
        resolve(result[0].current);
      }
    });
  });
module.exports = getCurrent;

Notice how we create a Promise to retrieve the weather and return a JavaScript object containing the current weather data for a given city.

Using our same INI config file, let's test our function and ensure it works:

const fs = require('fs');
const ini = require('ini');
const weather = require('./weather');

const city = 'San Diego, CA';

const config = ini.parse(fs.readFileSync('./config.ini', 'utf-8'));
const degreeType = config.degreeType;

console.log(degreeType);

weather(city, degreeType)
  .then(result => console.log(result))
  .catch(error => console.log(error));

We hardcode the city to retrieve (San Diego) in line 5 and then invoke our newly created weather function in line 12. The following JavaScript object is returned:

{ temperature: '67',
  skycode: '30',
  skytext: 'Partly Sunny',
  date: '2018-01-12',
  observationtime: '14:20:00',
  observationpoint: 'San Diego, CA',
  feelslike: '67',
  humidity: '66',
  winddisplay: '6 mph Northwest',
  day: 'Friday',
  shortday: 'Fri',
  windspeed: '6 mph',
  imageUrl: 'http://blob.weather.microsoft.com/static/weather4/en-us/law/30.gif' }

Very good - this gives us a clear picture of the various properties we can display with our results.

As a bonus exercise, change the degreeType in the configuration file to "C" and rerun the program. You should see the temperature in degrees Celsius this time around!

Retrieve INI section data

Let's expand our INI file to include a section for cities:

; Weather configuration data
degreeType=F

[cities]
San Diego, CA
Chicago, IL
Tokyo, Japan

We can now get the cities from the INI configuration:

const fs = require('fs');
const ini = require('ini');

const config = ini.parse(fs.readFileSync('./config.ini', 'utf-8'));

console.log(config.cities);

This program returns the following results:

{ 'San Diego, CA': true,
  'Chicago, IL': true,
  'Tokyo, Japan': true }

Very interesting! The INI file object returned is an object containing fields for each city under [cities], all set to a value of true.

We'd rather receive an array of cities back rather than an object. We can use the Object.keys method to accomplish this goal:

const cities = Object.keys(config.cities);

We'll revise our program to utilize the [cities] section data and return the weather for each city:

const fs = require('fs');
const ini = require('ini');
const weather = require('./weather');

const config = ini.parse(fs.readFileSync('./config.ini', 'utf-8'));
const degreeType = config.degreeType;

const cities = Object.keys(config.cities);

for (const city of cities) {
  weather(city, degreeType)
    .then(current => {
      const temperature = current.temperature;
      console.log(`${city}\t${temperature} deg${degreeType}`);
    })
    .catch(error => console.log(error));
}

The program output will look something like this:

Tokyo, Japan    36 degF
Chicago, IL     21 degF
San Diego, CA   66 degF

Perfect. Let's expand our INI configuration file to include a list of fields to return from the Weather service:

; Weather configuration data
degreeType=F

[cities]
San Diego, CA
Chicago, IL
Tokyo, Japan

[fields]
temperature
skytext
windspeed

We can then modify our code to retrieve those specific field values for each city:

const fs = require('fs');
const ini = require('ini');
const weather = require('./weather');

const config = ini.parse(fs.readFileSync('./config.ini', 'utf-8'));
const degreeType = config.degreeType;

const cities = Object.keys(config.cities);
const fields = Object.keys(config.fields);

for (const city of cities) {
  weather(city, degreeType)
    .then(current => {
      current.temperature += ` deg${degreeType}`;
      const data = fields.map(f => current[f]).join('\t');
      console.log(`${city}\t${data}`);
    })
    .catch(error => console.log(error));
}

Excellent! Our new output now looks like this:

Tokyo, Japan    36 degF 9 mph   Mostly Sunny
Chicago, IL     20 degF 22 mph  Light Snow
San Diego, CA   67 degF 8 mph   Partly Sunny

What if we don't want to see the Chicago data temporarily? We can simply update our INI file as follows:

; Weather configuration data
degreeType=F

[cities]
San Diego, CA
;Chicago, IL
Tokyo, Japan

[fields]
temperature
windspeed
skytext

Run the program again and you will no longer see the Chicago weather. That configuration file update was easy!

Retrieve fields and value from a section

We'd like to include custom heading titles for each of the weather fields returned from our cities. We'll start by updating the INI configuration file:

; Weather configuration data
degreeType=F

[cities]
San Diego, CA
Chicago, IL
Tokyo, Japan

[fields]
temperature=Temp
windspeed=Wind
skytext=Text

We are then able to use, for example, "Temp" as the column heading for the temperature field returned, etc. Here's how it's done:

const fs = require('fs');
const ini = require('ini');
const weather = require('./weather');

const config = ini.parse(fs.readFileSync('./config.ini', 'utf-8'));
const degreeType = config.degreeType;

const cities = Object.keys(config.cities);
const fields = Object.keys(config.fields);

const fieldHeadings = fields.map(f => config.fields[f]).join('\t');
console.log(`City\t\t${fieldHeadings}`);

for (const city of cities) {
  weather(city, degreeType)
    .then(current => {
      current.temperature += ` deg${degreeType}`;
      const data = fields.map(f => current[f]).join('\t');
      console.log(`${city}\t${data}`);
    })
    .catch(error => console.log(error));
}

We use the fields array returned in line 9 to accomplish the magic. And...here are the results returned (including our custom heading names):

City            Temp    Wind    Text
San Diego, CA   67 degF 8 mph   Partly Sunny
Chicago, IL     20 degF 22 mph  Light Snow
Tokyo, Japan    36 degF 9 mph   Mostly Sunny

Write data to an INI file

While arguably not as useful, we are also able to write configuration data back to an INI file using the INI stringify method. Let's give it a try:

const fs = require('fs');
const ini = require('ini');
const weather = require('./weather');

const city = 'San Diego, CA';
const configOut = './weather-data.ini';

const config = ini.parse(fs.readFileSync('./config.ini', 'utf-8'));
const degreeType = config.degreeType;

weather(city, degreeType)
  .then(current => {
    const iniText = ini.stringify(current, { section: city });
    fs.writeFileSync(configOut, iniText);
    console.log(`Data written to ${configOut}`);
  })
  .catch(error => console.log(error));

We call the stringify method in the highlighted line above to marshal the current weather data into an INI format for San Diego. The second argument of the stringify function enables us to specify the name of the section in the INI file which will render as [San Diego, CA], in our case. The resulting INI file looks like this:

[San Diego, CA]
temperature=67
skycode=21
skytext=Partly Sunny
date=2018-01-13
observationtime=15:20:00
observationpoint=San Diego, CA
feelslike=67
humidity=67
winddisplay=8 mph Northwest
day=Saturday
shortday=Sat
windspeed=8 mph
imageUrl=http://blob.weather.microsoft.com/static/weather4/en-us/law/21.gif

It works!

Using arrays in INI files

To complete our tutorial, let's consider the different options we have available for modeling arrays in our INI configuration files. We'll use an array of baseball players from the days of yore to help us.

Method 1: Object.keys

Given this INI file:

[players]
Reggie Jackson
Joe Rudi
Sal Bando

We can utilize the Object.keys method we have already been using to retrieve a JavaScript array as follows:

const fs = require('fs');
const ini = require('ini');

const config = ini.parse(fs.readFileSync('./config.ini', 'utf-8'))

// Retrieve the array
const players = Object.keys(config.players);

console.log(players);

There are no real surprises here.

Method 2: Use the INI file array syntax

We can also use a specific INI array syntax to accomplish the goal by creating an INI file with these contents:

[team]
players[]=Reggie Jackson
players[]=Joe Rudi
players[]=Sal Bando

We are then able to retrieve the values from the players array with the following code:

const fs = require('fs');
const ini = require('ini');

const config = ini.parse(fs.readFileSync('./config.ini', 'utf-8'));

const players = config.team.players;

console.log(players);

Once again, an array is returned. As the Perl hackers always say: TMTOWTD (There's More Than One Way to Do It). 😉

Bonus - Modeling more complex objects

We can get creative and model even more complex objects with the INI configuration syntax. We might quickly reach a point where it makes more sense to use JSON/JS, but let's keep having fun with INI files. 😎

Let's retrieve some additional information about each of our players using the vertical bar (|) character as a field delimiter.

[players]
;Name             Position         Batting Avg
Reggie Jackson  | Right Field   |         .253
Joe Rudi        | First Base    |         .278
Sal Bando       | Third Base    |         .230

This INI file looks very readable and clean. Let's parse it and create a JavaScript object:

const fs = require('fs');
const ini = require('ini');

const config = ini.parse(fs.readFileSync('./config.ini', 'utf-8'));
const playersRaw = Object.keys(config.players);

function splitPlayer(player) {
  return player.split('|').map(i => i.trim());
}

const players = playersRaw.map(p => splitPlayer(p));

console.log(players);

We utilize our typical Object.keys method in line 5 to retrieve an array of each player.

We create a function in line 7 to split each line using the | character and trim the empty space. This function is called as we iterate through the array of players in line 11.

The resulting output looks like this:

[ [ 'Reggie Jackson', 'Right Field', '.253' ],
  [ 'Joe Rudi', 'First Base', '.278' ],
  [ 'Sal Bando', 'Third Base', '.230' ] ]

If we want to be even more concise, we can rewrite the code as follows and eliminate the splitPlayers function altogether:

const fs = require('fs');
const ini = require('ini');

const config = ini.parse(fs.readFileSync('./config.ini', 'utf-8'));
const playersRaw = Object.keys(config.players);

const players = playersRaw.map(p => p.split('|').map(i => i.trim()));

console.log(players);

INI files can be very versatile with a little bit of coaxing!

Conclusion

INI files provide a very simple and versatile format, and you are now equipped! Your call of action is to go find some contexts to use the INI file format in your Node.js applications. Health and profit awaits.😀

Additional articles

Guide to Using Redis with Node.js
Getting Started with YAML in Node.js
Making Interactive Node.js Console Apps That Listen for Keypress Events

Last updated Jan 23 2018

Follow @thisDaveJ (Dave Johnson) on Twitter to stay up to date with the latest tutorials and tech articles.

Share