← Back to FastAPI
Lesson 1.7 · Getting Started

The Interactive Docs

Turn the automatic docs into clear docs: a title and description, groups (tags), summaries, docstrings, field examples, documented errors and deprecated endpoints. Then use "Try it out" in a real browser - and learn the safe way to hide the docs.

Beginner25 min

What you will be able to do

  • Explain how /docs, /redoc and /openapi.json are built
  • Give the API a title, description and version
  • Group endpoints with tags, and describe them with summaries and docstrings
  • Document fields with descriptions and examples, and errors with responses=
  • Mark endpoints as deprecated, or hide them from the docs
  • Turn the docs off or move them - safely

The idea, in plain English

Since Lesson 1.2 you have had free documentation at /docs. FastAPI builds it from your code: every path, method, parameter and model. But by default it is plain: the title is "FastAPI", the endpoints are in one long list, and errors you raise yourself are missing (Lesson 1.3).

Good docs matter because other people use your API - a frontend developer, a mobile team, another company. If the docs are clear, they do not need to ask you or read your code. And with "Try it out" they can test every endpoint in the browser.

In this lesson we take the to-do API and add everything FastAPI offers for docs, check what it puts into /openapi.json, click through the docs in a real browser, and test the ways to turn the docs off. FastAPI 0.143.0.

Worked example: The to-do API with good docs: grouped endpoints, a documented 404, field examples, a deprecated route and a hidden one - clicked through in Swagger UI.

workflowFrom code to docsstep 1 / 3

1 - FastAPI reads your code

Paths, methods, type hints, models, plus everything you add: title, tags, summaries, docstrings, descriptions, examples.

title
Todo API 1.0.0
tags
todos, health
docstring
-> description
responses=
documents 404

What FastAPI reads from your code, and where it shows up. Real results from our app.

Words you will see in this lesson

A few documentation words.

Small dictionary
OpenAPIA standard JSON format that describes an API. FastAPI writes it to /openapi.json.
Swagger UIThe page at /docs: shows the API and lets you send test requests.
ReDocThe page at /redoc: the same information, in a read-only layout.
TagA group name for endpoints, like "todos". Shown as sections in the docs.
SummaryA short title for one endpoint, shown next to its path.
DeprecatedStill works, but should not be used any more - it will be removed later.

An everyday example: a menu

A restaurant with a good menu needs fewer questions from guests. The menu has a name, sections (starters, mains, drinks), a short name and description for each dish, notes like "contains nuts", and sometimes "no longer available". Some dishes are kept off the menu - the staff meal.

API docs are the menu. The app title is the restaurant name, tags are the sections, summaries and docstrings are dish names and descriptions, examples are photos, deprecated marks old dishes, and include_in_schema=False keeps the staff meal off the menu.

Example 1 - the to-do API with good docs

Read the comments. FastAPI(...) sets the title, a Markdown description, the version, and openapi_tags - the groups, in your order, each with a description. Each route gets tags= (its group) and summary= (a short title). The docstring of get_todo becomes its long description, and Markdown like **bold** and lists work there too.

responses={404: ...} documents the 404 we raise inside the function - FastAPI cannot see it otherwise. Field(description=..., examples=[...]) describes model fields. deprecated=True marks the old endpoint. include_in_schema=False hides /internal/stats from the docs - it still works.

The "-> TodoOut" return types tell FastAPI and the docs what each endpoint returns; Lesson 2.5 explains them properly.

