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.jsonConsole 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 NaNThe 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:
- Add
parseArgsandreadOptionstoqa-datagen.js. Test by printingopts. Confirm defaults apply when arguments are missing. - Create
config.jsonwith theusersblock above. AddloadConfigand call it. Print the loaded object to confirm. - Create
generators/users.jswithrandomFrom,generateEmail,generateUser. ExportgenerateUser. Test by calling it once from the main script and printing the result. - Add
writeOutput. Test by writing a fixed array[{ a: 1 }]tooutput/test.jsonand confirming the folder and file get created. - Assemble
main()with theswitchdispatch and thetry/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.