FastAPI · Documentation · OpenAPI
FastAPI documentation beyond Swagger UI, in one line of Python
What FastAPI's /docs really is, how to change it, and how to serve full documentation plus a request collection from the same OpenAPI document.
Every FastAPI app comes with documentation: start the server, open /docs, and Swagger UI lists your endpoints with a Try it out button on each. It is one of the reasons people pick FastAPI. But the page is a reference, not a guide; it loads about 1.6 MB of JavaScript and CSS; and changing what it says means changing your Python code. This guide explains what /docs actually is, the options FastAPI gives you, and how to add a documentation page and a request collection with the swastik-codes package, keeping Swagger UI or replacing it.
What FastAPI's /docs actually is
FastAPI does two separate things for you.
- It writes an OpenAPI document. From your path operations, their type hints, Pydantic models,
summary,description,tagsandresponses, FastAPI builds an OpenAPI 3.1 document and serves it at/openapi.json. The methodapp.openapi()builds it on the first call and caches it on the app. - It serves a page that reads that document.
/docsreturns a short HTML page that loads Swagger UI's JavaScript and CSS from a CDN and points it at/openapi.json./redocdoes the same with ReDoc.
The second part is replaceable. FastAPI itself shows how: FastAPI(docs_url=None, redoc_url=None) turns both pages off, and fastapi.openapi.docs.get_swagger_ui_html() lets you serve Swagger UI from your own route, for example with the files hosted locally instead of on a CDN. Any page that can read an OpenAPI document can take its place, and that document is where all the real work lives.
What people want that Swagger UI doesn't give
Teams who outgrow the default page usually ask for the same things:
- An introduction and a reading order. How to get a token, which server to use, what errors look like, and the endpoints grouped the way a new developer should read them, not alphabetically by tag.
- Code samples in the languages their users write, not only a cURL line after you have run a request.
- A real request editor: switch between requests without losing what you typed, upload files, see status, time and size, and keep the token for the whole session.
- Edits without a deploy. Fixing a typo in a description shouldn't need a change to a route decorator.
- A lighter page, for people on phones, slow links or a company VPN.
Add Swastik Codes docs to a FastAPI app
Install the package. It has no dependencies of its own:
pip install swastik-codesAdd one line after your routes:
from fastapi import FastAPI
from swastik_codes import mount
app = FastAPI(title="Acme Shop API", version="1.4.0")
@app.get("/products/{product_id}", tags=["Products"], summary="Get a product")
def get_product(product_id: str):
...
mount(app)Start the server as usual (uvicorn main:app --reload or fastapi dev) and open two new pages:
/swc/docs: documentation with an introduction, the base URL, how to authenticate, and every request with its parameters, body fields, responses, an example response and code samples in cURL, JavaScript, Python, Go and PHP./swc/collection: every request in a folder tree. Pick one, fill in path and query parameters, a token, headers and a JSON, form or file body, press Send (orCtrl+Enter) and read the response.
mount() reads FastAPI's own cached app.openapi(), so the pages always match your code. Your /docs and /redoc keep working. If you only want the new pages, create the app with FastAPI(docs_url=None, redoc_url=None); to put them at /docs itself, also pass prefix="":
app = FastAPI(title="Acme Shop API", docs_url=None, redoc_url=None)
# ... routes ...
mount(app, prefix="") # /docs, /collection and /openapi.jsonEdit the pages with swastik-codes.json
Write a starter file next to your app:
python -m swastik_codes initThen point mount() at it:
mount(app, config="swastik-codes.json")Every field is optional; whatever you leave out comes from FastAPI's document. A realistic file:
{
"description": "Products and orders for the Acme store.\n\n## Getting started\n\n1. Exchange your client id and secret for a token with `POST /auth/token`.\n2. Send it as `Authorization: Bearer <token>`.\n3. Use the sandbox until you go live.",
"servers": [
{ "url": "https://sandbox.acme.example", "description": "Sandbox" },
{ "url": "https://api.acme.example", "description": "Production" }
],
"theme": { "accent": "#0d9488" },
"tags": { "Orders": { "description": "Place, read and cancel orders." } },
"groups": [
{ "name": "Start here", "tags": ["Authentication"] },
{ "name": "Store", "tags": ["Products", "Orders"] }
],
"operations": {
"POST /orders": { "description": "Reserves the stock for **15 minutes**." }
},
"hide": ["/internal/*", "/health"],
"pages": [
{ "title": "Errors", "body": "| Status | Meaning |\n|---|---|\n| 401 | Missing or expired token |\n| 422 | The body did not validate |" }
]
}The file is re-read when it changes: save it, reload the page. hide removes requests from both pages and from the served openapi.json, which is handy for health checks and internal endpoints that FastAPI documents by default. Keys are checked as the file loads, and a typo is reported as a warning in your server log rather than failing the app.
Write better FastAPI docs at the source
The pages can only show what the OpenAPI document contains, and FastAPI gives you good tools to fill it in. A few habits make a large difference:
Name every endpoint. Without summary, FastAPI turns the function name into one (place_order becomes "Place Order"), which works but is rarely what you would write. A deliberate summary reads far better in a sidebar.
@app.post("/orders", tags=["Orders"], summary="Place an order", status_code=201)Describe it. A docstring becomes the description, and Markdown works:
def place_order(body: OrderIn):
"""Creates an order and reserves the stock for **15 minutes**.
Pay within that time or the order is cancelled.
"""Give fields examples and descriptions. They become the request body example, the code samples and the table under Request body:
class OrderIn(BaseModel):
email: str = Field(..., examples=["maya@example.com"], description="Where the receipt goes")
coupon: str | None = Field(None, examples=["WELCOME10"])Declare responses. response_model= documents the success body, and responses={404: {"description": "No such order"}} adds the errors your endpoint really returns.
Describe your tags. FastAPI(openapi_tags=[{"name": "Orders", "description": "Place, read and cancel orders."}]), or the tags field in swastik-codes.json.
Declare security. Using HTTPBearer, APIKeyHeader or OAuth2PasswordBearer as a dependency adds a security scheme, so the docs show Requires on each protected request, the code samples include the header, and the collection page asks for the token once.
Behind a proxy, and in production
Under a path prefix. If a proxy serves your app under /api with root_path="/api", the pages work as they are (they use relative links), but requests sent from the collection page need the prefix. List it as a server:
{ "servers": [{ "url": "/api", "description": "This server" }] }Public or private docs. mount() adds public routes, like FastAPI's own /docs. To keep them out of production, mount them only where you want them:
if settings.environment != "production":
mount(app, config="swastik-codes.json")or put the /swc/ paths behind your proxy's authentication.
Cost. The pages are under 1 KB of HTML each. The script and stylesheet (22 KB together, compressed) are cached by browsers for a year, and the merged openapi.json is built once and cached until your code or the JSON file changes. Repeat visits get 304 Not Modified.
Security headers. Both pages are sent with a strict Content-Security-Policy (no inline scripts; scripts and styles from your server only). Tokens a reader types stay in that browser tab and go only to your API.
Swagger UI, ReDoc or Swastik Codes?
| Swagger UI | ReDoc | Swastik Codes | |
|---|---|---|---|
| Comes with FastAPI | Yes, at /docs | Yes, at /redoc | pip install swastik-codes + one line |
| Size (compressed) | about 427 KB | about 232 KB | 22 KB |
| Send requests | Try it out, per operation | No | A collection page, like an API client |
| Code samples | cURL after a run | Through vendor extensions | cURL, JavaScript, Python, Go, PHP |
| Edit text without code changes | No | No | swastik-codes.json |
| Licence | Apache 2.0 | MIT | Free, proprietary |
Sizes are from our measurement on 5 November 2026. The full comparison with Swagger UI also covers where Swagger UI is the better choice, such as OAuth 2 login flows and plugins.
Frequently asked questions
Does it work with Starlette, Flask and Django too?
Yes. Starlette uses the same mount() with spec= set to your document. Flask uses init_flask(app, spec=...) and Django uses urlpatterns += django_urls(spec=...), with the document from apispec, flask-smorest or drf-spectacular. Any other ASGI or WSGI app can be wrapped with SwastikCodesMiddleware or SwastikCodesWSGI.
Do I have to remove Swagger UI?
No. mount() adds new paths and leaves /docs and /redoc alone, so you can compare them side by side and decide later.
Does it slow my app down?
No measurable amount. The package only answers requests under its own prefix; every other request passes straight to your routes. The OpenAPI document is merged once and cached.
Can readers import the API into an API client?
Yes. Both pages have a Download link for openapi.json and a Run in Swastik Codes button that opens the API as a collection in Swastik Codes, where requests can be saved, tested and shared. On localhost the button offers the file instead, because a website can't reach your computer.
Is it open source?
No. The package is free to install and use in development and production, but it is proprietary software from Shwastik Tech Solutions Pvt Ltd, and the Swastik Codes logo and Run in Swastik Codes button stay on the pages.