Chapter 04 — One Thing Well

Text Streams and Universal Interfaces

Tenet three: write programs to handle text streams, because that is a universal interface. This one is about what the interface between programs actually looks like. The first two tenets told you to do one thing and to hand off to other programs. This one answers the obvious follow-up question: hand off how?

The answer was text. Plain, unformatted, human-readable text. Every Unix tool writes to standard output as a stream of characters. Every Unix tool reads from standard input as a stream of characters. No binary formats, no proprietary protocols, no shared memory. Characters. The interface is so simple it never has to change.

That’s why a tool written in 1972 can pipe output into a tool written in 2024. cat predates most of the tools it gets piped into by decades. It doesn’t matter. Text is text. The contract hasn’t moved.

JSON as the universal interface of the web

The web found its own answer to the same problem. When a browser needs data from a server, it doesn’t negotiate a protocol, agree on a binary format, or share any state. It sends an HTTP request and gets back text. Usually JSON.

JSON is text. It serializes into a string, travels over the wire as bytes, and gets parsed back into a structure on the other side. The producer doesn’t know what the consumer is. The consumer doesn’t know how the data was generated. They agree on a format and stay out of each other’s way. That’s McIlroy’s third tenet, restated for HTTP.

This is exactly cat file | grep pattern. cat doesn’t know grep will consume it. grep doesn’t know cat produced it. They share an interface. Here, the interface is JSON instead of newline-delimited text, but the principle is the same.

The interface is text. The consumer parses it.

js~/tutorials/
// The API returns text. This is the raw response:
// '{"posts":[{"id":1,"title":"Hello"},{"id":2,"title":"World"}]}'

// fetch() reads that text and parses it:
async function getPosts() {
  const res = await fetch('/api/posts');
  return res.json(); // text → object
}

The API could be written in Go, Ruby, Python, or anything else. The consumer doesn’t care. It reads JSON. The producer writes JSON. The format is the contract, and the contract is just text with agreed-upon structure.

HTML as a text stream

HTML is also text. Templates produce text. Build tools process text. Browsers receive text over the wire, parse it into a tree, and render from that. Every step in that chain handles the same thing: characters.

Templating systems, from PHP includes in 1997 to Astro components in 2023, work on the same premise: take a template (text), combine it with data, produce HTML (more text). The abstractions changed. The stream didn’t.

These things work because HTML never left the text stream. It’s not compiled into a binary. It’s not stored in a proprietary format. It travels as characters from the server to the browser, and the browser’s job is to read those characters.

That’s why Git diffs are text diffs. When you change a template, you can read the diff. When something breaks, you can inspect the source. You can grep an HTML file, pipe it through prettier, store it in a database field, version it, compare it, and restore it. The format never got in the way, because the format is just text.

What this means for version control

Don’t commit compiled output. Commit source.

Sass compiles to CSS. TypeScript compiles to JavaScript. The compiled output is a transformation of the source, not the source of truth. Git tracks text, and tracking generated text means tracking the wrong thing.

Compiled output directories like dist/ belong in .gitignore. Your Sass files, your TypeScript, your templates: those are what you commit. Those are the text your tools can meaningfully compare, merge, and restore.

When two developers change the same Sass variable and both compile, the compiled CSS differs because the source differed. If you only committed the CSS, you’d have a conflict with no useful context. The Sass source makes the conflict readable. You can see what actually changed.

The principle is the same one that makes Unix pipelines work: text is the source, transformations are steps, and the steps are not the thing. Don’t confuse your compiled artifacts for your source. One of them is a text stream you authored. The other is output.

Reference