Docs and OpenAPI endpoints
Three endpoints exist on a default FastAPI() and all three return 200 with no
configuration:
| Constructor parameter | Default |
|---|---|
docs_url |
'/docs' |
redoc_url |
'/redoc' |
openapi_url |
'/openapi.json' |
swagger_ui_oauth2_redirect_url |
'/docs/oauth2-redirect' |
The interactive pages get the attention, but /openapi.json is the one that matters.
Turning off /docs and /redoc while leaving the schema up removes the human
interface and keeps the machine-readable one — which is the wrong half to keep.
What the schema contains
Section titled “What the schema contains”Every path and method. Every path, query and header parameter, with names and types. Every request and response model, with all their field names. The security schemes you declared, which names your auth mechanism and where the token endpoint is. And your route descriptions and docstrings, which are written for colleagues and read accordingly.
That is a reconnaissance document produced automatically and kept current by construction. It does not disclose data and it is not a vulnerability — it removes the guesswork from finding the endpoints that might be.
Because the schema is generated from your models, it also publishes field names you may not have intended to expose. That overlaps with response_model, where the same schema is the verification tool.
openapi_urloptionThe machine-readable schema, and the one that matters most — it is the complete map of every route, parameter and model. Setting it to None also takes down /docs and /redoc, because both are rendered from it; disabling only the two UIs leaves this served.
- accepts
"/openapi.json"|None (disables the schema and both UIs)- default
"/openapi.json"- set in
FastAPI(...)
docs_urloptionSwagger UI. On by default, at a path anyone would guess first.
- accepts
"/docs"|None (disables)- default
"/docs"- set in
FastAPI(...)
redoc_urloptionThe second documentation UI, and the one people forget — disabling docs_url alone leaves this one serving the same information.
- accepts
"/redoc"|None (disables)- default
"/redoc"- set in
FastAPI(...)
swagger_ui_oauth2_redirect_urloptionThe OAuth2 callback Swagger UI uses. A third default route under /docs, worth knowing when you are enumerating what the app exposes rather than assuming two URLs.
- accepts
"/docs/oauth2-redirect"|None- default
"/docs/oauth2-redirect"- set in
FastAPI(...)
import osfrom fastapi import FastAPI
IS_PROD = os.getenv("ENV") == "production"
app = FastAPI( docs_url=None if IS_PROD else "/docs", redoc_url=None if IS_PROD else "/redoc", openapi_url=None if IS_PROD else "/openapi.json",)None removes the route rather than protecting it. Setting openapi_url=None also
disables /docs and /redoc, since both fetch the schema — so if you only change one
line, make it that one.
for p in /docs /redoc /openapi.json /docs/oauth2-redirect; do printf '%-24s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://api.example.com$p)"done404 on all four is the goal. A 200 on /openapi.json with 404 on /docs is the
half-configured case, and it is the common one.
Keeping them, behind auth
Section titled “Keeping them, behind auth”Internal APIs often want the docs. Serving them behind a dependency is reasonable — disable the built-ins and re-add them guarded:
from fastapi.openapi.docs import get_swagger_ui_htmlfrom fastapi.openapi.utils import get_openapi
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
@app.get("/internal/openapi.json", include_in_schema=False)def schema(user=Depends(require_staff)): return get_openapi(title=app.title, version=app.version, routes=app.routes)
@app.get("/internal/docs", include_in_schema=False)def docs(user=Depends(require_staff)): return get_swagger_ui_html(openapi_url="/internal/openapi.json", title="docs")include_in_schema=False keeps these two out of the document they serve. Note this is
worth doing for its own sake: an endpoint marked that way is absent from the schema but
still routable, so it is a documentation choice and not an access control. Anything
genuinely sensitive needs the dependency, which is why require_staff is on both
routes above and not just the UI one.
Moving them is not hiding them
Section titled “Moving them is not hiding them”Changing docs_url to /internal-docs-8f3a is a common half-measure. It raises the
cost of finding the path and nothing else — the URL appears in browser history, in
proxy access logs, in referrer headers, and in any screenshot anyone pastes into chat.
Obscurity here is a speed bump, and it is worth being honest that this control is mostly about reducing the free reconnaissance rather than closing an exposure. That is why it is rated medium: on its own, a published schema harms nothing.
Disabling the schema breaks anything that consumes it, and those consumers are easy to forget because they are not your front end: generated client SDKs, contract tests, API gateway definitions imported from the spec, Postman collections synced from the URL, and internal developer portals.
A generation step that suddenly fetches a 404 usually fails loudly, which is the good case. A contract test that silently skips when it cannot reach the schema is the bad one — check that yours fails rather than passes when the schema is gone.
The workable pattern is to generate the spec at build time and publish it as an artefact, so the file continues to exist for tooling while the running application stops serving it:
python3 -c 'import json; from app.main import app; print(json.dumps(app.openapi()))' > openapi.jsonThat runs in CI, needs no live endpoint, and gives you a versioned spec per release.
Related
Section titled “Related”- response_model — the schema as an audit tool for what your API can emit
- Debug mode and tracebacks — the other development affordance that ships enabled