Skip to content

Commit

Permalink
Add Open API prefix route - correct docs behind reverse proxy (fastap…
Browse files Browse the repository at this point in the history
…i#26)

Add Open API prefix route - correct docs behind reverse proxy.
  • Loading branch information
Kabir Khan authored and tiangolo committed Feb 14, 2019
1 parent 890f1f7 commit 0ea0d0e
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 4 deletions.
Binary file added docs/img/tutorial/sub-applications/image01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/tutorial/sub-applications/image02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions docs/src/sub_applications/tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from fastapi import FastAPI

app = FastAPI()


@app.get("/app")
def read_main():
return {"message": "Hello World from main app"}


subapi = FastAPI(openapi_prefix="/subapi")


@subapi.get("/sub")
def read_sub():
return {"message": "Hello World from sub API"}


app.mount("/subapi", subapi)
95 changes: 95 additions & 0 deletions docs/tutorial/sub-applications-proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
There are at least two situations where you could need to create your **FastAPI** application using some specific paths.

But then you need to set them up to be served with a path prefix.

It could happen if you have a:

* **Proxy** server.
* You are "**mounting**" a FastAPI application inside another FastAPI application (or inside another ASGI application, like Starlette).

## Proxy

Having a proxy in this case means that you could declare a path at `/app`, but then, you could need to add a layer on top (the Proxy) that would put your **FastAPI** application under a path like `/api/v1`.

In this case, the original path `/app` will actually be served at `/api/v1/app`.

Even though your application "thinks" it is serving at `/app`.

And the Proxy could be re-writing the path "on the fly" to keep your application convinced that it is serving at `/app`.

Up to here, everything would work as normally.

But then, when you open the integrated docs, they would expect to get the OpenAPI schema at `/openapi.json`, instead of `/api/v1/openapi.json`.

So, the frontend (that runs in the browser) would try to reach `/openapi.json` and wouldn't be able to get the OpenAPI schema.

So, it's needed that the frontend looks for the OpenAPI schema at `/api/v1/openapi.json`.

And it's also needed that the returned JSON OpenAPI schema has the defined path at `/api/v1/app` (behind the proxy) instead of `/app`.

---

For these cases, you can declare an `openapi_prefix` parameter in your `FastAPI` application.

See the section below, about "mounting", for an example.


## Mounting a **FastAPI** application

"Mounting" means adding a complete "independent" application in a specific path, that then takes care of handling all the sub-paths.

You could want to do this if you have several "independent" applications that you want to separate, having their own independent OpenAPI schema and user interfaces.

### Top-level application

First, create the main, top-level, **FastAPI** application, and its path operations:

```Python hl_lines="3 6 7 8"
{!./src/sub_applications/tutorial001.py!}
```

### Sub-application

Then, create your sub-application, and its path operations.

This sub-application is just another standard FastAPI application, but this is the one that will be "mounted".

When creating the sub-application, use the parameter `openapi_prefix`. In this case, with a prefix of `/subapi`:

```Python hl_lines="11 14 15 16"
{!./src/sub_applications/tutorial001.py!}
```

### Mount the sub-application

In your top-level application, `app`, mount the sub-application, `subapi`.

Here you need to make sure you use the same path that you used for the `openapi_prefix`, in this case, `/subapi`:

```Python hl_lines="11 19"
{!./src/sub_applications/tutorial001.py!}
```

## Check the automatic API docs

Now, run `uvicorn`, if your file is at `main.py`, it would be:

```bash
uvicorn main:app --debug
```

And open the docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.

You will see the automatic API docs for the main app, including only its own paths:

<img src="/img/tutorial/sub-applications/image01.png">


And then, open the docs for the sub-application, at <a href="http://127.0.0.1:8000/subapi/docs" target="_blank">http://127.0.0.1:8000/subapi/docs</a>.

You will see the automatic API docs for the sub-application, including only its own sub-paths, with their correct prefix:

<img src="/img/tutorial/sub-applications/image02.png">


If you try interacting with any of the two user interfaces, they will work, because the browser will be able to talk to the correct path (or sub-path).
9 changes: 7 additions & 2 deletions fastapi/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def __init__(
description: str = "",
version: str = "0.1.0",
openapi_url: Optional[str] = "/openapi.json",
openapi_prefix: str = "",
docs_url: Optional[str] = "/docs",
redoc_url: Optional[str] = "/redoc",
**extra: Dict[str, Any],
Expand All @@ -43,6 +44,7 @@ def __init__(
self.description = description
self.version = version
self.openapi_url = openapi_url
self.openapi_prefix = openapi_prefix.rstrip("/")
self.docs_url = docs_url
self.redoc_url = redoc_url
self.extra = extra
Expand All @@ -66,6 +68,7 @@ def openapi(self) -> Dict:
openapi_version=self.openapi_version,
description=self.description,
routes=self.routes,
openapi_prefix=self.openapi_prefix,
)
return self.openapi_schema

Expand All @@ -80,15 +83,17 @@ def setup(self) -> None:
self.add_route(
self.docs_url,
lambda r: get_swagger_ui_html(
openapi_url=self.openapi_url, title=self.title + " - Swagger UI"
openapi_url=self.openapi_prefix + self.openapi_url,
title=self.title + " - Swagger UI",
),
include_in_schema=False,
)
if self.openapi_url and self.redoc_url:
self.add_route(
self.redoc_url,
lambda r: get_redoc_html(
openapi_url=self.openapi_url, title=self.title + " - ReDoc"
openapi_url=self.openapi_prefix + self.openapi_url,
title=self.title + " - ReDoc",
),
include_in_schema=False,
)
Expand Down
5 changes: 3 additions & 2 deletions fastapi/openapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ def get_openapi(
version: str,
openapi_version: str = "3.0.2",
description: str = None,
routes: Sequence[BaseRoute]
routes: Sequence[BaseRoute],
openapi_prefix: str = ""
) -> Dict:
info = {"title": title, "version": version}
if description:
Expand All @@ -234,7 +235,7 @@ def get_openapi(
if result:
path, security_schemes, path_definitions = result
if path:
paths.setdefault(route.path, {}).update(path)
paths.setdefault(openapi_prefix + route.path, {}).update(path)
if security_schemes:
components.setdefault("securitySchemes", {}).update(
security_schemes
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ nav:
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
- NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
- Bigger Applications - Multiple Files: 'tutorial/bigger-applications.md'
- Sub Applications - Under a Proxy: 'tutorial/sub-applications-proxy.md'
- Application Configuration: 'tutorial/application-configuration.md'
- Extra Starlette options: 'tutorial/extra-starlette.md'
- Concurrency and async / await: 'async.md'
Expand Down
Empty file.
66 changes: 66 additions & 0 deletions tests/test_tutorial/test_sub_applications/test_tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from starlette.testclient import TestClient

from sub_applications.tutorial001 import app

client = TestClient(app)

openapi_schema_main = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/app": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Read Main Get",
"operationId": "read_main_app_get",
}
}
},
}
openapi_schema_sub = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/subapi/sub": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Read Sub Get",
"operationId": "read_sub_sub_get",
}
}
},
}


def test_openapi_schema_main():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema_main


def test_main():
response = client.get("/app")
assert response.status_code == 200
assert response.json() == {"message": "Hello World from main app"}


def test_openapi_schema_sub():
response = client.get("/subapi/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema_sub


def test_sub():
response = client.get("/subapi/sub")
assert response.status_code == 200
assert response.json() == {"message": "Hello World from sub API"}

0 comments on commit 0ea0d0e

Please sign in to comment.