programming
Why Generated API Docs Suck
Generated documentation is often worse than no documentation at all. Let's discuss how to write docs that actually help.
February 2026
Over the years I’ve been exposed to a lot of code bases across many languages. And over time it has become painfully clear that
Generated API docs just suck. Not as a concept, but what most teams make out of them.
The core problem of documentation is that it needs to be kept up to date. Having it separate from the code makes it really hard to maintain. So the idea of keeping docs close to the code isn’t bad at all. But somewhere along the way, teams confused “having documentation” with “having useful documentation.”
The boilerplate epidemic
Here’s a Java snippet that should look familiar to anyone who has worked on a larger code base.
/**
* Construct an entry with only a name.
*
* @param name the entry name
*/
public FooEntry(String name) {
this.name = name;
}
/**
* Get this entry's name.
*
* @return This entry's name.
*/
public String getName() {
return name;
}
What’s the benefit here? Every comment just restates what the code already says. It’s noise dressed up as documentation.
This isn’t a Java problem though. TypeScript has the same disease.
/**
* Gets the user by ID.
* @param id - The user ID.
* @returns The user object.
*/
function getUser(id: string): User {
return users.get(id);
}
Go isn’t immune either.
// GetUser returns the user for the given id.
func GetUser(id string) *User {
return users[id]
}
And even Rust, despite having one of the best documentation cultures, still attracts this pattern.
/// Returns the name of the entry.
fn name(&self) -> &str {
&self.name
}
In all four cases the type system already tells you everything the comment does. The signature is the documentation. Adding a comment that just rephrases it creates maintenance burden with zero informational value.
Why do we keep doing this?
For one, there’s a good chance developers didn’t write much of these docs in the first place. IDEs generate them. Linters enforce them. Checkstyle rules fail the build if they’re missing. And now AI assistants happily generate /** Gets the name. @return the name */ for every getter without anyone asking.
The incentive is to satisfy the tooling, not the reader. That’s backwards.
When you write documentation for the linter instead of the human, the human loses.
What good documentation looks like
The best documentation tells a story. It explains the why, outlines the important parts of the how, and gives notice of anything that might surprise you. Method signatures already cover the what.
Here’s a better version of that Java class.
/**
* Represents a single entry in a Foo archive.
*
* Entries are created during archive construction and
* written sequentially. The header format follows the
* Foo specification v2.1, section 4.3.
*
* Instances are not thread-safe. If shared between threads,
* external synchronization is required.
*/
public class FooEntry implements ArchiveEntry {
private String name = "";
public FooEntry(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void writeEntryHeader(OutputStream out)
throws IOException {
out.write(1);
out.write(2);
}
}
The class-level comment does the heavy lifting. It explains what this thing is, where it fits in the bigger picture, and what you need to watch out for. The individual methods don’t need comments because their names and signatures are self-explanatory.
Focus on self-explanatory class, function and variable names. Often that’s even better than comments because the explanation is visible not only at declaration time but with every use. That’s the path to code as documentation. A lot of comments then become duplication and can be removed. Less visual clutter, more signal.
Languages that got it right
Some ecosystems have found clever ways to make documentation both useful and trustworthy.
Rust’s doc-tests let you write examples in documentation comments that the compiler actually runs as tests. If the example breaks, the build breaks. This means documentation stays in sync with the code by force, not by discipline.
Go’s testable examples follow a similar philosophy. You write functions named ExampleFoo in test files. They show up in the generated docs and run as part of the test suite. The output is verified against a comment.
Elixir’s doctests take this even further. Examples in @doc attributes are executed as part of the test suite. The documentation is the test. It’s the closest thing to documentation that can’t go stale.
Python’s doctest module has been doing this since 1999. Interactive examples in docstrings get extracted and verified. The idea is old but the principle is timeless: documentation that gets tested stays correct.
What all of these have in common is that documentation must be verified, not just present.
Documentation needs structure
The Divio documentation system makes a useful distinction between four types of documentation: tutorials, how-to guides, reference, and explanation. Most auto-generated docs only cover reference. That’s the least useful type when you’re trying to understand a new codebase.
What developers actually need when they encounter unfamiliar code is explanation and context. Why does this module exist? How does it fit into the architecture? What are the trade-offs?
Projects like Django and Stripe are known for documentation that reads like a guided tour, not a phone book. Elixir’s ecosystem has built this into its culture. Modules in Phoenix often start with pages of discussion about what something is, how to use it, and what to watch out for. The API reference comes after.
AI won’t save us
AI coding assistants can generate documentation at scale. But they tend to produce exactly the kind of boilerplate this article argues against. They’re very good at restating what code does. They’re not good at explaining why it exists or how it fits into the bigger picture.
That said, AI can help with reading and understanding undocumented code. It can summarize, explain patterns, and answer questions about a codebase in ways that static docs never could. But even AI needs to “understand” the code the same way a human does. If there’s no narrative structure, no explanation of intent, the AI is guessing just like the next developer would be.
Write for the reader
First and foremost, empathize with the person reading and trying to understand your code.
If you enforce documentation through linting, do it only for public APIs and focus on class-level or module-level docs. Nobody benefits from a mandatory @param name the name on every setter.
Write documentation at the highest useful level. Explain the module, not the getter. Tell the story of why this code exists. If you want to enforce documentation quality, verify your examples compile and run. Rust, Go, Elixir and Python have shown that this works.
The best documentation isn’t the most complete. It’s the most useful.