Documentation Writing
This skill covers best practices for creating effective documentation that developers actually want to read and maintain.
Core Philosophy
Great documentation is:
-
Clear: Easy to understand on first reading
-
Complete: Answers the questions users actually have
-
Current: Kept in sync with code changes
-
Concise: No unnecessary fluff
-
Scannable: Easy to find what you need
-
Practical: Focuses on how-to, not just what
Types of Documentation
- README Files
The front door to your project:
Project Name
Brief description (1-2 sentences) of what this does.
Features
- Key feature 1
- Key feature 2
- Key feature 3
Quick Start
```bash npm install npm run dev ```
Visit http://localhost:3000
Project Structure
``` src/ routes/ # Remix routes lib/ # Shared utilities components/ # React components ```
Documentation
Contributing
See CONTRIBUTING.md
License
MIT
Key elements:
-
What it does (immediately)
-
How to get started (quickly)
-
Where to find more information
-
How to contribute
- Code Comments
Use sparingly and wisely:
✅ Do explain WHY:
// We batch writes to avoid hitting DynamoDB's 25-item transaction limit const batches = chunk(items, 25);
// Disable cache for this endpoint because user data changes frequently // and stale data causes support tickets. See issue #123 export const loader = async () => { // ... };
✅ Do document complex algorithms:
/**
- Implements the Luhn algorithm for credit card validation.
- https://en.wikipedia.org/wiki/Luhn_algorithm */ function validateCardNumber(cardNumber: string): boolean { // Double every second digit from right to left // If doubling results in two digits, add them together // ... }
❌ Don't state the obvious:
// Get the user const user = await getUser(id);
// Check if user exists if (!user) { // Return error return error("Not found"); }
- API Documentation
Document public APIs thoroughly:
/**
- Creates a new user account and sends a welcome email.
- @param email - User's email address (must be unique)
- @param password - Plain text password (will be hashed)
- @param name - User's display name
- @returns The created user object (without password)
- @throws {ValidationError} If email is invalid or already exists
- @throws {EmailError} If welcome email fails to send
- @example
-
- const user = await createUser({
- email: "user@example.com",
- password: "secure123",
- name: "John Doe"
- });
-
*/ export async function createUser( email: string, password: string, name: string ): Promise<User> { // Implementation }
- Architecture Documentation
Explain the big picture:
Architecture Overview
System Components
Frontend (Remix)
- Server-side rendering for initial load
- Progressive enhancement for interactivity
- Nested routes for UI composition
Backend (SST)
- Lambda functions for API endpoints
- DynamoDB for data storage (single table design)
- S3 for file uploads
- SES for email sending
Data Flow
- User submits form → Remix action
- Action validates data
- Action calls business logic in
src/lib/ - Business logic updates DynamoDB
- Action redirects or returns errors
- Loader refetches data
- Component renders updated state
Key Design Decisions
Why Single Table DynamoDB?
We use single table design because:
- Lower costs (one table vs many)
- Better performance (no joins needed)
- Atomic transactions across entities
- Aligns with serverless architecture
See: docs/dynamodb-design.md
Why SST over CDK?
- Type-safe resource bindings
- Better developer experience
- Simpler infrastructure code
- Great local development story
- Setup Documentation
Make it easy for new developers:
Setup Guide
Prerequisites
- Node.js 20+
- AWS account with CLI configured
- GitHub account (for deployment)
Installation
-
Clone the repository: ```bash git clone https://github.com/your-org/project cd project ```
-
Install dependencies: ```bash npm install ```
-
Set up environment: ```bash cp .env.example .env
Edit .env and add your values
```
-
Start SST: ```bash npm run sst:dev ```
-
In another terminal, start Remix: ```bash npm run dev ```
-
Visit http://localhost:3000
Troubleshooting
"Cannot find module 'sst'"
Run npm install again. SST might not have installed correctly.
Port 3000 already in use
Kill the process using port 3000 or change the port in package.json.
- ADR (Architecture Decision Records)
Document important decisions:
ADR 001: Use Single Table Design for DynamoDB
Status
Accepted
Context
We need to store users, posts, comments, and relationships between them. Traditional approach would be separate tables, but we're building a serverless app.
Decision
We will use single table design with generic pk/sk keys.
Consequences
Positive
- Lower costs (one table vs many)
- Better query performance (no joins)
- Atomic transactions across entity types
- Simpler infrastructure
Negative
- Higher initial learning curve
- Requires understanding access patterns upfront
- More complex to query during development
Alternatives Considered
- Multiple tables (rejected: higher costs, no cross-table transactions)
- Relational DB (rejected: doesn't fit serverless model well)
Documentation Patterns
Pattern 1: Tutorial-Style Guides
Walk through a complete example:
Building Your First Feature
Let's build a simple blog post feature together.
Step 1: Create the Database Schema
First, we'll define how posts are stored in DynamoDB.
A post has:
- ID (unique identifier)
- Author (user who created it)
- Title
- Content
- Created date
In single table design, we'll store this as:
```typescript { pk: "POST#<postId>", sk: "METADATA", authorId: "<userId>", title: "My First Post", content: "Hello, world!", createdAt: "2025-01-02T10:00:00Z" } ```
Step 2: Create the Route
Create a new file: app/routes/posts.new.tsx
```typescript // ... code here ```
Step 3: Add the Form
// ... continue the tutorial
Pattern 2: Reference Documentation
Quick lookup for APIs:
Database API Reference
createPost()
Creates a new blog post.
Signature: ```typescript createPost(authorId: string, data: PostData): Promise<Post> ```
Parameters:
authorId- ID of the user creating the postdata.title- Post title (required, max 200 chars)data.content- Post content (required, max 50000 chars)
Returns:
- Promise resolving to created Post object
Throws:
ValidationError- If data is invalidDatabaseError- If write fails
Example: ```typescript const post = await createPost("user123", { title: "Hello World", content: "My first post!" }); ```
Pattern 3: Troubleshooting Guides
Help users solve common problems:
Troubleshooting Guide
Common Issues
SST Deploy Fails with "Resource already exists"
Symptoms: ``` Error: Resource MyFunction already exists ```
Cause: A previous deployment failed partway through.
Solution:
- Delete the CloudFormation stack manually: ```bash aws cloudformation delete-stack --stack-name my-app-dev ```
- Wait for deletion to complete
- Deploy again:
npm run deploy
DynamoDB Query Returns No Results
Symptoms: Query runs without errors but returns empty array.
Common Causes:
- Wrong key format - Check your pk/sk format matches
- Using scan instead of query - Use Query for single partition
- GSI not ready - Wait a few seconds after creating items
Debug Steps:
- Log the query parameters
- Check item in DynamoDB console
- Verify key format matches exactly
Best Practices
- Keep Docs Close to Code
src/ lib/ email/ send.ts README.md # Email system docs examples.md # Usage examples
- Use Markdown Formatting
Headers for sections
Subheaders for subsections
Bold for emphasis Italic for terms
code for inline code
```typescript
// code blocks
```
Blockquotes for notes
- Bullet lists
- For multiple items
-
Numbered lists
-
For sequential steps
-
Include Code Examples
Always show working code:
Creating a User
```typescript import { createUser } from "./lib/users";
const user = await createUser({ email: "user@example.com", password: "secure123", name: "John Doe" });
console.log(Created user: ${user.id});
```
- Link Related Documentation
See also:
- Keep It Current
<!-- Add a "Last Updated" note -->
Last Updated: 2025-01-02 Version: 2.0.0
Documentation Tools
JSDoc for TypeScript
/**
- Represents a blog post. / export interface Post { /* Unique identifier */ id: string;
/** Post title (max 200 characters) */ title: string;
/** Post content in Markdown format */ content: string;
/** ISO 8601 timestamp of creation */ createdAt: string;
/** ID of the author */ authorId: string; }
README Badges
Diagrams with Mermaid
```mermaid graph TD A[User submits form] --> B[Remix action] B --> C{Valid data?} C -->|Yes| D[Save to DB] C -->|No| E[Return errors] D --> F[Redirect to success] E --> G[Show form with errors] ```
Documentation Checklist
When documenting a new feature:
-
Update README if user-facing
-
Add JSDoc comments to public APIs
-
Create usage examples
-
Document configuration options
-
Explain error messages
-
Add troubleshooting tips
-
Link to related docs
-
Update architecture diagrams
-
Review for clarity
-
Test all code examples
Common Mistakes to Avoid
❌ Don't:
-
Write docs that are out of date
-
Use jargon without explanation
-
Skip error handling in examples
-
Document internal implementation details
-
Write novels (keep it concise)
-
Assume prior knowledge
✅ Do:
-
Update docs with code changes
-
Define terms on first use
-
Show how to handle errors
-
Document public APIs and behavior
-
Be clear and direct
-
Provide context and examples
Documentation as Code
Treat docs like code:
// docs/examples/create-user.test.ts // This file is both docs and tests!
import { createUser } from "../src/lib/users";
test("creating a user", async () => { // This example appears in docs const user = await createUser({ email: "user@example.com", password: "secure123", name: "John Doe" });
expect(user.email).toBe("user@example.com"); expect(user.name).toBe("John Doe"); });
Further Reading
-
Write The Docs: https://www.writethedocs.org/
-
MDN Writing Guidelines: https://developer.mozilla.org/en-US/docs/MDN/Writing_guidelines
-
Google Developer Documentation Style Guide: https://developers.google.com/style