Putting it together: a validating CLI
You have all the pieces now. A schema that describes what valid data looks like. fs/promises to read and write files. Commander to expose the work as a real CLI. Time to compose them into one tool that does the whole job from end to end.
The pipeline
A bad input file goes in, the CLI validates every record, and a clean file comes out. Anything invalid is rejected with a clear error.
import fs from 'fs/promises';
import { z } from 'zod';
import { program } from 'commander';
// Declare the contract once
const BookSchema = z.object({
id: z.number().int().positive(),
title: z.string().min(1, 'Title cannot be empty'),
author: z.string().min(1, 'Author cannot be empty'),
pages: z.number().int().positive(),
published: z.string(),
});
const InventorySchema = z.array(BookSchema);
export async function processData(inputFile: string, outputFile: string) {
// 1. Read raw text
const rawData = await fs.readFile(inputFile, 'utf-8');
// 2. Parse to a JS value (throws if not valid JSON at all)
const parsedJson = JSON.parse(rawData);
// 3. Validate with the schema
const result = InventorySchema.safeParse(parsedJson);
if (!result.success) {
console.error('Validation failed:', result.error.format());
throw new Error('Invalid data format');
}
// 4. Write the clean data
await fs.writeFile(outputFile, JSON.stringify(result.data, null, 2), 'utf-8');
console.log('Success. Clean data written to ' + outputFile);
}
program
.name('book-parser')
.requiredOption('-i, --input <path>', 'input file')
.requiredOption('-o, --output <path>', 'output file')
.parse(process.argv);
const opts = program.opts();
processData(opts.input, opts.output).catch(() => process.exit(1));The full processData function. Read the comments first, then the code.
Read processData top to bottom: read, parse, validate, write. Each step is one line. The schema sits at the top so anyone can see the contract at a glance. The CLI block at the bottom wires it to the command line. The catch on the last line maps any failure to a non-zero exit code so CI can detect it. This shape (clear pipeline, contract at the top, CLI at the bottom) is the template for almost every small backend script you will ever write.
Mistakes catch almost everyone the first time. One: forgetting await on fs.readFile, which gives you a Promise instead of a string and JSON.parse blows up. Two: using parse instead of safeParse and getting a Zod error instead of your own. Both are easy to fix once you know the shape.
Quiz: Quiz
Loading practiceโฆ
Validation checklist: Run the full pipeline
Loading practiceโฆ
Files were the easy case. Real backends serve data over the network, not from disk. Next up you stand up a real HTTP server and see what every framework is doing underneath the hood.
Checkpoint: Absolute basics checkpoint
Loading practiceโฆ