Guided Walkthrough — Reading Config, Generating Data, Writing Output

12 min read

This is the build lesson. Type along — every snippet is a real piece of the working tool, not pseudocode. By the end you'll have qa-datagen running, generating ten users with one command. Open the qa-datagen folder you scaffolded in lesson 1 and let's start.

Step 1 — Parse the command-line arguments

Node.js exposes the command-line arguments as process.argv — an array. The first two entries are always Node and the script path; everything after is what the user typed.

node qa-datagen.js --type users --count 10

…produces:

[
  "/usr/bin/node",
  "/path/to/qa-datagen.js",
  "--type", "users",
  "--count", "10"
]

You only care about the entries from index 2 onwards. A small parser turns the flag-and-value pairs into an object:

function parseArgs(argv) {
  const args = {};
  for (let i = 2; i < argv.length; i += 2) {
    const key = argv[i].replace(/^--/, "");
    args[key] = argv[i + 1];
  }
  return args;
}

For our minimal tool that pattern is enough. Real CLIs use libraries like yargs or commander; we're keeping it dependency-free.

Validate and apply defaults:

function readOptions(argv) {
  const args = parseArgs(argv);
  if (!args.type) {
    throw new Error("Missing required argument: --type <users|products|orders>");
  }
  return {
    type:   args.type,
    count:  Number(args.count ?? 5),
    output: args.output ?? "output.json"
  };
}

Required arguments throw an explicit Error — the user gets a one-line message, not a 30-line stack trace. Optional arguments use the nullish coalescing operator (??) for defaults.

Drop both functions into qa-datagen.js and add a sanity-check at the bottom:

const opts = readOptions(process.argv);
console.log(opts);

Run node qa-datagen.js --type users --count 10. Output:

{ type: 'users', count: 10, output: 'output.json' }

That's step 1 done.

Step 2 — Read the config

Create config.json:

{
  "users": {
    "firstNames": ["Alice", "Bob", "Carol", "Dan", "Eve", "Frank", "Grace", "Heidi"],
    "lastNames":  ["Adams", "Baker", "Clark", "Doyle", "Evans"],
    "domains":    ["test.com", "example.org", "qa.codes"],
    "roles":      ["admin", "member", "guest"]
  }
}

(You'll add products and orders blocks in lesson 3's stretch goals.)

The loader is a small function with try/catch around the parse — exactly the pattern from chapter 7:

const fs = require("node:fs");
 
function loadConfig(path) {
  try {
    return JSON.parse(fs.readFileSync(path, "utf-8"));
  } catch (error) {
    throw new Error(`Could not load ${path}: ${error.message}`);
  }
}

If config.json doesn't exist or is malformed, the user sees a clear "Could not load …" message instead of a raw ENOENT or SyntaxError.

Step 3 — Build the user generator

Create generators/users.js. Two helpers and one main function:

function randomFrom(array) {
  return array[Math.floor(Math.random() * array.length)];
}
 
function generateEmail(first, last, domains) {
  const domain = randomFrom(domains);
  const user = `${first}.${last}`.toLowerCase();
  return `${user}@${domain}`;
}
 
function generateUser(config, index) {
  const first = randomFrom(config.firstNames);
  const last  = randomFrom(config.lastNames);
  return {
    id:        index + 1,
    firstName: first,
    lastName:  last,
    email:     generateEmail(first, last, config.domains),
    role:      randomFrom(config.roles),
    createdAt: new Date().toISOString()
  };
}
 
module.exports = { generateUser };

randomFrom picks one item from any array. generateEmail composes a plausible address. generateUser builds one user object. module.exports makes the function importable from the main script.

Generating many users is one line in the main script:

const users = Array.from({ length: opts.count }, (_, i) => generateUser(config.users, i));

Array.from({ length: N }, callback) is the idiomatic way to generate an array of N items — chapter 4 covered the array methods, and this is the bulk-generation cousin. The (_, i) ignores the first argument (which is undefined for sparse arrays from length) and uses the index to give each user a unique id.

Step 4 — Write the output

Outputs go to a file. The path might include a folder that doesn't exist yet, so create it first:

const path = require("node:path");
 