Example 1 - docs_app.py
from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field app = FastAPI( title="Todo API", # the big title in /docs description="A small API to keep a **to-do list**. Built in the FastAPI course.", # Markdown works version="1.0.0", openapi_tags=[ # order and descriptions of the groups {"name": "todos", "description": "Create, read and delete to-dos."}, {"name": "health", "description": "Is the API up?"}, ], ) class TodoIn(BaseModel): title: str = Field(description="What to do", examples=["Buy milk"]) done: bool = Field(default=False, description="Finished yet?") class TodoOut(TodoIn): id: int class ErrorOut(BaseModel): detail: str TODOS = {1: {"id": 1, "title": "Buy milk", "done": False}} @app.get("/todos", tags=["todos"], summary="List all to-dos") def list_todos() -> list[TodoOut]: return list(TODOS.values()) @app.get( "/todos/{todo_id}", tags=["todos"], summary="Get one to-do", responses={404: {"model": ErrorOut, "description": "No to-do with this id"}}, # document the 404 ) def get_todo(todo_id: int) -> TodoOut: """ Return the to-do with this **id**. - Answers **404** if it does not exist. """ if todo_id not in TODOS: raise HTTPException(status_code=404, detail="Todo not found") return TODOS[todo_id] @app.post("/todos", tags=["todos"], status_code=201, summary="Create a to-do", response_description="The new to-do, with its id") def create_todo(todo: TodoIn) -> TodoOut: new_id = max(TODOS) + 1 TODOS[new_id] = {"id": new_id, **todo.model_dump()} return TODOS[new_id] @app.get("/todos-old", tags=["todos"], deprecated=True, summary="Old list endpoint") def old_list(): return list(TODOS.values()) @app.get("/health", tags=["health"]) def health(): return {"status": "ok"} @app.get("/internal/stats", include_in_schema=False) # works, but hidden from the docs def stats(): return {"todos": len(TODOS)}

What went into /openapi.json

Before opening the browser, we read the OpenAPI document from Python with app.openapi(). Every setting is there. The 404 now appears for GET /todos/{todo_id} - in Lesson 1.3 it was missing. /health got the summary "Health", made from the function name, because we did not give one. /internal/stats is not listed, but a direct request still answered.

The field description and example are in the TodoIn schema, and "required": ["title"] shows done is optional. Client developers see all of this without reading Python.

Output - reading app.openapi()
info: {'title': 'Todo API', 'description': 'A small API to keep a **to-do list**. Built in the FastAPI course.', 'version': '1.0.0'} tags: ['todos', 'health'] GET /todos tags=['todos'] summary='List all to-dos' deprecated=False responses=['200'] POST /todos tags=['todos'] summary='Create a to-do' deprecated=False responses=['201', '422'] GET /todos/{todo_id} tags=['todos'] summary='Get one to-do' deprecated=False responses=['200', '404', '422'] GET /todos-old tags=['todos'] summary='Old list endpoint' deprecated=True responses=['200'] GET /health tags=['health'] summary='Health' deprecated=False responses=['200'] description of GET /todos/{todo_id}: 'Return the to-do with this **id**.\n\n- Answers **404** if it does not exist.' 201 description: The new to-do, with its id TodoIn schema: {"properties": {"title": {"type": "string", "title": "Title", "description": "What to do", "examples": ["Buy milk"]}, "done": {"type": "boolean", "title": "Done", "description": "Finished yet?", "default": false}}, "type": "object", "required": ["title"], "title": "TodoIn"} hidden route still works: {'todos': 1} | in docs: False

Try it out - in a real browser

We opened /docs in a real browser (with an automation tool) and did what you would do by hand. The page showed two sections, "todos" and "health", with the descriptions from openapi_tags, and every endpoint with its summary.

Then: open GET /todos/{todo_id}, click "Try it out", type 1 into the todo_id box, click "Execute". Swagger sent a real request to http://127.0.0.1:8140/todos/1 and showed the answer: status 200 and the to-do. With 9 it showed 404. Swagger also shows the curl command for the same request - handy for copying into a terminal.

Output - what the browser saw and did
sections: [ 'todos', 'health' ] operations: [ 'GET /todos List all to-dos', 'POST /todos Create a to-do', 'GET /todos/{todo_id} Get one to-do', 'GET /todos-old Old list endpoint', 'GET /health Health' ] request url: http://127.0.0.1:8140/todos/1 status: 200 | body: { "title": "Buy milk", "done": false, "id": 1 } with 9 -> status: 404 curl shown by Swagger: curl -X 'GET' \ 'http://127.0.0.1:8140/todos/9' \ -H 'accept: */*'

Watch out: "Try it out" sends REAL requests. On a real server, a DELETE from the docs really deletes. Use the docs against a test copy of your data.

Turning the docs off, or moving them

Some teams do not want public docs in production. FastAPI() has three settings for it, and we tested each. docs_url=None and redoc_url=None remove the two pages - but /openapi.json still answered 200: the full description of your API was still public. To hide everything, set openapi_url=None; then all three answered 404.

You can also move the docs: docs_url="/help" served Swagger UI at /help instead of /docs. Hiding docs is not security - anyone can still call your endpoints. Protect endpoints with authentication (Module 6), not by hiding their description.

Example 2 - nodocs.py
from fastapi import FastAPI from fastapi.testclient import TestClient for label, app in [("default", FastAPI()), ("docs_url=None, redoc_url=None", FastAPI(docs_url=None, redoc_url=None)), ("openapi_url=None", FastAPI(openapi_url=None)), ("docs_url='/help'", FastAPI(docs_url="/help"))]: c = TestClient(app) codes = {u: c.get(u).status_code for u in ["/docs", "/redoc", "/openapi.json", "/help"]} print(f"{label:31} {codes}")
Output
default {'/docs': 200, '/redoc': 200, '/openapi.json': 200, '/help': 404} docs_url=None, redoc_url=None {'/docs': 404, '/redoc': 404, '/openapi.json': 200, '/help': 404} openapi_url=None {'/docs': 404, '/redoc': 404, '/openapi.json': 404, '/help': 404} docs_url='/help' {'/docs': 404, '/redoc': 200, '/openapi.json': 200, '/help': 200}

What makes docs good

Write for someone who has never seen your code. A good summary is a short action ("Create a to-do"), not the function name. A good description says what happens in special cases ("Answers 404 if it does not exist"). Examples show real-looking values. Every error a client can get is listed.

Docs checklist
Apptitle, a one-paragraph description, version.
Groupstags on every route; openapi_tags for order and descriptions.
Each endpointA summary, and a docstring for special cases.
FieldsField(description=..., examples=[...]).
Errorsresponses={404: ...} for errors you raise yourself.
Old endpointsdeprecated=True before you remove them.

Docs settings at a glance

App info

Shown at the top of /docs.

FastAPI(title="Todo API", description="...", version="1.0.0")
Groups

Sections, in this order.

openapi_tags=[{"name": "todos", "description": "..."}]
Endpoint text

Group and short title.

@app.get("/todos", tags=["todos"], summary="List all to-dos")
Long description

Markdown docstring.

"""Return the to-do with this **id**."""
Document an error

FastAPI cannot see raises.

responses={404: {"model": ErrorOut, "description": "..."}}
Field text

Description and examples.

Field(description="What to do", examples=["Buy milk"])
Old endpoint

Shown as deprecated.

deprecated=True
Hide one route

Still works.

include_in_schema=False
Hide all docs

Also hides /openapi.json.

FastAPI(openapi_url=None)

Try it yourself

The code does not change. Swap the content string and the program does something else entirely.

Click through

“Run docs_app.py, open /docs, create a to-do with POST /todos and "Try it out". Then read the curl command Swagger shows.”

ReDoc

“Open /redoc. Where do the tag descriptions and the docstring appear?”

Document more

“Add DELETE /todos/{todo_id} with tags, a summary, status_code=204 and a documented 404.”

Production

“Hide the docs only when an environment variable ENV=production is set. (Hint: pass openapi_url=None conditionally.)”

What usually goes wrong

Thinking docs_url=None hides the API description

/openapi.json stayed public (200). Use openapi_url=None to hide everything.

✗ FastAPI(docs_url=None, redoc_url=None)
✓ FastAPI(openapi_url=None)
Undocumented errors

A 404 raised in the function does not appear in the docs unless you add responses=.

Hiding instead of protecting

Hidden endpoints (include_in_schema=False) still answer everyone. Protect them with authentication.

Trying DELETE on real data

"Try it out" sends real requests. Use a test copy.

Key points

  • /docs and /redoc are built from /openapi.json, which FastAPI builds from your code.
  • FastAPI(title, description, version, openapi_tags) describes the whole API.
  • tags, summary, the docstring and response_description describe each endpoint.
  • Field(description, examples) describes fields; responses= documents errors you raise.
  • deprecated=True marks old endpoints; include_in_schema=False hides one (it still works).
  • openapi_url=None hides all docs; docs_url=None alone leaves /openapi.json public.

Quick check before you move on

Where do /docs and /redoc get their information?
From /openapi.json, which FastAPI builds from your code.
How do you make the 404 appear in the docs?
Add responses={404: {...}} to the route decorator.
What does the docstring of an endpoint become?
Its description in the docs (Markdown works).
After docs_url=None and redoc_url=None, is /openapi.json still public?
Yes - it answered 200. Use openapi_url=None.

Interview questions

How does FastAPI generate API documentation?

It builds an OpenAPI 3.1 schema from routes, parameters, Pydantic models, return types and metadata (tags, summaries, docstrings, responses), serves it at /openapi.json, and renders it with Swagger UI and ReDoc.

How do you document error responses in FastAPI?

With the responses parameter on the route, mapping status codes to a model and description, since exceptions raised inside the function are not visible to FastAPI.

Should you disable API docs in production?

It depends. Public APIs benefit from docs; internal ones may hide them with openapi_url=None. Either way, hiding docs is not access control - endpoints must be protected by authentication.

Quiz

  1. 1.

    What summary did /health get, and why?

  2. 2.

    Does a route with include_in_schema=False still work?

  3. 3.

    What did Swagger send when we executed GET /todos/{todo_id} with 1?

  4. 4.

    How do you serve Swagger UI at /help?

Comments

Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.

Loading comments...