Good documentation is force multiplier. It reduces onboarding time, decreases support burden, and enables developers to be productive without constant hand-holding. Yet most documentation fails to achieve these goals. This guide covers how to write documentation that developers actually read and find useful.
Why Documentation Fails
Common failure modes:
- Out of date: Docs written once, never maintained
- Too abstract: Concepts without concrete examples
- Missing context: Assumes too much prior knowledge
- Poor discoverability: Can't find what you need
- Wrong audience: Written for experts, used by beginners
Documentation Types
Different purposes need different documentation:
Tutorials
Learning-oriented, lessons that take the reader through a series of steps to complete a project.
Characteristics:
- Learning by doing
- Gets the user started
- Shows concrete steps
- Inspires confidence
Tutorials should set clear expectations upfront - what the reader will build, what they need to know beforehand, and how long it will take. Here's how to structure the opening of a tutorial.
# Build Your First API
By the end of this tutorial, you'll have a working REST API
that creates, reads, updates, and deletes todo items.
## Prerequisites
- Node.js 18+
- Basic JavaScript knowledge
## Step 1: Initialize the Project
...
Notice how this immediately tells readers what they'll accomplish and what they need. This helps them decide if the tutorial is right for them before investing time.
How-To Guides
Goal-oriented, solve a specific problem.
Characteristics:
- Assumes basic knowledge
- Focuses on completing a task
- Provides practical steps
- Addresses a specific need
Unlike tutorials, how-to guides assume the reader already has a working project and wants to add a specific capability. The structure reflects this - jump straight into requirements and steps.
# How to Add Authentication
This guide shows how to add JWT authentication to your existing API.
## Requirements
- An existing Express API
- A user database table
## Steps
### 1. Install Dependencies
```bash
npm install jsonwebtoken bcrypt
2. Create Auth Middleware
...
The key difference from tutorials: you're solving a specific problem, not teaching from scratch. Readers arrive with context and want to get straight to the solution.
### Reference
Information-oriented, accurate and complete.
**Characteristics:**
- Technical descriptions
- Structured consistently
- Comprehensive coverage
- Dry but precise
Reference documentation should follow a consistent format for every entry. Readers learn the pattern once and can quickly find what they need. Here's a template for configuration options.
```markdown
# Configuration Options
## `database.connection`
Type: `string`
Default: `'mysql'`
Environment variable: `DB_CONNECTION`
The database driver to use. Supported values:
- `mysql` - MySQL/MariaDB
- `pgsql` - PostgreSQL
- `sqlite` - SQLite
Consistency matters more than style in reference docs. Pick a format and stick to it across every page.
Explanation
Understanding-oriented, provides context and background.
Characteristics:
- Clarifies concepts
- Provides background
- Discusses alternatives
- Explains reasoning
Explanation documents help readers understand "why" rather than "how." They're the right place for architecture decisions, trade-off discussions, and conceptual foundations.
# Why We Use Event Sourcing
Event sourcing stores state changes as a sequence of events
rather than current state...
## Traditional vs Event Sourcing
In a traditional system, when a user updates their profile...
## When Event Sourcing Makes Sense
Consider event sourcing when you need:
- Complete audit trail
- Temporal queries
- Complex domain logic
These documents age well because they explain reasoning that remains valid even as implementation details change.
Writing Principles
Start With the User's Goal
Don't describe what something is;describe what it helps the user do.
The first example below describes the class; the second describes what the reader can accomplish. Always lead with the benefit to the user.
# Bad
## The Cache Class
The Cache class provides methods for storing and retrieving
cached data using various drivers.
# Good
## Caching
Speed up your application by caching expensive operations
and frequently accessed data.
Show, Don't Just Tell
Every concept needs a concrete example:
Telling readers they "can" do something isn't enough - show them exactly how. Code examples transform abstract capabilities into actionable knowledge.
# Bad
Use the `retry` option to automatically retry failed requests.
# Good
Use the `retry` option to automatically retry failed requests:
```php
$response = Http::retry(3, 100)->get($url);
// Retries up to 3 times with 100ms delay between attempts
### Use Progressive Disclosure
Start simple, add complexity gradually:
Most readers need the basic case. Put that first. Advanced users will scroll down; beginners won't be overwhelmed by complexity they don't need yet.
```markdown
## Quick Start
```php
Cache::put('key', 'value', 60);
$value = Cache::get('key');
Configuration
For most applications, the default configuration works well. When you need to customize caching behavior...
Advanced: Custom Cache Drivers
When the built-in drivers don't meet your needs...
This structure serves both beginners (who can stop at Quick Start) and experts (who can jump to Advanced).
### Write for Scanning
Developers scan before they read. Help them:
- **Clear headings**: Make structure obvious
- **Code first**: Put examples before explanations
- **Bullet points**: For lists and options
- **Bold key terms**: Highlight important concepts
### Include Error Messages
Document what goes wrong and how to fix it:
Error documentation is often more valuable than feature documentation. When someone hits an error, they're blocked and searching for answers. Be there with the solution.
```markdown
## Troubleshooting
### "Connection refused" error
This usually means Redis isn't running.
**Solution:**
```bash
redis-server --daemonize yes
"WRONGTYPE Operation" error
You're using a string command on a hash key.
Solution: Check your key names. You may have a collision.
Include the exact error message text - this is what developers will paste into their search engine. Matching that text makes your docs discoverable.
## Structure and Organization
### Consistent Navigation
A predictable folder structure helps readers build a mental model of where to find information. Group related content and use descriptive folder names.
docs/ ├── getting-started/ │ ├── installation.md │ ├── configuration.md │ └── quickstart.md ├── guides/ │ ├── authentication.md │ ├── caching.md │ └── testing.md ├── api-reference/ │ ├── cache.md │ ├── database.md │ └── http.md └── concepts/ ├── architecture.md └── security.md
### Landing Pages
Help users navigate to what they need:
Landing pages act as signposts. They don't contain detailed content themselves - they help readers find the right document for their current need.
```markdown
# Documentation
## Getting Started
New here? Start with these guides.
- [Installation](./installation.md) - Get up and running
- [Quick Start](./quickstart.md) - Build something in 5 minutes
## Guides
Step-by-step guides for common tasks.
- [Authentication](./guides/auth.md)
- [Deploying to Production](./guides/deploy.md)
## API Reference
Complete reference for all methods and options.
- [Cache API](./api/cache.md)
- [Database API](./api/database.md)
Brief descriptions after each link help readers choose without clicking through multiple pages.
Version Awareness
When you maintain multiple versions, make it obvious which version the reader is viewing. Call out breaking changes prominently.
> **Note**: This documentation is for version 3.x.
> For version 2.x, see [legacy docs](./v2/).
## Breaking Changes in 3.0
The `config()` helper now returns `null` instead of throwing...
Code Examples
Make Examples Complete
Incomplete examples frustrate readers. They have to guess at imports, variable setup, and context. Provide everything needed to actually run the code.
# Bad - Missing context
```php
$user->notify(new InvoicePaid($invoice));
Good - Complete, runnable example
use App\Models\User;
use App\Notifications\InvoicePaid;
use App\Models\Invoice;
$user = User::find(1);
$invoice = Invoice::find(42);
$user->notify(new InvoicePaid($invoice));
### Show Input and Output
When demonstrating functions or methods, show both what goes in and what comes out. This helps readers verify they understand correctly.
```markdown
```php
$collection = collect([1, 2, 3, 4, 5]);
$filtered = $collection->filter(fn($value) => $value > 2);
$filtered->all();
// [3, 4, 5]
### Use Realistic Data
Abstract variable names like `$foo` and `$bar` force readers to do mental translation. Realistic examples communicate intent alongside mechanics.
```markdown
# Bad - Meaningless
$result = process($foo, $bar);
# Good - Realistic
$order = Order::create([
'customer_id' => $customer->id,
'total' => 99.99,
'status' => 'pending',
]);
Domain-specific examples also help readers understand how the code fits into real applications.
Maintaining Documentation
Docs as Code
Treat documentation like code:
- Store in version control
- Review changes in PRs
- Run linting and link checking
- Deploy automatically
Automating your documentation workflow catches broken links, validates formatting, and ensures docs deploy alongside code changes.
# .github/workflows/docs.yml
name: Documentation
on:
push:
branches: [main]
paths: ['docs/**']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run docs:build
- run: npm run docs:check-links
The paths filter ensures the workflow only runs when documentation actually changes, saving CI minutes.
Keep Docs Close to Code
Co-locating documentation with the code it describes increases the chance it gets updated when the code changes. Developers are more likely to update a README in the same directory than navigate to a separate docs folder.
src/
├── auth/
│ ├── AuthManager.php
│ └── README.md # Auth module docs
├── cache/
│ ├── CacheManager.php
│ └── README.md # Cache module docs
Automate What You Can
- Generate API reference from code comments
- Extract examples from tests
- Validate code examples actually run
PHPDoc comments can generate reference documentation automatically. The example below shows how to document a method so tooling can extract both the API signature and usage examples.
/**
* Store an item in the cache.
*
* @param string $key
* @param mixed $value
* @param int $ttl Time to live in seconds
* @return bool
*
* @example
* Cache::put('user:1', $user, 3600);
*/
public function put(string $key, mixed $value, int $ttl): bool
When documentation is generated from code, it stays in sync automatically.
Measuring Documentation Quality
Analytics
Track what people search for and don't find:
- Search queries with no results
- Pages with high bounce rates
- Time on page
Feedback Loops
Give readers an easy way to signal when documentation helped or fell short. Even simple binary feedback reveals problem areas.
---
Was this page helpful?
[Yes](#) | [No](#) | [Report an issue](#)
---
Documentation Tests
You can write tests that verify your documentation examples actually work. This catches staleness when APIs change and examples break.
// Test that documentation examples work
public function test_cache_example_from_docs()
{
Cache::put('key', 'value', 60);
$this->assertEquals('value', Cache::get('key'));
}
These tests serve double duty - they validate docs and serve as additional integration tests for your codebase.
Conclusion
Good documentation is written for a specific audience with a specific goal. Choose the right type (tutorial, how-to, reference, explanation), show concrete examples, structure for scanning, and maintain docs like code. The investment pays off in reduced support burden and faster onboarding.