To document code, work from the outside in: start with a README that explains what the project does and how to run it, add inline comments that explain why tricky code exists, write docstrings or doc comments on public functions, and generate reference docs from those comments. Keep the docs next to the code so they update together, then publish them somewhere your team can search. That order, from project overview down to single lines, is how to document code without writing a wall of text nobody reads.
The hard part is not the typing. It is deciding what deserves a comment and what is noise. This guide walks through each layer in order, with short before-and-after examples, so you can document code that stays accurate as the project changes. For a wider view of writing docs beyond source files, see our guide on how to write documentation, and for a curated set of generators, the rundown of code documentation tools.
1. Start with the README
The README is the front door. It lives in the repo root and answers the first questions a new developer or user has: what is this, how do I run it, and where do I go next. A reader who cannot get the project running in five minutes will not read your docstrings.
A useful README covers the project name and one-line purpose, prerequisites, install steps, a minimal usage example, and links to deeper docs. Keep it short. The README is a map, not the territory.
# Payments API
Charge cards and issue refunds over a small REST API.
## Setup
1. `npm install`
2. Copy `.env.example` to `.env` and add your Stripe key
3. `npm run dev`
## Example
POST /charges { "amount": 500, "currency": "usd" }
If you want more patterns to copy, our post on README examples breaks down strong READMEs by project type. Get this file right and everything below has somewhere to point back to.
2. Write meaningful inline comments
Inline comments explain code that the code itself cannot. The mistake most developers make is narrating: a comment that repeats the line below it adds clutter and goes stale the moment the line changes.
Compare these two:
# bad: restates the obvious
i = i + 1 # increment i
# good: explains a non-obvious decision
# Retry once; the upstream API drops the first request after an idle timeout.
response = client.post(url, retries=1)
The second comment earns its place because the reasoning is invisible in the code. Good inline comments flag edge cases, workarounds, performance trade-offs, and links to the ticket or spec that explains a strange choice. If a comment just describes what the next line does, delete it and rename the variable instead.
3. Use docstrings and doc comments
Docstrings sit at the top of a function, class, or module and describe its contract: what it takes, what it returns, and what can go wrong. Unlike loose inline comments, docstrings follow a structured format your tools can read and turn into reference pages.
def charge_card(amount: int, currency: str) -> Charge:
"""Charge a card and return the resulting Charge.
Args:
amount: Amount in the smallest currency unit (cents for USD).
currency: Three-letter ISO currency code.
Returns:
A Charge with status "succeeded" or "pending".
Raises:
CardError: If the card is declined.
"""
Every language has its own convention: Python docstrings, JavaScript and TypeScript use JSDoc/TSDoc, Java uses Javadoc, Go uses doc comments above each declaration. Document the public surface first, the functions and classes other people call, before you spend time on internal helpers. For language-specific patterns, our guide on JavaScript documentation covers JSDoc and TypeScript in depth.
4. Document the why, not the what
This is the rule that separates docs that age well from docs that rot. Code already tells the reader what it does. Your job is to capture why it does it that way, the context that lives in your head and disappears when you switch projects.
The "why" includes business rules, the reason an obvious approach was rejected, assumptions about input, and the trade-off behind a design choice. Six months later, the what will have changed but the why is what saves the next person from re-deriving a decision you already made.
// We round half-up, not banker's rounding, because Finance
// reconciles against the legacy ledger which rounds half-up.
const total = roundHalfUp(subtotal * taxRate);
If you find yourself explaining what a block does, that is usually a sign the code needs a clearer name or a smaller function, not a comment. Self-documenting code, code where names and structure carry the meaning, is the goal. Comments fill the gaps that names cannot.
5. Generate reference docs from your code
Once your functions carry docstrings, you do not have to maintain a separate API reference by hand. Documentation generators read the comments straight from your source and produce browsable reference pages, so the docs stay in sync with the signatures they describe.
Common generators by ecosystem:
- Python: Sphinx (reads docstrings, outputs HTML/PDF)
- JavaScript/TypeScript: JSDoc, TypeDoc
- Java: Javadoc
- Multi-language: Doxygen
- REST APIs: OpenAPI/Swagger from annotations
Wire the generator into your build or CI so the reference rebuilds on every merge. That way a renamed parameter or a new exception shows up in the docs automatically instead of waiting for someone to remember. For broader API guidance, see API documentation best practices, and for a full list of generators, code documentation tools.
6. Keep docs close to the code (docs-as-code)
Documentation that lives in a separate wiki drifts away from the code within weeks. The fix is docs-as-code: store prose docs as Markdown in the same repository, review them in the same pull requests, and version them with the same Git history. A change to behavior and the docs describing it land in one commit.
This is why the README, docstrings, and a /docs folder all belong in the repo. When docs travel with code, reviewers can catch a stale paragraph during code review, and git blame tells you why a sentence exists. We cover the workflow, tooling, and trade-offs in our guide to docs-as-code.
A practical setup looks like this: inline comments and docstrings in source files, a /docs directory of Markdown guides for setup and architecture, and a generated reference built from docstrings on every CI run. All three live behind one git push.
7. Publish docs for your team and users
Markdown in a repo is readable, but a search box and a clean site are what make docs actually get used. The last step is turning your /docs folder and generated reference into a published site people can search, link to, and bookmark.
You can stand up a static site generator yourself, or skip the setup. To turn that into a published, searchable docs site, tools like Docsio generate a branded site from your existing content in minutes, so the Markdown you already wrote becomes a hosted site with search and navigation without a manual build pipeline. Either way, the principle holds: docs nobody can find are docs nobody reads.
Aim to publish on every merge so the live site never lags behind the code. A doc that is one release out of date is worse than no doc, because readers trust it and act on stale information.
Quick checklist
Run through this before you call a project documented:
- README in the repo root: purpose, setup, one usage example, links out
- Inline comments only where the why is not obvious from the code
- Docstrings on every public function, class, and module
- The why captured: business rules, rejected approaches, assumptions
- Reference docs generated from docstrings in CI, not by hand
- Docs in the repo, reviewed in the same pull requests as code
- Published to a searchable site, rebuilt on every merge
- No stale docs: if behavior changes, the docs change in the same commit
When should you document code?
Document as you write, not at the end. Writing the docstring for a function before or while you build it surfaces a muddled interface early, when it is cheap to fix. Documenting after the fact turns into a chore that gets skipped under deadline, and a chore that gets skipped is how codebases end up undocumented.
That said, do not document everything. A private one-line helper with a clear name needs nothing. Spend your effort on the public surface, the tricky decisions, and the setup steps, the places where a missing explanation costs the next person an hour.
Frequently asked questions
How do you document code?
Document code in layers: a README for project-level orientation, inline comments for non-obvious decisions, and docstrings on public functions describing parameters, return values, and errors. Generate reference docs from those docstrings, keep all of it in the same repository as the code, and publish it to a searchable site so the team can find it.
What is the difference between comments and documentation?
Comments are notes inside source files aimed at developers reading the code, explaining why a specific line or block exists. Documentation is the broader set of READMEs, guides, and generated reference pages aimed at anyone using or maintaining the project. Comments live in the code; documentation surrounds it. Strong projects use both together.
Should code be self-documenting instead of commented?
Both, not either. Self-documenting code uses clear names and small functions so the what is obvious without comments, which is the right default. But names cannot explain why a decision was made, what was rejected, or which business rule a calculation enforces. Use clear naming for the what, and reserve comments for the why.
How do I keep documentation from going out of date?
Keep docs in the same repository as the code so they are reviewed in the same pull requests, generate reference pages from docstrings in CI so signatures stay in sync automatically, and publish on every merge. When a change to behavior and the doc describing it land in one commit, docs cannot quietly drift away from the code.
