Write the README first
Name
readme-driven — write the readme first
Synopsis
Usage examples are where developer experience gets decided, and now the agents read them too.
Filed
· 1,556 words · 14 min
Description
Before I write any code for a new library, I write the part of the README that shows how to use it.
Not the badges, not the install line, not the API reference. Just the usage block: a few imports, a few calls, and a comment after each one that shows what comes back. If that block reads well, I build the thing. If it does not, I have saved myself a week.
This is not my idea. It is older than most of my packages, and I think it matters more today than it did when it was first written down.
#An old idea that few people kept
In August 2010, Tom Preston-Werner wrote a short post called Readme Driven Development. The advice fits in four words: "Write your Readme first." The argument fits in one line:
A perfect implementation of the wrong specification is worthless.
Everyone I know agrees with this. Almost nobody does it.
I think the reason is simple. Writing a README does not feel like work. There is no compiler, no test going green, no number that goes up. Code feels like progress, so we write code, and then at the end we write a README that describes whatever we happened to build. That README is a report, not a design. It explains the API. It never had a chance to change it.
#The usage block decides the DX
An API has two audiences: the person who writes it and the person who calls it. When you write the implementation first, you design for the first audience by accident. Names come from internal variables. Argument order follows the order you needed things inside the function. Options appear because a branch exists, not because a caller asked for one.
The usage block flips that. When I write find.up('package.json', { cwd }) before find.up exists, I am the caller. I have to pick a name I would want to type. I have to decide what comes back, and write it down in a //=> comment where I can look at it. I have to decide which argument is required and which one is an option, and I can see right away if the call needs a comment to make sense.
That //=> comment is the most important part. A call without its result is only half of a sentence. Most of the design mistakes I have caught early, I caught while I was writing down what a call should return and found that I did not know.
If the example reads badly, the API is bad. You do not need a framework or a review to find out. You only need to look at it.
#My order of work
For anything I publish, the order is:
- README usage. The happy path, with results. Ten to thirty lines.
- Types. A
.d.tsfile or the exported signatures, with JSDoc. Now the edge cases have to have names: what is optional, what can beundefined, what throws. - Tests. The usage block becomes the first tests, almost line for line. The types tell me which edge cases need tests of their own.
- Code. Last, and usually the fastest part.
Each step is cheaper to change than the next one. Renaming an option in a README is one edit. Renaming it in types touches a few signatures. Renaming it after tests and code exist is a refactor. Renaming it after people install it is a major version. I want to make my mistakes as far up that list as I can.
The steps also check each other. When the types disagree with the README, one of them is wrong, and it is almost always better to fix the code than the prose.
#Before and after: stop becomes last
Here is a real case where I did not follow my own order, and paid for it.
empathic has a walk.up function and a find.up function. They climb from a directory toward the root. Both take an option that says where to stop climbing. In 0.0.1 that option was called limit. In 0.0.2 I renamed it to stop. The JSDoc said it plainly:
/**
* The directory to stop at.
*
* > [NOTE]
* > This directory WILL NOT be included in the results.
*/
stop?: string;
The README said "Controlled via cwd and stop options," and it had no example of stop with a result next to it.
Nine months later, someone working to replace find-up with empathic in Storybook opened an issue. They wrote this example, and expected it to search up to and including the home directory:
find.up("package.json", { stop: "~" });
It did not, because stop was exclusive. The docs were correct. The name was wrong. And they found a worse case:
find.up("dev"); // finds "/dev"
find.up("dev", { stop: "/" }); // does not
To include a known last directory, a caller had to pass its parent, and to special-case the root:
find.up("dev", { stop: stop === "/" ? undefined : path.dirname(stop) });
Nobody should have to write that line. I wrote the fix the same day, and it shipped in empathic@2: replace stop with last, make it inclusive, and default it to "/".
find.up("package.json", { last: "~" }); // includes "~"
I am sure that if I had written the { stop: '~' } example with its //=> result before I wrote walk.ts, I would have seen the problem. The result would have looked wrong to me too. A note in the JSDoc cannot save a bad name. An example with a result can show you that the name is bad before anyone else has to tell you.
#Why agents raise the stakes
When I started doing this, the README had one kind of reader: a person who skimmed the usage block, copied the first example, and then changed it until it worked.
Now there is a second kind of reader, and it reads more of your docs than any person does. A coding agent reads your README, your types, and your JSDoc, or it reads whatever version of them was in its training data. Then it writes code that looks like your examples.
That has three results.
Examples get copied literally. A person treats an example as a hint. An agent treats it as a template. If your usage block uses a deprecated option, or skips error handling "for brevity", that is what shows up in thousands of codebases. A bad example now spreads faster than a bad implementation, because the implementation lives in one place and the examples get copied everywhere.
Old examples live a long time. When you change an API, the old README stays in the training data of models that people will use for years. The stop option is gone from empathic, but I expect to see it in generated code for a while. The best defense I have is the old one: get the names right before the first release, so there is no old version to remember.
The README is the spec now. When I hand a task to an agent, the README is the first thing I want it to read. If the usage block is precise, with real results in //=> comments, the agent has a spec it can check its work against. If the README is a vague report written after the fact, the agent fills the gaps with guesses.
I wrote more about how this changes the shape of an API, not only its docs, in Designing APIs for agents and humans.
#Examples become tests, and tests become guardrails
The usage block is not only a design document. It is the first test file.
When I go from README to tests, I copy the examples over almost unchanged and turn each //=> comment into an assertion. Now the README and the code cannot drift apart without a test failing, provided I keep the two in sync when either one changes. I do not have tooling that runs README code blocks directly. A small test file that mirrors them has worked well enough.
This matters more with agents in the loop. An agent that edits your library will run your tests. If the tests mirror the README, the agent cannot "fix" a bug by quietly changing the public API, because the examples that define that API will fail. The README sets the contract, the tests enforce it, and the agent works inside both.
#What this looks like on a page
A README written first tends to share a few traits. None of them are rules. They are what the process leaves behind.
- The usage block comes before the API reference, and it shows results.
- There are few options, because every option had to appear in an example and earn its line. (I wrote about why the budget matters in Small on purpose.)
- Names read well at the call site, not only in the source.
- A short "Why?" section explains what the library is for, written when that was still the only thing I knew.
Look at any of my READMEs and you can usually tell which ones I wrote first. They are the ones I rarely had to rewrite.
#Start with the caller
This is the whole idea: before you decide how something works, decide how it feels to use. Write the call. Write what it returns. Read it out loud. If you would not want to type it, nobody else will either, and now that includes the agents typing it for them.
Writing the README first is still the cheapest design review I know. It costs an hour. The mistakes it catches would cost a major version.
See also
api-design(7), documentation(7), agents(7), open-source(7), lukeed(1)