How to Document a FastAPI Backend (Three-Layer Walkthrough)
You built a FastAPI backend. Visit /docs and you already get a Swagger UI listing every endpoint, every request body, every response model. On paper, the docs problem is solved. In practice, nobody integrating against your API knows what a "shared link" is, which endpoints to call first, or what the auth flow actually looks like end to end. Auto-generated reference is necessary. It is not enough.
This walkthrough documents a FastAPI backend using what I call the three-layer FastAPI docs stack: (1) the auto-generated OpenAPI reference FastAPI gives you for free, (2) handwritten conceptual docs covering how the API thinks, and (3) a quickstart plus recipes that get a developer to a working call in under five minutes. By the end you will have a public docs site with all three layers, published under your own domain.
What you will build
A documentation site for a hypothetical notes API called Snapnote. It has three resource groups: users (signup, login, me), notes (CRUD plus search), and shared_links (generate a public link, revoke it, fetch by slug). Authentication is bearer token. The same three-layer pattern applies to any FastAPI project. Swap in your own routers.
Prerequisites
Before starting, confirm you have:
- Python 3.11 or newer (
python --version) - FastAPI 0.110+ installed (
pip show fastapi) - A FastAPI app that runs locally with
fastapi devoruvicorn main:app --reload - Node.js 20+ if you plan to self-host with Docusaurus
- Either a Docsio account (free tier is fine) or a local Docusaurus install
Optional but useful: a GitHub repo for the docs site so it rebuilds on push, and a production URL for the API you can link to from examples. If you do not have the Snapnote example repo handy, the snippets below are copy-pasteable on their own.
Step 1: Confirm the OpenAPI layer works
FastAPI generates an OpenAPI schema from your route signatures, Pydantic models, and docstrings. You usually do not have to do anything to turn it on. Start your app:
fastapi dev main.py
Then open three URLs in your browser:
http://localhost:8000/docs: Swagger UIhttp://localhost:8000/redoc: ReDoc, cleaner for readinghttp://localhost:8000/openapi.json: the raw JSON schema
If all three load and show your endpoints, the reference layer is already live. The rest of this walkthrough assumes they do.
Step 2: Tighten the auto-generated reference
The default output is mediocre because most people never edit the code that produces it. Five small changes make it genuinely useful.
First, set real metadata on the app. This shows up at the top of Swagger and ReDoc:
from fastapi import FastAPI
app = FastAPI(
title="Snapnote API",
summary="Capture, share, and sync notes across devices.",
version="1.2.0",
contact={"name": "Snapnote Support", "url": "https://snapnote.example/support"},
license_info={"name": "MIT"},
openapi_tags=[
{"name": "users", "description": "Signup, login, and profile."},
{"name": "notes", "description": "Create, read, update, delete notes."},
{"name": "shared_links", "description": "Public sharing via signed slug."},
],
)
Second, give every router a tags argument so endpoints cluster by resource instead of by file name:
from fastapi import APIRouter, Depends
from app.deps import current_user
from app.schemas import NoteOut, NoteCreate
router = APIRouter(prefix="/notes", tags=["notes"])
@router.post("", response_model=NoteOut, status_code=201)
def create_note(payload: NoteCreate, user=Depends(current_user)):
"""Create a new note for the current user.
The note is private by default. To share it, call
`POST /shared_links` with this note id and an optional expiry.
"""
return service.create_note(user.id, payload)
Three things happen automatically from that snippet. The NoteCreate Pydantic model becomes the request body schema. NoteOut becomes the response schema. The docstring becomes the endpoint description in Swagger UI and ReDoc.
Third, fill in Pydantic field descriptions and examples. Most people skip this and it is the single biggest reference upgrade:
from pydantic import BaseModel, Field
class NoteCreate(BaseModel):
title: str = Field(..., max_length=200, examples=["Grocery list"])
body: str = Field(..., description="Markdown-formatted note body.")
folder_id: str | None = Field(
None, description="Parent folder. Omit for top-level notes."
)
model_config = {
"json_schema_extra": {
"examples": [{
"title": "Grocery list",
"body": "- Eggs\n- Milk\n- Coffee",
"folder_id": None,
}]
}
}
Fourth, document error responses. Swagger will show them next to the 200:
@router.get("/{note_id}", response_model=NoteOut,
responses={
404: {"description": "Note not found or not owned by current user."},
401: {"description": "Missing or invalid bearer token."},
},
)
def get_note(note_id: str, user=Depends(current_user)):
...
Fifth, export the schema to a file you can version in git. This becomes the artifact your docs site imports:
python -c "from main import app; import json; print(json.dumps(app.openapi()))" > openapi.json
Run that in CI on every merge to main. Now the reference layer is deterministic, reviewable, and ready to hand off to a docs site.
Step 3: Pick the conceptual-docs layer
The reference answers "what endpoints exist." It does not answer "how do I auth," "what happens when a shared link expires," or "what is the rate limit." Those belong in a separate docs site. You have two realistic paths.
Path A: paste your FastAPI site URL into Docsio. Go to docsio.co, start a new project, paste the URL of your deployed FastAPI instance (or a live OpenAPI JSON URL), and the generator creates a branded docs site around your schema. The agent pulls titles, models, and tag groupings straight from /openapi.json, then scaffolds conceptual pages you edit in a live preview. Custom domain and publish are one click on the free tier. Good fit if you want conceptual layer and reference in one site without wiring a build.
Path B: self-host with Docusaurus plus an OpenAPI plugin. Install Docusaurus 3, add docusaurus-plugin-openapi-docs, point it at the openapi.json you exported in step 2. You write conceptual pages as Markdown under docs/, and the plugin injects reference pages generated from the schema. More control. More YAML to babysit. Good fit if you already run a Docusaurus monorepo for other projects.
A third path, Mintlify, exists and produces nice reference pages. It is a paid SaaS starting around $150 a month once you move off the free tier, so I usually only recommend it for teams already committed to its ecosystem. See our documentation platform breakdown for the full comparison.
For the rest of this walkthrough I will show Path A because the setup is shorter. The Path B structure is identical on the content side. Only the tooling differs.
Step 4: Generate the site and map the structure
In Docsio, start a new project, paste your FastAPI production URL, and wait for the onboarding to finish. You get a docs site with a homepage, a reference section scaffolded from your OpenAPI schema, and empty conceptual pages you fill in.
Open the file tree and set up this structure:
docs/quickstart.md: first call in five minutesdocs/concepts/authentication.md: how bearer tokens work in Snapnotedocs/concepts/notes-and-folders.md: the data model, plain Englishdocs/concepts/shared-links.md: slug format, expiry, revocationdocs/guides/import-from-csv.md: one common integration recipedocs/guides/webhooks.md: if you have webhooks, document them heredocs/reference/: auto-generated fromopenapi.json, do not hand-edit
The reference section lives next to the handwritten pages but under its own folder so nobody confuses the two. For the principles behind this split see our post on api reference documentation.
Step 5: Write the quickstart
The quickstart is the single most important page on the site because it is the only one most visitors read. Keep it under one screen. For Snapnote it looks like this:
# Quickstart
Create your first note in under five minutes.
## 1. Get an API key
Sign in at [app.snapnote.example](https://app.snapnote.example), open
Settings -> API Keys, and click **Create key**. Copy the value. It starts
with `sn_live_`.
## 2. Make your first request
```bash
curl -X POST https://api.snapnote.example/notes \
-H "Authorization: Bearer sn_live_..." \
-H "Content-Type: application/json" \
-d '{"title": "Grocery list", "body": "- Eggs\n- Milk"}'
```
You should get back a `201 Created` with the new note id.
## 3. Share it
```bash
curl -X POST https://api.snapnote.example/shared_links \
-H "Authorization: Bearer sn_live_..." \
-H "Content-Type: application/json" \
-d '{"note_id": "note_abc123", "expires_in_days": 7}'
```
The response includes a `url` field. Open it in a browser. The recipient
does not need an account.
Next: read [Authentication](/concepts/authentication) for token scopes,
or jump to the full [API reference](/reference).
Three things matter here. Every snippet is copy-pasteable. The keys are fake but formatted exactly like real ones. Every step ends with a confirmation signal the reader can check.
Step 6: Write the concepts pages
Concepts pages explain how your API thinks. They do not list endpoints. A good test: if removing a concept page would make several reference pages incomprehensible, it belongs. If it just repeats what the reference already says, kill it.
For Snapnote, three concept pages earn their keep:
- Authentication: where tokens come from, what scopes exist (
read,write,admin), how rotation works, what a 401 vs 403 means - Notes and folders: the data model. Why folders are optional. What happens when you delete a folder with notes in it (soft-delete, 30-day restore)
- Shared links: slug format (
sl_prefix plus 22 base58 chars), signing approach (HMAC with rotating secret), expiry rules, revocation behavior
Each page is 300 to 600 words. Each one links to the specific reference endpoints it describes. The reader can always get from concept to code in one click. That round-trip is the whole point.
Step 7: Add one recipe per integration pattern
Recipes are the layer that converts readers into users. Pick three integrations your support inbox already asks about. Write one recipe per pattern, end to end, with real code:
- Importing existing notes from a CSV
- Polling for new notes vs subscribing to webhooks
- Rotating API keys without downtime
A recipe is not reference and it is not a concept. It is a procedure: "You want X, here is the exact sequence of calls and code." Recipes are where docs become genuinely useful. For a broader take on this see documentation automation and our docs-as-code guide.
Step 8: Keep reference and concepts in sync
This is the step most teams skip. Six months later the reference says POST /notes takes title and the concept page still references the deprecated name field. Two practical rules keep drift away:
- The OpenAPI export runs in CI on every merge. If the schema changes, the reference page changes on the next deploy.
- Concept pages cite endpoints by path, not by prose. "See
POST /shared_links" stays accurate even when the slug format changes. "See the sharing endpoint" does not.
Docs-as-code advocates will tell you to put the conceptual Markdown in the same repo as the FastAPI app. That is the gold-standard setup and it works. A lighter version also works: keep the docs in Docsio or a separate repo, and add a CI check on the FastAPI repo that fails the PR if openapi.json changed but no doc-repo PR is linked. The enforcement matters more than the location. The same sync problem and the same fix apply if you are documenting a Supabase backend or any other schema-driven service.
Step 9: Publish and verify
Once the site reads the way you want, hit publish. Point a custom domain (something like docs.snapnote.example) at it. Then do the three-link test:
- From the homepage, can a new reader reach a working API call in two clicks? If not, the quickstart is buried.
- From any reference page, is there a link back to the relevant concept? If not, readers will bounce when they hit unfamiliar terms.
- From the 404 page, is there a search bar or a list of top pages? Broken deep links are normal. Broken 404 pages are not.
Fix those before announcing the site. You only get one chance at first impressions with integrators.
What to do next
You now have the three-layer stack live: an OpenAPI reference generated from the code, concept pages explaining the model, and a quickstart plus recipes. The next moves depend on scale.
If your API has more than thirty endpoints, add full-text search. If you have customers on multiple versions, add doc versioning so v1 and v2 reference pages can coexist. If you want AI assistants to cite your docs accurately, generate a /llms.txt file and serve it from the site root. All three are standard at this point and none of them require rewriting the foundation you just built.
Want to skip the tooling setup entirely and see if the Path A approach fits your API? Paste your FastAPI URL into Docsio and you will have a draft site before you finish your coffee. Free tier includes the custom domain, publish, and the OpenAPI import.
