How to Write Documentation That Developers Actually Read
The Slack message arrived at 3pm: "How do I authenticate API requests? Can't find it in the docs."
I stared at the message in disbelief. Authentication was documented. Extensively. I'd written 2,500 words explaining OAuth flows, API keys, token refresh cycles, and security best practices.
It was on page 4 of the documentation, following the architecture overview and philosophical introduction I'd carefully crafted.
No one had read page 4.
Analytics confirmed it: 87% of developers never scrolled past the first page. Of those who did, median time on page was 45 seconds—nowhere near enough to absorb 2,500 words of authentication concepts.
I'd optimised for comprehensiveness. Developers optimised for "show me the code and get out of my way."
My documentation was technically accurate, beautifully written, and functionally useless.
Here's what I learned about writing documentation developers actually read—and more importantly, actually use.
Why Most Documentation Fails (From a Developer's Perspective)
I interviewed 50 developers about documentation experiences. Common themes:
Problem 1: "I Don't Have Time to Read a Novel"
Developers arrive at documentation with a specific problem:
- "How do I authenticate?"
- "How do I paginate results?"
- "What's the rate limit?"
They want the answer in 30 seconds, not a philosophical journey through your API's design decisions.
What bad docs do: Bury the answer in paragraphs of context.
What good docs do: Answer the question in the first 10 words, then provide optional depth.
Problem 2: "Show Me the Code, Not Concepts"
Developers think in code. Prose explanations require translation. Code examples are immediately applicable.
Bad documentation:
"To authenticate, you'll need to obtain an OAuth 2.0 access token through the authorisation flow. This involves redirecting users to our auth endpoint with appropriate scope parameters, then exchanging the resulting authorisation code for an access token via a POST request to our token endpoint."
Good documentation:
// Get access token
const response = await fetch('https://api.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code: authCode,
client_id: 'your_client_id',
client_secret: 'your_client_secret'
})
});
const { access_token } = await response.json();
The second version is immediately copy-pasteable. The first requires mental translation.
Problem 3: "I Don't Trust It's Current"
Documentation with no timestamps, no version numbers, and no changelog feels untrustworthy.
Is this still accurate? Has the API changed since this was written? Am I implementing deprecated patterns?
Missing trust signals:
- No "last updated" date
- No version specificity ("This guide covers v2.1")
- No deprecation warnings
- Examples with outdated syntax
Result: Developers abandon docs and reverse-engineer from API responses or ask colleagues.
Problem 4: "Examples Don't Match My Use Case"
Every example shows the happy path. No examples show:
- Error handling
- Edge cases
- Pagination
- Rate limit handling
- Real-world complexity
Bad example:
# Create a user
user = api.create_user("john@example.com")
Good example:
# Create a user with error handling
try:
user = api.create_user("john@example.com")
except RateLimitError:
# Wait and retry
time.sleep(60)
user = api.create_user("john@example.com")
except ValidationError as e:
# Handle invalid email
print(f"Invalid email: {e.message}")
Real code handles failures. Examples should too.
Problem 5: "Navigation Is Impossible"
Landing page has 40 links. No clear starting point. No indication of which path matches my use case.
I need quickstart. I'm seeing reference documentation for advanced edge cases.
Result: Cognitive overload → close tab → ask in Slack.
The Documentation Hierarchy: What to Write First
Not all documentation is equally important. Write in this order:
Tier 1: Quickstart (First 5 Minutes)
Purpose: Get developer from zero to working example in <5 minutes.
Format:
- One-sentence description of what your product does
- Prerequisites (what they need installed)
- Installation (one command)
- Minimal working example (5-10 lines of code)
- Expected output (what success looks like)
- Next steps (links to common use cases)
Example structure:
# Quickstart: Your First API Call
Acme API lets you manage user data programmatically.
## Prerequisites
- Node.js 18+
- API key (get one at dashboard.acme.com)
## Install
npm install @acme/api
## Example
const AcmeAPI = require('@acme/api');
const api = new AcmeAPI('your_api_key');
async function main() {
const user = await api.users.create({
email: 'test@example.com',
name: 'Test User'
});
console.log('Created user:', user.id);
}
main();
## Expected Output
Created user: usr_abc123
## Next Steps
- [Authentication methods](./authentication.md)
- [User management guide](./users.md)
- [Error handling](./errors.md)
Why this works:
- Developer has working code in 5 minutes
- Builds confidence ("I can use this")
- Creates momentum to explore further
This is your most important documentation. If quickstart fails, nothing else matters.
Tier 2: Common Use Cases (First 30 Minutes)
After quickstart, developers need guidance for actual work:
- "How do I authenticate production users?" (not just test example)
- "How do I handle pagination?"
- "How do I upload files?"
- "How do I search/filter results?"
Format: Task-oriented guides with complete code examples.
Structure:
# How to Paginate API Results
## Problem
The `/users` endpoint returns max 100 results. You need to fetch all users.
## Solution
const allUsers = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await api.users.list({
page,
per_page: 100
});
allUsers.push(...response.data);
hasMore = response.has_more;
page++;
// Respect rate limits
await sleep(1000);
}
console.log(`Fetched ${allUsers.length} total users`);
## Explanation
- `has_more`: Boolean indicating if more results exist
- `page`: Starts at 1, increment for each request
- Rate limiting: 1 request/second prevents hitting limits
Why this works:
- Matches actual developer intent ("I need to paginate")
- Complete working code (copy-paste friendly)
- Explains the important bits without over-explaining
Tier 3: Reference Documentation (Ongoing Use)
Comprehensive listing of every endpoint, parameter, and response field.
This is important but not urgent. Developers reach for reference docs after they understand basics.
Format: Structured, consistent, searchable.
Example:
## POST /users
Create a new user.
### Request
#### Headers
- `Authorization`: Bearer {access_token} (required)
- `Content-Type`: application/json
#### Body Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| email | string | yes | User's email address. Must be unique. |
| name | string | yes | User's full name. Max 100 characters. |
| role | enum | no | User role. Options: admin, member, viewer. Default: member. |
### Response
#### Success (201 Created)
{
"id": "usr_abc123",
"email": "test@example.com",
"name": "Test User",
"role": "member",
"created_at": "2024-10-05T14:23:00Z"
}
#### Errors
- `400 Bad Request`: Invalid email format
- `409 Conflict`: Email already exists
- `429 Too Many Requests`: Rate limit exceeded
### Example
const user = await api.users.create({
email: 'jane@example.com',
name: 'Jane Doe',
role: 'admin'
});
Why this works:
- Comprehensive but scannable
- Table format for parameters (faster than prose)
- Shows both success and error cases
- Example demonstrates actual usage
Tier 4: Conceptual/Architecture Docs (Advanced Users)
Explains why things work the way they do. Design decisions, architecture, best practices.
This is valuable for advanced users. Beginners don't need it.
Topics:
- Architecture overview
- Security model
- Data model and relationships
- Scaling considerations
- Best practices
Critical: Don't make beginners read this to get started. Link to it from quickstart for those interested.
The "Answer-First" Principle
Every documentation page should answer its title question in the first 10 words.
Bad:
How Do I Authenticate?
Authentication is a critical component of API security. Acme API supports multiple authentication methods to accommodate different use cases and security requirements. Before diving into implementation...
Reader still doesn't know how to authenticate.
Good:
How Do I Authenticate?
Include your API key in the Authorization header:
Authorization: Bearer your_api_key_hereThat's it. For most use cases, this is all you need.
[Continue reading for OAuth, SSO, and advanced options →]
Reader has the answer in 3 seconds. Can leave immediately or continue for depth.
Formula:
- Answer the question immediately (code example if applicable)
- Confirm it works ("That's it" / "This will..." / Expected output)
- Offer optional depth (link to advanced guide, not inline)
Code Examples: What Makes Them Actually Useful
Rule 1: Complete, Not Snippets
Bad (snippet):
api.users.create(email, name)
Questions this raises:
- How do I import
api? - What's the full syntax?
- What does it return?
Good (complete):
from acme_api import AcmeAPI
api = AcmeAPI(api_key='your_key_here')
user = api.users.create(
email='test@example.com',
name='Test User'
)
print(f"Created user {user.id}")
# Output: Created user usr_abc123
Developer can copy-paste and run this immediately.
Rule 2: Show Errors, Not Just Success
Real code handles failures. Examples should model this.
Bad:
const user = await api.users.get(userId);
console.log(user.email);
Good:
try {
const user = await api.users.get(userId);
console.log(user.email);
} catch (error) {
if (error.status === 404) {
console.error('User not found');
} else if (error.status === 429) {
console.error('Rate limit exceeded. Retry after:', error.retryAfter);
} else {
throw error; // Unexpected error
}
}
This teaches proper error handling patterns.
Rule 3: Realistic, Not Toy Examples
Toy example:
# Create a user
user = api.users.create("test@example.com", "Test")
Realistic example:
# Bulk create users from CSV with error handling
import csv
from acme_api import AcmeAPI, RateLimitError, ValidationError
api = AcmeAPI(api_key=os.getenv('ACME_API_KEY'))
created_users = []
failed_rows = []
with open('users.csv') as f:
reader = csv.DictReader(f)
for row in reader:
try:
user = api.users.create(
email=row['email'],
name=row['name']
)
created_users.append(user)
except RateLimitError:
time.sleep(60) # Wait 1 minute
# Retry (implementation left as exercise)
except ValidationError as e:
failed_rows.append({
'row': row,
'error': str(e)
})
print(f"Created {len(created_users)} users")
print(f"Failed {len(failed_rows)} rows")
This shows:
- Environment variable for API key (security best practice)
- CSV processing (common use case)
- Rate limit handling
- Error tracking
- Progress reporting
Developer can adapt this to their actual work.
Rule 4: Multi-Language Examples
Different developers use different languages. Provide examples in:
- JavaScript/TypeScript (most common for web)
- Python (data science, backend)
- cURL (language-agnostic, shows raw HTTP)
Use tabs to avoid overwhelming:
Navigation: Making Docs Discoverable
Strategy 1: Progressive Disclosure
Don't show everything at once. Start simple, reveal complexity as needed.
Landing page structure:
- Hero: One sentence explaining what this is
- Quickstart: "Get started in 5 minutes" → /quickstart
- Common tasks: 4-6 most common use cases with direct links
- Reference: "Full API reference" → /reference
- Support: How to get help
Not:
- List of 40 documentation pages
- Alphabetical index
- "Introduction" that assumes you already know what this is
Strategy 2: Search-First Architecture
Assume developers will search, not browse.
Requirements:
- Fast, accurate search
- Search results show code snippets, not just page titles
- Search indexed on every content update
Best practices:
- Use Algolia DocSearch (free for open source)
- Alternatively: Lunr.js (self-hosted), Meilisearch (open source)
- Show "popular searches" on landing page
Strategy 3: Breadcrumb Context
Every page should show:
- Where you are in the docs hierarchy
- How to get back to common starting points
Example breadcrumb:
Home > Guides > Authentication > OAuth 2.0 Flow
Prevents "where am I?" disorientation.
Strategy 4: Related Pages
Bottom of every page:
Related:
Helps discovery of adjacent topics.
Visual Formatting: Making Docs Scannable
Developers scan, they don't read linearly.
Use Visual Hierarchy:
Bad:
To authenticate requests you need to include your API key in the Authorization header using the Bearer scheme. You can obtain an API key from the dashboard by navigating to Settings then API Keys then clicking Create New Key. Once you have your key include it in every request like this: Authorization: Bearer your_key.
Wall of text. Hard to scan.
Good:
How to Authenticate
1. Get your API key
Dashboard → Settings → API Keys → Create New Key
2. Include in requests
Authorization: Bearer your_api_key_hereThat's it. Include this header in every request.
Numbered steps, code block, clear sections.
Use Callout Boxes:
⚠️ Important: API keys are secret. Never commit them to version control.
💡 Tip: Store API keys in environment variables (process.env.API_KEY)
📝 Note: Rate limits apply per API key. See Rate Limiting
Visual distinction makes important information pop.
Use Tables for Structured Data:
Bad:
The rate limit is 1000 requests per hour for the free tier, 10000 requests per hour for the pro tier, and unlimited for enterprise tier.
Good:
| Tier | Rate Limit | |------|------------| | Free | 1,000 requests/hour | | Pro | 10,000 requests/hour | | Enterprise | Unlimited |
Tables are 10× more scannable.
Common Mistakes (And Fixes)
Mistake 1: Documentation for Documentation's Sake
Problem: "We need comprehensive docs" → write every possible detail → no one reads it.
Fix: Write for specific developer tasks, not completeness.
Ask: "What is a developer trying to accomplish?" Write that guide.
Mistake 2: Assuming Context
Problem: "Obviously they'll read the introduction first."
Fix: Assume every page is the entry point. Make each page self-contained.
Mistake 3: Out-of-Date Examples
Problem: Example code uses deprecated syntax. Developers copy it. Things break.
Fix:
- Version documentation (docs for v2.0, v2.1, v3.0 separate)
- Automated testing of code examples (run them in CI)
- "Last updated" timestamps on every page
Mistake 4: No Changelog
Problem: API changes. Developers don't know what broke.
Fix: Maintain detailed changelog:
## v2.1.0 - 2024-10-01
### Added
- New `users.search()` method with fuzzy matching
### Changed
- `users.list()` now returns max 100 results (was 50)
### Deprecated
- `users.findByEmail()` - use `users.search()` instead
### Removed
- Removed support for API v1 (EOL announced 2024-01-01)
Link to changelog prominently.
Mistake 5: Jargon Without Definition
Problem: "Configure the OAuth2 client credentials flow with PKCE."
If reader doesn't know what PKCE is, they're lost.
Fix: Either explain or link:
Configure the OAuth2 flow with PKCE (Proof Key for Code Exchange)
Or avoid jargon:
Use the secure authentication flow (includes proof-of-origin verification)
Measuring Documentation Success
How do you know if docs are working?
Metrics to Track:
Usage metrics:
- Page views per page (which pages are most valuable?)
- Time on page (are they reading or bouncing?)
- Search queries (what are they trying to find?)
- Bounce rate from landing page (is navigation clear?)
Behaviour metrics:
- Support ticket volume (good docs reduce tickets)
- Time to first API call (how long until developer succeeds?)
- Common error patterns (if everyone hits same error, docs failed)
Direct feedback:
- "Was this helpful?" widget on every page
- GitHub issues on docs repo (let developers suggest improvements)
- Developer interviews (talk to 5 users quarterly)
What Good Looks Like:
- 70%+ of developers successfully complete quickstart without help
- Support tickets decrease 30% after improving docs
- Developers say "documentation is excellent" in feedback
TL;DR: Writing documentation developers actually use
The problems:
- Too comprehensive → nobody reads it
- Conceptual explanations → developers want code
- Hidden answers → bury the lede
- Toy examples → don't match real use
The solutions:
1. Write in priority order:
- Tier 1: Quickstart (5-minute success)
- Tier 2: Common use cases (task-oriented guides)
- Tier 3: Reference (comprehensive API docs)
- Tier 4: Architecture (for advanced users)
2. Answer-first principle:
- Every page answers its question in first 10 words
- Lead with code example
- Offer optional depth via links
3. Better code examples:
- Complete, not snippets
- Show error handling
- Realistic use cases
- Multiple languages
4. Make it scannable:
- Visual hierarchy (headings, lists, callouts)
- Tables for structured data
- Search-first navigation
- Related pages for discovery
5. Keep it current:
- Version documentation
- Test code examples in CI
- Maintain changelog
- Add "last updated" timestamps
Chaos helps technical teams maintain documentation by surfacing what needs updating based on API changes and common support questions. Context-aware reminders ensure docs stay current. Start your free 14-day trial.