function writeOutput(filepath, data) {
  const dir = path.dirname(filepath);
  if (dir && dir !== ".") {
    fs.mkdirSync(dir, { recursive: true });
  }
  fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
}

path.dirname("output/users.json") returns "output". fs.mkdirSync(dir, { recursive: true }) creates it if missing and is a no-op if it exists. JSON.stringify(data, null, 2) produces pretty-printed JSON — friendly to diffs and to humans who'll open the file later.

Step 5 — Wire it all together

The main function ties the four steps into one flow, wrapped in a single try/catch:

const { generateUser } = require("./generators/users");
 
function main() {
  try {
    const opts = readOptions(process.argv);
    const config = loadConfig("config.json");
 
    const cfgForType = config[opts.type];
    if (!cfgForType) {
      throw new Error(`Unknown --type: ${opts.type}. Known: ${Object.keys(config).join(", ")}`);
    }
 
    let items;
    switch (opts.type) {
      case "users":
        items = Array.from({ length: opts.count }, (_, i) => generateUser(cfgForType, i));
        break;
      // case "products": ...  (add in lesson 3)
      // case "orders":   ...
      default:
        throw new Error(`No generator implemented for type: ${opts.type}`);
    }
 
    writeOutput(opts.output, items);
    console.log(`Generated ${items.length} ${opts.type} → ${opts.output}`);
  } catch (error) {
    console.error("qa-datagen:", error.message);
    process.exit(1);
  }
}
 
main();

The switch dispatches by --type (chapter 2). The try/catch swallows every error from any step into a single, clean failure path (chapter 7). process.exit(1) returns a non-zero status code so CI pipelines can detect failure — small but important.

Run it end to end

In your qa-datagen folder:

node qa-datagen.js --type users --count 3 --output output/users.json

Console output:

Generated 3 users → output/users.json

output/users.json content (yours will differ — random):

[
  {
    "id": 1,
    "firstName": "Alice",
    "lastName": "Doyle",
    "email": "alice.doyle@qa.codes",
    "role": "member",
    "createdAt": "2026-05-05T12:34:56.789Z"
  },
  {
    "id": 2,
    "firstName": "Frank",
    "lastName": "Adams",
    "email": "frank.adams@test.com",
    "role": "admin",
    "createdAt": "2026-05-05T12:34:56.790Z"
  },
  {
    "id": 3,
    "firstName": "Grace",
    "lastName": "Evans",
    "email": "grace.evans@example.org",
    "role": "guest",
    "createdAt": "2026-05-05T12:34:56.790Z"
  }
]

Real, varied, ready to load as a fixture. Try a few error cases too:

node qa-datagen.js                          # missing --type
node qa-datagen.js --type penguins          # unknown type
node qa-datagen.js --type users --count abc # count NaN

The first two should print clear messages and exit cleanly. The third probably succeeds with an empty array (because Number("abc") is NaN and Array.from({ length: NaN }) is []) — that's a known gap to fix in the next lesson's stretch goals.

The whole flow at a glance

Step 1 of 6

Parse CLI args

process.argv → readOptions → { type, count, output } with defaults applied

🎯 Project task

You've now seen every line. Type it in (don't copy-paste — typing reinforces) and verify each step:

  1. Add parseArgs and readOptions to qa-datagen.js. Test by printing opts. Confirm defaults apply when arguments are missing.
  2. Create config.json with the users block above. Add loadConfig and call it. Print the loaded object to confirm.
  3. Create generators/users.js with randomFrom, generateEmail, generateUser. Export generateUser. Test by calling it once from the main script and printing the result.
  4. Add writeOutput. Test by writing a fixed array [{ a: 1 }] to output/test.json and confirming the folder and file get created.
  5. Assemble main() with the switch dispatch and the try/catch. Run end to end and confirm the example output above.

Run the three error commands at the bottom of this lesson. The first two should produce clean errors; the third should silently produce an empty file — note that as a defect to fix in lesson 3.

You now have a working CLI tool. The next (and final) lesson walks through the self-assessment checklist, asks you the reflection questions, and lays out the stretch goals — products, orders, CSV output, deterministic seeds, validation.

// tip to track lessons you complete and pick up where you left off across devices.