Documentation That Matters
Code comments are one form of documentation. But there's another form that matters deeply: API documentation.
When you write code that others will use—libraries, public APIs, shared modules—formal documentation becomes essential. This isn't about explaining implementation details. It's about explaining the contract.
When to Write Formal Documentation
Write Documentation For:
Public APIs and Libraries
/**
* Sends an email through the configured mail provider.
*
* @param to - Recipient email address
* @param subject - Email subject line (max 998 characters per RFC 2822)
* @param body - Email body (HTML supported)
* @returns Promise resolving to the message ID
* @throws {InvalidEmailError} If recipient address is malformed
* @throws {RateLimitError} If sending rate exceeds 100/minute
*
* @example
* const messageId = await sendEmail(
* 'user@example.com',
* 'Welcome!',
* '<h1>Thanks for signing up</h1>'
* );
*/
async function sendEmail(to: string, subject: string, body: string): Promise<string>
Complex Algorithms
/**
* Implements Levenshtein distance calculation using Wagner-Fischer algorithm.
*
* Time complexity: O(m*n) where m and n are string lengths
* Space complexity: O(min(m,n)) using single-row optimization
*
* @see https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm
*/
function levenshteinDistance(a: string, b: string): number
Configuration and Setup
/**
* Database connection configuration.
*
* @property host - Database server hostname (default: 'localhost')
* @property port - Database port (default: 5432)
* @property maxConnections - Maximum pool size. Set based on
* (core_count * 2) + effective_spindle_count for optimal performance.
* @property idleTimeout - How long idle connections stay open (ms)
*/
interface DatabaseConfig { }
Skip Documentation For:
Internal implementation details — These change frequently and documentation quickly goes stale.
Obvious code — getName() doesn't need a docstring saying "Gets the name."
Private methods — Unless they're complex, the code should speak for itself.
The Elements of Good API Documentation
1. Description
What does this do at a high level?
/**
* Validates and normalizes a phone number to E.164 format.
* Handles various input formats including local, national, and international.
*/
function normalizePhoneNumber(input: string, countryCode?: string): string
2. Parameters
What does each parameter accept?
/**
* @param input - Raw phone number string in any common format
* @param countryCode - ISO 3166-1 alpha-2 country code (e.g., 'US', 'GB').
* Required if input doesn't include country code.
*/
Document:
- What format is expected
- What values are valid
- What happens with edge cases
3. Return Value
What comes back?
/**
* @returns Phone number in E.164 format (e.g., '+14155551234')
* or null if the number cannot be parsed
*/
4. Errors and Exceptions
What can go wrong?
/**
* @throws {InvalidPhoneError} If the number is malformed
* @throws {UnsupportedCountryError} If the country is not supported
*/
5. Examples
Show it in action:
/**
* @example
* // US number with country code
* normalizePhoneNumber('+1 (415) 555-1234') // Returns: '+14155551234'
*
* @example
* // UK number without country code
* normalizePhoneNumber('020 7946 0958', 'GB') // Returns: '+442079460958'
*
* @example
* // Invalid number
* normalizePhoneNumber('not-a-number') // Throws: InvalidPhoneError
*/
Examples are often the most valuable part of documentation.
JSDoc/TSDoc Best Practices
Do Document
/**
* Creates a new user account.
*
* @param userData - User information for account creation
* @param options - Additional options for account setup
* @returns The created user with generated ID and timestamps
* @throws {ValidationError} If required fields are missing
* @throws {DuplicateEmailError} If email is already registered
*/
async function createUser(
userData: CreateUserInput,
options?: CreateUserOptions
): Promise<User>
Don't Duplicate the Obvious
// BAD: Adds nothing
/**
* @param id - The id
* @returns The user
*/
function getUser(id: string): User
// BETTER: Adds context
/**
* @param id - User's unique identifier (UUID v4)
* @returns User object, or throws if not found
* @throws {NotFoundError} If no user exists with this ID
*/
function getUser(id: string): User
Keeping Documentation in Sync
Documentation that lies is worse than no documentation. Keep it accurate:
1. Document Close to Code
Put documentation next to the code it documents. Separate wiki pages go stale.
2. Include Documentation in Code Review
When reviewing a function change, check if the documentation still matches.
3. Use TypeScript/Type Annotations
Types are documentation that can't lie—the compiler enforces them.
4. Generate from Source
Tools like TypeDoc, JSDoc, and Swagger generate documentation from code comments. This keeps them closer to the source of truth.
5. Test Your Examples
Example code in documentation should be tested:
/**
* @example
* ```typescript
* // This code is extracted and run as a test
* expect(add(2, 3)).toBe(5);
* ```
*/
README Documentation
For projects and packages, the README is essential:
What to Include
- What it does — One paragraph summary
- How to install — Package manager commands
- Quick start — Minimal example to get running
- API overview — Key functions and concepts
- Configuration — Environment variables, options
- Contributing — How to contribute (if applicable)
What to Avoid
- Implementation details that will change
- Extensive API reference (generate that separately)
- Obvious information ("This is a JavaScript library")
Architecture Documentation
Sometimes you need higher-level documentation:
Architecture Decision Records (ADRs)
Document significant decisions:
# ADR 001: Use PostgreSQL for User Data
## Status
Accepted
## Context
We need to store user data with complex relationships.
## Decision
We will use PostgreSQL.
## Consequences
- Need to manage database migrations
- Team needs SQL knowledge
- Good query performance for our access patterns
System Diagrams
For complex systems, diagrams help:
- Component relationships
- Data flow
- Deployment architecture
But keep them high-level. Detailed diagrams go stale instantly.
Key insight: Documentation serves different purposes at different levels. Code comments explain implementation intent. API documentation explains contracts. Architecture documentation explains decisions. Match the documentation type to the need, and always keep it in sync.