logo

๐Ÿ”ง Make your Swagger configuration files dynamic using Node.js

I got the idea for this article after solving this problem for the team I was working with, for a client.

So, what’s the problem?

After wrapping up work on a Swagger configuration file, and successfully generating a client JavaScript library to consume the API, we ended up with the following problem: how do we make sure that we’re able to generate the client library and use the proper environment URL, so that we don’t end up calling UAT endpoints from production, or the other way around.

The requirements

At first, I thought about just having a bash script that would run this build, that would do some sed magic on the YAML file and then call swagger-codegen on the newly obtained configuration. But I didn’t wanted to maintain yet another bash script (YABS).

So I did what every Node.js developer would do: I built a CLI that does the environment variable replacement for me. I thought there should be a reliable and simple library, or set of libraries, that I could use to build a simple command-line interface.

So the final requirements for my CLI tool were as follows:

  1. Should be run from the command line
  2. Should take a YAML input file and should populate every environment variable name with the value provided in a config file
  3. Should output a new YAML config file and not overwrite the old one
  4. Should call swagger-codegen with the new config file and generate a client API

The last step was not necessary, since I now believe it would have been better to leave this to the end-user to decide, but hell, it’s better than not doing anything.

Technical choices

This was not the type of task that would make me have to go into a cave for two weeks and come out with a 30-page Word document with the conclusions of a technical spike, and a prototype.

If what I need is simple CLI tool that would take YAML as input do something with it and spit out YAML, back, then I need libraries to accomplish the following tasks:

  1. Command line arguments reading โ€” surely there must be a reliable CLI abstraction since there are more Node.js CLI tools than Java versions.
  2. YAML parsing and serialization โ€” pretty popular data format.
  3. Environment variable / config parsing โ€” DotEnv, that’s it!
  4. String interpolation or property replacement โ€” I was a bit unsure here.

I don’t mean to say that I did not think about my technical choices, but I built this project in about 30 minutes, testing included. I did, nevertheless, applied a simplified version the principles I outlined in my article, Top 5 things to consider when choosing a new technology.

So here are the libraries I used to build this small project.

CLI โ€” NPM

As I said, because there are so many CLI tools build with Node.js, I just took the first library that had a decent number of downloads, and that looked like it provides good abstractions for working with the command line.

This was the first thing that attracted me to this specific library. I had to write ver little code to make it work.

dotenv โ€” NPM

This was a no-brainer for me. I’ve been using dotenv since around mid-2015 and I absolutely cannot live without it if I’m doing any work that requires me to set even one environment variable โ€” sorry people who think that I should do it another way, it’s just like jQuery for environment variables.

js-yaml โ€” NPM

I pondered a bit on this one, because I had a hard time understanding its documentation on the first go. I was also talking to two team members and explaining to them what I was doing. I got the gist of it once I sat for 5 minutes in silence and read through the documentation, carefully. The main issue I had with it was related to the fact that I forgot to convert the JavaScript string after interpolating the env vars, back to a JavaScript object. So it errored all over the place until I figured out what I was doing wrong.

shellsubstitute โ€” NPM

Didn’t have to look a lot for this one since there aren’t a lot of modules on NPM that do this. While I’m writing this, I realise that I could have also used Lodash’s template function and it would have done the same thing, but I feel just fine with this technical decision.

In my case, it takes whatever it finds on process.env and pumps it into the JSON string that results from stringifying what js-yaml returns after parsing the YAML config file.

The end result is this script below. If you’d like to find out more about the script and go through a “code review” of it, feel free to also check out the video I uploaded last week, on YouTube โ€” ๐Ÿ”ง Make your Swagger configuration files dynamic using Node.js.

// create-swagger-client.js

#!/usr/bin/env node

const dotenv = require('dotenv').config({
  path: `${__dirname}/.swagger_vars.env`
});
const fs = require('fs');
const path = require('path');
const cli = require('cli');
const yaml = require('js-yaml');
const substitute = require('shellsubstitute');

const OUTPUT_FILE_WITH_ENV_VARS = 'swagger-with-vars.yml';

const options = cli.parse({
  file: ['f', 'The YAML definition file', 'file'],
  lang: ['l', 'The target language of the client library', 'string'],
  output: ['o', 'The output path for the client library', 'string']
});

cli.spinner('Working...');

const swaggerConfig = yaml.safeLoad(fs.readFileSync(path.join(__dirname, options.file), 'utf-8'));
const JSONconfigWithEnvVars = substitute(JSON.stringify(swaggerConfig, null, 2), process.env);
const swaggerConfigWithEnvVars = yaml.safeDump(JSON.parse(JSONconfigWithEnvVars));
fs.writeFileSync(`./${OUTPUT_FILE_WITH_ENV_VARS}`, swaggerConfigWithEnvVars);

const success = () => {
  cli.spinner('Working... done!', true);
  cli.ok(`Finished generating client library: ${options.output}`);
}

/**
 * This is the error callback. Whenever an error occurs, this function is called.
 * The reason success is called at the end is because the Java cli tool โ€” swagger-codegen โ€”
 * dumps everything to stderr which tricks cli.js into thinking the command has failed.
 *
 * @see https://github.com/node-js-libs/cli/blob/master/cli.js#L1018
 *
 * @param error
 * @param stdout
 */
const error = (error, stdout) => {
  if (error instanceof Error) {
    cli.spinner('Working... not...!', true);
    cli.fatal(error);
  }

  //! Called because Java!
  success();
}

cli.exec(`swagger-codegen generate -i ${OUTPUT_FILE_WITH_ENV_VARS} -l ${options.lang} -o ${options.output}`, success, error);

For this code to work, you also need package.json and the environment variables file. You can find everything on GitHub โ€” oprearocks/swagger-environment-variables.

If you end up using this, or if this article or the video helps you in any way, please let me know. Any feedback is appreciated.

Cheers!

Copyright (c) 2023 Adrian Oprea. All rights reserved.