Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Technical Writing and Documentation

For .NET engineers who know: XML doc comments, Sandcastle, Azure DevOps Wiki, Confluence, and the Microsoft documentation style used across MSDN and learn.microsoft.com You’ll learn: How documentation works in open-source JS/TS projects — README-driven development, TSDoc, ADRs, and the docs-as-code philosophy — and our specific conventions for new projects Time: 10-15 min read


The .NET Way (What You Already Know)

Microsoft has a mature, consistent documentation culture built around a few tools. XML doc comments in C# (/// <summary>, /// <param>, /// <returns>) are first-class language constructs that IDEs render on hover and that Sandcastle or DocFX compile into browsable HTML documentation. Azure DevOps Wiki provides a centralized wiki with Markdown support, often used for runbooks, onboarding guides, and architectural notes. Confluence is common in larger organizations. Swagger/Swashbuckle generates API reference from controller annotations.

The pattern: documentation lives in dedicated tools or generated from structured comments. It is authoritative when someone goes looking for it, but it is separate from the code and can drift.

/// <summary>
/// Calculates the total price for an order, including applicable taxes
/// and discounts. Returns zero if the order has no line items.
/// </summary>
/// <param name="order">The order to calculate. Must not be null.</param>
/// <param name="applyDiscount">When true, applies the customer's loyalty discount.</param>
/// <returns>The total price in USD as a decimal, rounded to two decimal places.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="order"/> is null.</exception>
public decimal CalculateTotal(Order order, bool applyDiscount = true)

This is good documentation. The hover tooltip in Visual Studio is genuinely useful. The generated HTML documentation is browsable. The problem is maintenance: when you change the behavior of CalculateTotal, the docs do not automatically fail CI. They silently drift.


The JS/TS Way

Open-source JS/TS projects use a fundamentally different documentation model: docs-as-code. Documentation is:

  • Written in Markdown, stored in the same repository as the code
  • Versioned with Git alongside the code it documents
  • Reviewed in pull requests alongside code changes
  • Checked for broken links and formatting in CI

The philosophy is pragmatic: documentation that is hard to update will not be updated. Keeping docs in the repo, in the same PR as code changes, is the only reliable way to keep them current.

The README as the Front Door

Every repository has a README.md that serves as the primary entry point for anyone who encounters the project. On GitHub, it renders automatically on the repository home page. A README is not optional decoration — it is the first thing a new engineer reads, and its quality signals the maturity of the project.

A complete README includes:

# Project Name

One sentence describing what this project does and who it is for.

## Prerequisites

- Node.js 22+ (use `.nvmrc` or `volta pin`)
- pnpm 9+
- Docker (for local database)

## Getting Started

