Loading content...
Having the right tools and formats is necessary but not sufficient. Documentation is a product, and like any product, its value depends on how well it serves its users. Technically accurate documentation that no one can understand or find is worthless; well-organized, clearly written documentation accelerates development and reduces support burden.
This page establishes the principles and patterns that transform documentation from an obligation into an asset. These practices apply whether you're writing reference docs, tutorials, guides, or inline code comments—they're about communication, not just information.
The documentation mindset:
By the end of this page, you will understand documentation types and their purposes, writing principles for clarity and usability, information architecture for discoverability, example design for learnability, and strategies for maintaining accuracy. You'll be equipped to create documentation that developers actually want to read.
Not all documentation serves the same purpose. Different developers, at different stages, need different types of documentation. Understanding these types helps you create comprehensive coverage without redundancy.
The Diátaxis framework (developed by Daniele Procida) identifies four fundamental documentation types:
| Type | Purpose | Orientation | Example |
|---|---|---|---|
| Tutorials | Learning by doing | Learning → Practical | "Build Your First Order API Integration" |
| How-to Guides | Solving problems | Working → Practical | "How to Handle Payment Failures" |
| Reference | Information lookup | Working → Theoretical | "Order API Endpoint Reference" |
| Explanation | Understanding concepts | Learning → Theoretical | "How Order Processing Works" |
Each type has distinct characteristics:
A common mistake is mixing documentation types in a single page—explaining concepts in the middle of a tutorial, or adding how-to steps in reference docs. This confuses readers. Keep types separate and link between them instead.
Developer documentation has a specific audience with specific needs. Developers are typically:
Writing principles for this audience:
After writing any paragraph, ask "So what? What can the reader do with this information?" If there's no actionable answer, the content is either explanation (belongs in a concepts doc) or filler (delete it).
Even well-written documentation fails if readers can't find what they need. Information architecture (IA) is how you organize content to make it discoverable and navigable.
Key IA principles:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
# Create Order > Creates a new order with the specified items and shipping details. ## Endpoint `POST /v2/orders` ## Authentication Requires `Bearer` authentication with `orders:create` scope.See [Authentication Guide](/guides/authentication) for details. ## Request ### Headers | Header | Required | Description ||--------|----------|-------------|| Authorization | Yes | Bearer token || Idempotency-Key | Recommended | Unique key for safe retries | ### Body Parameters | Parameter | Type | Required | Description ||-----------|------|----------|-------------|| items | array | Yes | Order items (min: 1) || items[].productId | string | Yes | Product identifier || items[].quantity | integer | Yes | Quantity (1-99) || shippingAddress | object | Yes | Delivery address || promoCode | string | No | Promotional code | ### Example Request ```bashcurl -X POST https://api.example.com/v2/orders \ -H "Authorization: Bearer sk_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "items": [{"productId": "prod_abc", "quantity": 2}], "shippingAddress": {...} }'``` ## Response ### Success Response (201 Created) Returns the created [Order](/reference/types#order) object. ```json{ "id": "ord_123", "status": "pending", ...}``` ### Error Responses | Status | Error Code | Description ||--------|------------|-------------|| 400 | validation_error | Invalid request body || 401 | unauthorized | Missing or invalid auth || 422 | out_of_stock | Item unavailable | See [Error Handling](/guides/errors) for error response format. ## Related - [Get Order](/reference/orders/get) - Retrieve order details- [List Orders](/reference/orders/list) - List user's orders- [Order Lifecycle](/concepts/order-lifecycle) - Status transitionsCreate templates for each documentation type. When every endpoint page has the same structure, readers learn to find information quickly. Inconsistent structure forces re-learning on every page.
Examples are often more useful than descriptions. A well-designed example shows rather than tells, demonstrating actual usage in context. But not all examples are equal—thoughtful design makes them genuinely instructive.
Principles of effective examples:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
// ==============================================// EXAMPLE 1: Minimal - Just make it work// Goal: Show the simplest successful request// ============================================== import { OrderClient } from '@example/orders'; const client = new OrderClient({ apiKey: process.env.API_KEY }); // Create a simple orderconst order = await client.orders.create({ items: [ { productId: 'prod_shirt_blue_m', quantity: 1 } ], shippingAddress: { line1: '123 Main St', city: 'San Francisco', state: 'CA', postalCode: '94102', country: 'US', },}); console.log(`Order created: ${order.id}`); // ==============================================// EXAMPLE 2: Realistic - Common production usage// Goal: Show typical real-world patterns// ============================================== import { OrderClient, OrderError } from '@example/orders'; const client = new OrderClient({ apiKey: process.env.API_KEY, timeout: 30000, // 30 second timeout}); async function createOrderFromCart(cart: Cart, user: User): Promise<Order> { // Convert cart items to order items const items = cart.items.map(item => ({ productId: item.product.id, quantity: item.quantity, // Optional: Apply any item-level discounts priceOverrideCents: item.salePrice?.cents, })); try { const order = await client.orders.create({ items, shippingAddress: user.defaultAddress, billingAddress: user.billingAddress ?? user.defaultAddress, promoCode: cart.appliedPromoCode, // Optional promo code metadata: { cartId: cart.id, // Track origin for analytics campaignSource: cart.utmSource, }, }, { // Idempotency key prevents duplicate orders on retry idempotencyKey: `order-${cart.id}-${Date.now()}`, }); return order; } catch (error) { if (error instanceof OrderError) { // Handle specific error types switch (error.code) { case 'out_of_stock': // Identify which items are unavailable const unavailable = error.details.unavailableItems; throw new CartError(`Items unavailable: ${unavailable.join(', ')}`); case 'invalid_address': throw new AddressError(error.message, error.details.fields); case 'promo_expired': // Promo is no longer valid - proceed without it console.warn('Promo code expired, creating order without discount'); return createOrderFromCart({ ...cart, appliedPromoCode: null }, user); default: throw error; } } throw error; }} // ==============================================// EXAMPLE 3: Advanced - Edge cases and features// Goal: Cover advanced usage patterns// ============================================== import { OrderClient, WebhookHandler } from '@example/orders'; // Example: Subscription order with scheduled deliveryconst subscriptionOrder = await client.orders.create({ items: [ { productId: 'prod_coffee_beans', quantity: 2 } ], shippingAddress: customerAddress, // Subscription configuration subscription: { frequency: 'monthly', anchorDay: 15, // Deliver on 15th of each month skipHolidays: true, // Delay if 15th is a holiday }, // Future-dated order scheduledFor: '2024-02-15T09:00:00Z', // Gift order configuration gift: { recipientEmail: 'friend@example.com', message: 'Happy Birthday!', hidePrice: true, // Don't show price on packing slip }, // Custom fulfillment instructions fulfillment: { instructions: 'Signature required. Leave at door if not home.', preferredCarrier: 'fedex', requiresRefrigeration: false, },});Include documentation examples in your test suite. When examples are tested, they can't become outdated without failing CI. Many teams use documentation testing tools that extract code blocks and execute them.
Error documentation is often neglected despite being critically important. When things go wrong, developers turn to documentation—and finding clear, actionable error guidance can mean the difference between a quick fix and hours of debugging.
What error documentation should include:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
# Error Handling ## Error Response Format All errors return a JSON object with consistent structure: ```json{ "error": { "code": "validation_error", "message": "Request validation failed", "details": { "fields": { "items[0].quantity": "Must be between 1 and 99" } }, "requestId": "req_abc123", "documentation": "https://docs.example.com/errors#validation_error" }}``` | Field | Type | Description ||-------|------|-------------|| code | string | Machine-readable error code || message | string | Human-readable description || details | object | Additional context (varies by error) || requestId | string | Unique ID for support requests || documentation | string | Link to error documentation | --- ## Error Codes Reference ### validation_error **HTTP Status:** 400 Bad Request The request body failed validation. Check the `details.fields` objectfor specific field errors. **Common causes:**- Missing required field- Invalid field format (e.g., invalid email)- Value out of allowed range **Example:**```json{ "error": { "code": "validation_error", "message": "Request validation failed", "details": { "fields": { "email": "Invalid email format", "items": "At least one item is required" } } }}``` **Resolution:**1. Check the `details.fields` object for specific issues2. Correct the invalid fields3. Retry the request --- ### out_of_stock **HTTP Status:** 422 Unprocessable Entity One or more items in the order are unavailable. **Common causes:**- Item sold out between cart view and checkout- Quantity exceeds available inventory- Item discontinued **Example:**```json{ "error": { "code": "out_of_stock", "message": "Some items are unavailable", "details": { "unavailableItems": [ { "productId": "prod_abc123", "requestedQuantity": 5, "availableQuantity": 2 } ] } }}``` **Resolution:**1. Show user which items are unavailable2. Offer to reduce quantity to available amount3. Offer alternative products if available4. Allow user to remove items and proceed **Retryable:** No - inventory state won't change on immediate retry.Check availability before retrying. --- ### rate_limited **HTTP Status:** 429 Too Many Requests You've exceeded the API rate limit. **Headers included:**| Header | Description ||--------|-------------|| X-RateLimit-Limit | Requests allowed per window || X-RateLimit-Remaining | Requests remaining || X-RateLimit-Reset | Unix timestamp when limit resets || Retry-After | Seconds to wait before retrying | **Resolution:**1. Read `Retry-After` header for wait time2. Implement exponential backoff3. Consider request batching to reduce call volume4. Contact support if limits are insufficient for your use case **Retryable:** Yes - after waiting for the reset window. ```typescript// Example retry logicasync function fetchWithRetry(request: () => Promise<Response>): Promise<Response> { const response = await request(); if (response.status === 429) { const retryAfter = parseInt(response.headers.get('Retry-After') ?? '60'); console.log(`Rate limited. Retrying in ${retryAfter}s`); await sleep(retryAfter * 1000); return fetchWithRetry(request); // Retry } return response;}```Documentation should be accessible to all developers, including those using assistive technology, those reading in a second language, and those with varying experience levels.
Accessibility considerations:
Inclusive language:
Avoid terminology that can be exclusionary or carries problematic connotations. The tech industry is actively moving away from certain terms:
| Avoid | Prefer | Reason |
|---|---|---|
| master/slave | primary/replica, leader/follower | Historical connotations |
| whitelist/blacklist | allowlist/denylist | Racial connotations |
| dummy value | placeholder, sample, test | Ableist connotations |
| sanity check | validation check, confidence check | Mental health sensitivity |
| guys (for mixed groups) | everyone, folks, team | Gender inclusivity |
| simple/easy | straightforward, common | Relative to experience; can feel dismissive |
Inclusive language practices continue to evolve. What matters is the intent: welcoming all developers to your documentation. When updating legacy docs, update terminology as part of normal maintenance rather than all-at-once rewrites.
Documentation, like code, should be tested. Untested documentation degrades over time as APIs evolve. Testing catches broken examples, dead links, and outdated information before they frustrate users.
Types of documentation tests:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
// doc-tests/example-extractor.ts// Extract and test code examples from markdown files import { glob } from 'glob';import { readFile } from 'fs/promises';import { execSync } from 'child_process'; interface CodeBlock { language: string; code: string; file: string; line: number;} async function extractCodeBlocks(pattern: string): Promise<CodeBlock[]> { const files = await glob(pattern); const blocks: CodeBlock[] = []; for (const file of files) { const content = await readFile(file, 'utf-8'); const lines = content.split('\n'); let inCodeBlock = false; let currentBlock: Partial<CodeBlock> = {}; let codeLines: string[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith('```') && !inCodeBlock) { inCodeBlock = true; currentBlock = { language: line.slice(3).trim(), file, line: i + 1, }; codeLines = []; } else if (line === '```' && inCodeBlock) { inCodeBlock = false; blocks.push({ ...currentBlock, code: codeLines.join('\n'), } as CodeBlock); } else if (inCodeBlock) { codeLines.push(line); } } } return blocks;} async function testTypeScriptExamples(blocks: CodeBlock[]): Promise<void> { const tsBlocks = blocks.filter(b => b.language === 'typescript' || b.language === 'ts' ); for (const block of tsBlocks) { console.log(`Testing: ${block.file}:${block.line}`); // Write to temp file const tempFile = `/tmp/doc-test-${Date.now()}.ts`; await writeFile(tempFile, block.code); try { // Type-check without emitting execSync(`npx tsc --noEmit ${tempFile}`, { stdio: 'pipe' }); console.log(' ✓ Type check passed'); } catch (error) { console.error(` ✗ Type error in ${block.file}:${block.line}`); console.error(error.stderr?.toString()); process.exitCode = 1; } finally { await unlink(tempFile); } }} // Run testsconst blocks = await extractCodeBlocks('docs/**/*.md');await testTypeScriptExamples(blocks);123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
# .github/workflows/docs-test.ymlname: Documentation Tests on: pull_request: paths: - 'docs/**' - 'openapi.yaml' jobs: test-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Check for broken links - name: Link Checker uses: lycheeverse/lychee-action@v1 with: args: > --verbose --no-progress --accept 200,204 --exclude-mail './docs/**/*.md' fail: true # Validate OpenAPI spec - name: Validate OpenAPI run: npx @stoplight/spectral-cli lint openapi.yaml # Test code examples compile - name: Setup Node uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm ci - name: Test TypeScript examples run: npm run test:doc-examples # Spell check - name: Spell Check uses: streetsidesoftware/cspell-action@v5 with: files: 'docs/**/*.md' config: '.cspell.json' # Check for outdated docs (files not updated with code) - name: Doc freshness check run: | npm run docs:freshness-checkGreat documentation is a craft that combines clear writing, thoughtful organization, helpful examples, and ongoing maintenance. Let's consolidate the key principles:
What's next:
The final page addresses the hardest documentation challenge: Keeping Documentation Current. We'll explore strategies for detecting drift, integrating docs into development workflow, and building a culture where documentation stays accurate over time.
You now understand the principles and practices that make documentation genuinely useful—from writing style to information architecture to testing. Next, we'll tackle the ongoing challenge of keeping documentation accurate as APIs evolve.