A Field Guide to Useful NPM Package READMEs
A README that shows installation and nothing else forces users to read source code. Here's what a README needs to actually do its job.
A README has one job: help a developer decide whether to use the package, then help them use it without reading source code. Most READMEs fail the second part. They cover installation and the happy path, then leave the developer to figure out TypeScript types, error states, edge cases, and configuration options by digging through index.d.ts or the source. That's not documentation — that's treasure hunting. Here's what a README actually needs to do its job.
The minimum viable README
Before anything else, a README needs three things in this order:
One sentence that says what the package does. Not what problem it solves at a philosophical level — what it does, mechanically. "A zero-dependency React hook that manages async state with loading, error, and success phases" is useful. "The simplest way to handle async in React" is not, because it tells the reader nothing they can verify.
Installation. Copy-pasteable. If there are peer dependencies, list them with version ranges. A missing peer dependency error is the fastest way to lose a developer before they've seen a single line of your API.
The most common usage example. Not a toy example with console.log — a real one with the types shown, the props named, and a real use case. If the package is a React hook, show it in a component that looks like the component a developer would actually write.
That's the floor. If your README has those three things and nothing else, it's better than half the packages on npm. But a README that only has those three things is written for discovery, not integration — and most developers are reading it at integration time.
What makes READMEs fail at integration time
Only the happy path. The example works. The developer copies it. Then they hit a case the example didn't cover — an error state, an empty state, a loading state — and there's nothing in the README to help them. They open GitHub, search the issues, find a three-year-old thread, and cobble together a solution. Your README just cost them 20 minutes.
No TypeScript types shown. If your package has types, show them in the examples. Show what the return type of the hook is. Show the shape of the options object. A developer reading your README shouldn't have to hover over an import in their editor to know what they're working with.
No failure states documented. What does the error state look like? What does the function throw, or reject with, if something goes wrong? What are the validation rules on the props? "It throws an error if X" is documentation. "An error occurred" in the source is not.
No "when NOT to use this" section. This one is rare and therefore valuable. If your package is a dropdown component that doesn't support async search, say so. If your state management hook doesn't support time-travel debugging, say so. A developer who reads "this won't work if you need X" and closes the tab is not a lost user — they're a user you helped avoid a bad fit. They'll remember that.
What great READMEs include
A props table with types. Not prose descriptions — a table. Name, type, default value, required/optional, description. One row per prop. For a React component this should be machine-readable enough that a developer can scan it in 30 seconds and know whether their use case is covered.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | The current value (controlled) |
onChange | (value: string) => void | — | Called when value changes |
placeholder | string | "" | Shown when value is empty |
disabled | boolean | false | Disables interaction |
Multiple examples that cover real variation. The first example is the common case. The second example shows a more complex case — controlled vs. uncontrolled, with error handling, with custom configuration. The third example, if it exists, shows an edge case or an advanced usage. Three examples is usually the right number. One is never enough. Ten means you need documentation, not a README.
Known limitations. Every package has them. Documenting them honestly is a form of trust-building. A developer who discovers a limitation by running into it in production has a much worse experience than one who read about it in the README and planned accordingly.
Migration guide if the API has changed. If you've shipped a breaking change, the README is the first place developers look when their upgrade fails. A ## Migration from v1 to v2 section with before/after examples is the difference between a developer who upgrades confidently and one who pins your package at the old version forever.
Discovery vs. integration: two different readers
A developer finding your package via npm search or a blog post is reading for discovery: should I use this? They want the one-line description, a quick usage example, and the package size. They spend 90 seconds on your README before deciding.
A developer integrating your package into a production codebase is reading for integration: how does this work in my specific situation? They have a real component, real constraints, and a specific question — how do I configure X, what happens if Y, does this support Z? They'll spend 10 minutes on your README and leave frustrated if they can't find answers.
Most READMEs are written for discovery and used for integration. The mismatch is where the time goes.
The fix is to write the discovery section (description, install, quick example) first, then treat the rest of the README as integration documentation. Ask yourself: what questions will a developer have after they've pasted the first example into their codebase? Answer those questions in order of likelihood.
A concrete before and after
Here's a real failure pattern — an "error handling" section that documents nothing:
## Error Handling
The hook handles errors automatically. Check the `error` property.
And a version that actually helps:
## Error handling
If the async function rejects, `status` is set to `"error"` and `error` holds
the rejection value. The hook does not catch or transform errors — whatever
your async function throws is what you get in `error`.
\`\`\`tsx
function UserProfile({ id }: { id: string }) {
const { data, status, error } = useAsync(() => fetchUser(id));
if (status === "loading") return <Spinner />;
if (status === "error") {
return <ErrorMessage message={error instanceof Error ? error.message : "Unknown error"} />;
}
return <Profile user={data} />;
}
\`\`\`
If you need to retry on error, call the returned `execute` function again.
Re-mounting the component also triggers a fresh fetch.
The difference is specificity. The bad version is technically true. The good version is actually useful.
The README is part of the API
A package's README is not an afterthought — it's part of the API surface. A developer's first interaction with your package is reading it. A bad README creates friction that no amount of good code recovers from, because most developers won't get far enough to find out how good the code is.
Write the README before you're done building. Write it as if you're the developer who just found the package and needs to integrate it by end of day. Answer the questions they'll have, in the order they'll have them. Show the types. Document the failure states. Say what the package doesn't do.
That's the whole craft.