\`\`\`bash
git clone git@github.com:org/project.git
cd project
pnpm install
cp .env.example .env
# Edit .env with your local values
pnpm db:migrate
pnpm dev
\`\`\`

Open http://localhost:3000.

## Project Structure

\`\`\`
src/
  modules/        # NestJS feature modules
  common/         # Shared utilities and guards
  config/         # Environment configuration
  prisma/         # Database schema and migrations
tests/
  unit/
  e2e/
\`\`\`

## Available Commands

| Command | Description |
|---------|-------------|
| `pnpm dev` | Start development server with hot reload |
| `pnpm build` | Compile for production |
| `pnpm test` | Run unit tests |
| `pnpm test:e2e` | Run end-to-end tests |
| `pnpm db:migrate` | Apply pending migrations |
| `pnpm db:studio` | Open Prisma Studio (visual DB browser) |

## Environment Variables

See `.env.example` for all required variables with descriptions.

## Architecture

Brief description of the key design decisions. Link to ADRs in `/docs/decisions/` for detailed rationale.

## Contributing

See [CONTRIBUTING.md](./CONTRIBUTING.md) for branch naming, PR process, and coding standards.

What a README should not contain:

  • Comprehensive API reference (put this in code comments or a separate docs directory)
  • Architectural rationale for decisions made months ago (put this in ADRs)
  • Exhaustive troubleshooting guides (put this in runbooks)
  • A history of changes (that is what git log is for)

TSDoc: Documenting TypeScript APIs

TSDoc is the TypeScript equivalent of XML doc comments. It uses /** */ block comments with standardized tags. VS Code, WebStorm, and most TypeScript-aware tools render these as hover documentation.

/**
 * Calculates the total price for an order, including applicable taxes
 * and discounts. Returns `0` if the order has no line items.
 *
 * @param order - The order to calculate. Must have at least one line item.
 * @param options - Optional calculation parameters.
 * @param options.applyDiscount - When `true`, applies the customer's loyalty discount.
 * @returns The total price in USD, rounded to two decimal places.
 * @throws {@link OrderValidationError} When the order fails validation.
 *
 * @example
 * ```typescript
 * const total = calculateOrderTotal(order, { applyDiscount: true });
 * console.log(total); // 42.99
 * ```
 */
export function calculateOrderTotal(
  order: Order,
  options: { applyDiscount?: boolean } = {}
): number {
  // ...
}

TSDoc differs from XML docs in important ways:

  • Tags use @param name - description instead of <param name="x">description</param>
  • The @example tag takes a fenced code block, not a <code> element
  • There is no equivalent to <exception> — use @throws with a type reference
  • @internal marks a symbol as not part of the public API (tools can strip it)
  • @deprecated marks a symbol with a reason and replacement

TSDoc is enforced by ESLint with eslint-plugin-tsdoc:

pnpm add -D eslint-plugin-tsdoc
// eslint.config.js
import tsdoc from 'eslint-plugin-tsdoc';

export default [
  {
    plugins: { tsdoc },
    rules: {
      'tsdoc/syntax': 'warn',
    },
  },
];

When to write TSDoc:

  • All exported functions, classes, and interfaces in a library or shared module
  • All public methods on NestJS services that other services call
  • Anything with non-obvious parameters, side effects, or error conditions
  • Public API endpoints (supplement Swagger, do not replace it)

When to skip TSDoc:

  • Private implementation details that are not part of any API surface
  • Simple one-line functions where the name and types are self-documenting
  • Internal helpers used only within the same file

Swagger / OpenAPI for API Reference

NestJS generates Swagger documentation from decorators. This is the authoritative API reference for anyone calling your backend.

// main.ts
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

const config = new DocumentBuilder()
  .setTitle('Orders API')
  .setDescription('Order management API for the checkout system')
  .setVersion('1.0')
  .addBearerAuth()
  .build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs', app, document);
// In your controller and DTOs
import { ApiOperation, ApiResponse, ApiTags, ApiProperty } from '@nestjs/swagger';

export class CreateOrderDto {
  @ApiProperty({ description: 'Product ID from the catalog', example: 'prod_123' })
  productId: string;

  @ApiProperty({ description: 'Quantity to order', minimum: 1, maximum: 100 })
  quantity: number;
}

@ApiTags('orders')
@Controller('orders')
export class OrdersController {
  @Post()
  @ApiOperation({ summary: 'Place a new order' })
  @ApiResponse({ status: 201, description: 'Order created', type: OrderResponseDto })
  @ApiResponse({ status: 400, description: 'Invalid order data' })
  @ApiResponse({ status: 401, description: 'Not authenticated' })
  async createOrder(@Body() dto: CreateOrderDto) { ... }
}

Swagger UI is available at /api/docs in development and staging. In production, either disable it or protect it behind authentication — it is a complete map of your API surface.

Architecture Decision Records (ADRs)

An ADR documents a significant architectural decision: why you made it, what alternatives you considered, and what the trade-offs are. ADRs are the institutional memory of a project — they answer the question “why is it done this way?” without requiring anyone to be in the room when the decision was made.

File location and naming:

docs/
  decisions/
    0001-use-prisma-over-typeorm.md
    0002-use-nestjs-for-api-layer.md
    0003-postgres-as-primary-database.md
    0004-use-pnpm-workspaces-for-monorepo.md

ADR template:

# ADR 0004: Use pnpm Workspaces for Monorepo

**Date:** 2026-01-15
**Status:** Accepted
**Deciders:** Chris Therriault, [team members]

## Context

We are building a project with a NestJS API, a Next.js frontend, and shared
TypeScript types. We need a strategy for managing these three packages in a
single repository with shared dependencies.

## Decision

We will use pnpm workspaces to manage the monorepo. The workspace root will
contain a `pnpm-workspace.yaml` defining the packages. Shared types live in
`packages/types`. Build orchestration uses Turborepo.

## Alternatives Considered

**npm workspaces** — Available since npm 7. Rejected because pnpm's symlink
strategy avoids phantom dependencies and the store reduces disk usage.

**Nx** — More powerful build graph and code generation. Rejected because the
learning curve and configuration complexity exceeds our needs for 3 packages.
Revisit if the monorepo grows beyond 10 packages.

**Separate repositories** — Simpler per-repo tooling. Rejected because shared
type changes would require coordinated cross-repo PRs.

## Consequences

**Positive:**
- Single `pnpm install` installs all package dependencies
- TypeScript path aliases work across packages without publishing to npm
- Shared CI/CD pipeline configuration

**Negative:**
- pnpm-specific workspace syntax (not portable to npm/yarn without changes)
- Turborepo adds configuration complexity

## References

- [pnpm Workspaces documentation](https://pnpm.io/workspaces)
- [Turborepo documentation](https://turbo.build/repo/docs)

ADR rules:

  • Write the ADR at the time of the decision, not retrospectively
  • Status values: Proposed, Accepted, Deprecated, Superseded by ADR-XXXX
  • Supersede, do not delete. A deprecated decision is still history.
  • Keep ADRs short — if you are writing more than 500 words, you are probably documenting implementation details, not a decision

Runbooks

A runbook documents how to respond to a known operational situation: a recurring incident, a deployment procedure, a database maintenance task. Runbooks are for the human on call at 2am who needs to act quickly without reading code.

File location:

docs/
  runbooks/
    restart-background-jobs.md
    rotate-database-credentials.md
    handle-payment-webhook-failures.md
    emergency-rollback.md

Runbook template:

# Runbook: Emergency Rollback on Render

**Trigger:** A deployment causes errors, and the fix cannot be deployed immediately.
**Owner:** On-call engineer
**Time to complete:** ~5 minutes

## Prerequisites

- Access to the Render dashboard
- Access to Sentry to confirm error rates

## Steps

1. Open Render dashboard → [Service Name] → Deploys
2. Find the last known-good deploy (before the incident started)
3. Click "Rollback to this deploy"
4. Wait ~60 seconds for the rollback to complete
5. Verify in Sentry that the error rate drops
6. Post in #incidents Slack channel: "Rolled back [service] to [commit SHA]"
7. Create a post-incident task to fix the root cause before re-deploying

## Verification

After rollback, call `GET /health` and confirm the response is `{"status":"ok"}`.
Check Sentry: the incident's error rate should drop to zero within 2 minutes.

## Notes

Rollback does not revert database migrations. If the deployment included a
migration that is incompatible with the previous code version, a rollback alone
may not be sufficient. Escalate to senior engineer if the health check fails
after rollback.

The docs-as-code Workflow

The practical implication of keeping docs in the repository:

  1. New feature PR includes documentation. The PR that adds a new API endpoint also updates the README if the architecture changes, adds TSDoc to the new service, and adds/updates the relevant runbook if operational behavior changes.

  2. ADRs are reviewed in PRs. An ADR for a significant architectural decision is committed as a PR for team review before it is accepted.

  3. Broken documentation fails CI. Use tools to catch drift:

# .github/workflows/docs.yml
- name: Check for broken markdown links
  uses: gaurav-nelson/github-action-markdown-link-check@v1
  with:
    folder-path: './docs'
  1. Old documentation is updated, not appended. Do not write “UPDATE 2026-02-15: this no longer applies” in the middle of a document. Update the document to reflect current reality. Git history preserves the old version.

Key Differences

.NET/Azure ApproachJS/TS Approach
XML doc comments (///)TSDoc (/** */) with @param, @returns, @throws
Sandcastle / DocFX for generated docsTypeDoc or raw TSDoc rendered in IDE
Azure DevOps WikiMarkdown files in docs/ directory in the repo
Confluence for architecture docsADRs in docs/decisions/
Swagger generated from Swashbuckle XMLSwagger generated from @nestjs/swagger decorators
Documentation in a separate systemDocumentation in the repository, reviewed in PRs
Wiki page can be updated without a PRDoc change requires a PR (intentional friction)
<exception cref="T"> for error docs@throws {@link ErrorType} in TSDoc

Gotchas for .NET Engineers

Gotcha 1: Markdown formatting is more precise than it looks. Two spaces at the end of a line create a line break. One blank line creates a paragraph break. Four spaces at the start of a line create a code block. These rules vary slightly between Markdown parsers (CommonMark, GitHub Flavored Markdown, MDX). If your documentation renders incorrectly on GitHub, the cause is almost always whitespace or indentation in the Markdown source. Use a linter (markdownlint) to catch formatting issues before they reach the PR.

Gotcha 2: TSDoc is not JSDoc. You may encounter JSDoc in older codebases: @param {string} name - description. TypeScript projects should use TSDoc instead: @param name - description. The types come from TypeScript annotations, not JSDoc {type} syntax. Mixing them causes confusion and may produce incorrect hover documentation. ESLint with eslint-plugin-tsdoc enforces correct TSDoc syntax.

Gotcha 3: README rot is the most common documentation failure. A README written during initial setup and never updated is worse than no README — it actively misleads new engineers. Getting Started instructions that no longer work, command names that have changed, prerequisites that are out of date. Designate one person per project to own the README, and add “does the README reflect this change?” to your PR checklist. Better: include a README accuracy check as part of your onboarding checklist (Article 8.8), because a new engineer running the Getting Started steps will immediately identify drift.

Gotcha 4: ADRs are written once, not maintained. An ADR documents a decision at a point in time. You do not update an ADR when circumstances change — you write a new ADR that supersedes it. If you update an old ADR to reflect a new decision, you lose the historical record of what was true when the original decision was made and why it was later changed. When a decision is reversed, write Status: Superseded by ADR-0012 in the old ADR and reference the old one in the new one.

Gotcha 5: Swagger in production exposes your API surface publicly. In .NET, Swagger middleware is commonly disabled in production via if (env.IsDevelopment()). In NestJS projects, it is easy to forget this guard. Your Swagger UI at /api/docs is a complete, interactive documentation of every endpoint, parameter type, and response schema. In production, either disable it entirely or protect it with authentication middleware. This is not about security through obscurity — Swagger is genuinely a productivity tool for frontend engineers. It is about not handing attackers a structured attack surface map.


Hands-On Exercise

Create the complete documentation structure for a new NestJS project.

Part 1: README

  1. Start a new NestJS project or use an existing one.
  2. Write a README.md using the template above. Fill in every section for your actual project — do not use placeholder text.
  3. Verify: a new engineer who has never seen the project can follow the “Getting Started” section and get the application running without additional help.
  4. Add markdownlint to the project (pnpm add -D markdownlint-cli2) and add a lint check to package.json:
    "lint:docs": "markdownlint-cli2 '**/*.md' '#node_modules'"
    

Part 2: TSDoc

  1. Find three functions in the project that are exported and have non-obvious behavior.
  2. Write full TSDoc for each: @param, @returns, @throws (if applicable), and at least one @example.
  3. Install eslint-plugin-tsdoc and configure it. Fix any TSDoc syntax warnings.
  4. Hover over the documented functions in VS Code and verify the hover tooltip renders the documentation correctly.

Part 3: ADR

  1. Write an ADR for one significant decision already made in the project (which ORM you chose, which auth provider, which hosting platform). Use the ADR template above.
  2. Create docs/decisions/0001-your-decision.md.
  3. Have a colleague or AI reviewer check that the ADR answers: what was the decision, what alternatives were considered, and what are the consequences?

Part 4: Runbook

  1. Write a runbook for the most likely operational scenario in your project: “what do you do if the application returns 500 errors and you need to roll back?”
  2. Include specific steps with exact UI element names or CLI commands.
  3. Have someone unfamiliar with the infrastructure follow the runbook in staging and identify any steps that are unclear or missing.

Quick Reference

Documentation Locations

Document typeLocationTool
Project overview and setupREADME.md (root)Markdown
API code documentationInline TSDoc on exported symbolsTSDoc / eslint-plugin-tsdoc
REST API referenceAuto-generated at /api/docs@nestjs/swagger
Architectural decisionsdocs/decisions/NNNN-slug.mdMarkdown (ADR format)
Operational runbooksdocs/runbooks/slug.mdMarkdown
Component/function examplesdocs/examples/ or inline @exampleTSDoc / Storybook
ChangelogCHANGELOG.mdConventional Commits + changelogen
Contributing guideCONTRIBUTING.mdMarkdown

TSDoc Cheat Sheet

/**
 * Short summary sentence (appears in hover tooltip).
 *
 * Optional longer description. Can span multiple paragraphs.
 * Use code formatting: `variableName` or code blocks below.
 *
 * @param paramName - Description. Note the dash after the name.
 * @param options - Options object.
 * @param options.fieldName - Individual option field.
 * @returns What the function returns. Describe the shape for complex types.
 * @throws {@link ErrorClassName} When this error is thrown and why.
 * @deprecated Use {@link newFunction} instead. Removed in v3.0.
 * @internal Not part of the public API. Subject to change without notice.
 * @see {@link relatedFunction} for a related operation.
 *
 * @example
 * ```typescript
 * const result = myFunction('input', { option: true });
 * // result: 'expected output'
 * ```
 */

ADR Statuses

StatusMeaning
ProposedUnder discussion, not yet committed
AcceptedDecision has been made and implemented
DeprecatedNo longer relevant (technology was removed)
Superseded by ADR-XXXXReplaced by a newer decision

Our Project Templates

New project documentation checklist:

  • README.md with all sections filled in (Prerequisites, Getting Started, Commands, Environment Variables, Architecture)
  • .env.example with every variable listed and documented inline
  • docs/decisions/0001-*.md for the primary technology choices (why this framework, why this database, why this hosting)
  • docs/runbooks/emergency-rollback.md before the first production deployment
  • TSDoc on all exported service methods and utility functions
  • Swagger configured and accessible at /api/docs in development and staging
  • markdownlint-cli2 in dev dependencies and in CI

PR checklist additions:

  • Does the README reflect any changes to setup, commands, or environment variables?
  • Are new exported functions documented with TSDoc?
  • Does this decision warrant an ADR?
  • Is there a runbook to add or update?

Further Reading