diff --git a/docs/img/tutorial/bigger-applications/image01.png b/docs/img/tutorial/bigger-applications/image01.png new file mode 100644 index 000000000..78544b403 Binary files /dev/null and b/docs/img/tutorial/bigger-applications/image01.png differ diff --git a/docs/src/bigger_applications/app/tutorial003.py b/docs/src/bigger_applications/app/main.py similarity index 53% rename from docs/src/bigger_applications/app/tutorial003.py rename to docs/src/bigger_applications/app/main.py index 4c2a348db..4e614a007 100644 --- a/docs/src/bigger_applications/app/tutorial003.py +++ b/docs/src/bigger_applications/app/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI -from .routers.tutorial001 import router as users_router -from .routers.tutorial002 import router as items_router +from .routers.items import router as items_router +from .routers.users import router as users_router app = FastAPI() diff --git a/docs/src/bigger_applications/app/routers/tutorial002.py b/docs/src/bigger_applications/app/routers/items.py similarity index 75% rename from docs/src/bigger_applications/app/routers/tutorial002.py rename to docs/src/bigger_applications/app/routers/items.py index 46a241902..2297e2d27 100644 --- a/docs/src/bigger_applications/app/routers/tutorial002.py +++ b/docs/src/bigger_applications/app/routers/items.py @@ -3,11 +3,11 @@ from fastapi import APIRouter router = APIRouter() -@router.get("/") +@router.get("/", tags=["items"]) async def read_items(): return [{"name": "Item Foo"}, {"name": "item Bar"}] -@router.get("/{item_id}") +@router.get("/{item_id}", tags=["items"]) async def read_item(item_id: str): return {"name": "Fake Specific Item", "item_id": item_id} diff --git a/docs/src/bigger_applications/app/routers/tutorial001.py b/docs/src/bigger_applications/app/routers/users.py similarity index 68% rename from docs/src/bigger_applications/app/routers/tutorial001.py rename to docs/src/bigger_applications/app/routers/users.py index 37e3fbc4a..e88b20cd1 100644 --- a/docs/src/bigger_applications/app/routers/tutorial001.py +++ b/docs/src/bigger_applications/app/routers/users.py @@ -3,16 +3,16 @@ from fastapi import APIRouter router = APIRouter() -@router.get("/users/") +@router.get("/users/", tags=["users"]) async def read_users(): return [{"username": "Foo"}, {"username": "Bar"}] -@router.get("/users/me") +@router.get("/users/me", tags=["users"]) async def read_user_me(): return {"username": "fakecurrentuser"} -@router.get("/users/{username}") +@router.get("/users/{username}", tags=["users"]) async def read_user(username: str): return {"username": username} diff --git a/docs/tutorial/bigger-applications.md b/docs/tutorial/bigger-applications.md index 056dcbbbf..81ebb0440 100644 --- a/docs/tutorial/bigger-applications.md +++ b/docs/tutorial/bigger-applications.md @@ -1,13 +1,284 @@ -Coming soon... +If you are building an application or a web API, it's rarely the case that you can put everything on a single file. + +**FastAPI** provides a convenience tool to structure your application while keeping all the flexibility. + + +## An example file structure + +Let's say you have a file structure like this: + +``` +. +├── app +│   ├── __init__.py +│   ├── main.py +│   └── routers +│   ├── __init__.py +│   ├── items.py +│   └── users.py +``` + +!!! tip + There are two `__init__.py` files: one in each directory or subdirectory. + + This is what allows importing code from one file into another. + + For example, in `app/main.py` you could have a line like: + + ``` + from app.routers import items + ``` + + +* The `app` directory contains everything. +* This `app` directory has an empty file `app/__init__.py`. + * So, the `app` directory is a "Python package" (a collection of "Python modules"). +* The `app` directory also has a `app/main.py` file. + * As it is inside a Python package directory (because there's a file `__init__.py`), it is a "module" of that package: `app.main`. +* There's a subdirectory `app/routers/`. +* The subdirectory `app/routers` also has an empty file `__init__.py`. + * So, it is a "Python subpackage". +* The file `app/routers/items.py` is beside the `app/routers/__init__.py`. + * So, it's a submodule: `app.routers.items`. +* The file `app/routers/users.py` is beside the `app/routers/__init__.py`. + * So, it's a submodule: `app.routers.users`. + + +## `APIRouter` + +Let's say the file dedicated to handling just users is the submodule at `/app/routers/users.py`. + +You want to have the *path operations* related to your users separated from the rest of the code, to keep it organized. + +But it's still part of the same **FastAPI** application/web API (it's part of the same "Python Package"). + +You can create the *path operations* for that module using `APIRouter`. + + +### Import `APIRouter` + +You import it and create an "instance" the same way you would with the class `FastAPI`: + +```Python hl_lines="1 3" +{!./src/bigger_applications/app/routers/users.py!} +``` + + +### Path operations with `APIRouter` + +And then you use it to declare your *path operations*. + +Use it the same way you would use the `FastAPI` class: + +```Python hl_lines="6 11 16" +{!./src/bigger_applications/app/routers/users.py!} +``` + +You can think of `APIRouter` as a "mini `FastAPI`" class. + +All the same options are supported. + +All the same parameters, responses, dependencies, tags, etc. + +!!! tip + In this example, the variable is called `router`, but you can name it however you want. + +We are going to include this `APIrouter` in the main `FastAPI` app, but first, let's add another `APIRouter`. + + +## Another module with `APIRouter` + +Let's say you also have the endpoints dedicated to handling "Items" from your application in the module at `app/routers/items.py`. + +You have path operations for: + +* `/items/` +* `/items/{item_id}` + +It's all the same structure as with `app/routers/users.py`. + +But let's say that this time we are more lazy. + +And we don't want to have to explicitly type `/items/` in every path operation, we can do it later: + +```Python hl_lines="6 11 16" +{!./src/bigger_applications/app/routers/items.py!} +``` + + +## The main `FastAPI` + +Now, let's see the module at `app/main.py`. + +Here's where you import and use the class `FastAPI`. + +This will be the main file in your application that ties everything together. + +### Import `FastAPI` + +You import and create a `FastAPI` class as normally: + +```Python hl_lines="1 6" +{!./src/bigger_applications/app/main.py!} +``` + +### Import the `APIRouter` + +But this time we are not adding path operations directly with the `FastAPI` `app`. + +We import the `APIRouter`s from the other files: + +```Python hl_lines="3 4" +{!./src/bigger_applications/app/main.py!} +``` + +As the file `app/routers/items.py` is part of the same Python package, we can import it using "dot notation". + + +### How the importing works + +The section: ```Python -{!./src/bigger_applications/app/routers/tutorial001.py!} +from .routers.items import router ``` +Means: + +* Starting in the same package that this module (the file `app/main.py`) lives in (the directory `app/`)... +* look for the subpackage `routers` (the directory at `app/routers/`)... +* and from it, the submodule `items` (the file at `app/routers/items.py`)... +* and from that submodule, import the variable `router`. + +The variable `router` is the same one we created in the file `app/routers/items.py`. It's an `APIRouter`. + +We could also import it like: + ```Python -{!./src/bigger_applications/app/routers/tutorial002.py!} +from app.routers.items import router ``` +!!! info + The first version is a "relative import". + + The second version is an "absolute import". + + To learn more about Python Packages and Modules, read the official Python documentation about Modules. + + +### Avoid name collisions + +We are importing a variable named `router` from the submodule `items`. + +But we also have another variable named `router` in the submodule `users`. + +If we import one after the other, like: + ```Python -{!./src/bigger_applications/app/tutorial003.py!} +from .routers.items import router +from .routers.users import router +``` + +The `router` from `users` will overwrite the one form `items` and we won't be able to use them at the same time. + +So, to be able to use both of them in the same file, we rename them while importing them using `as`: + +```Python hl_lines="3 4" +{!./src/bigger_applications/app/main.py!} +``` + + +### Include an `APIRouter` + +Now, let's include the router from the submodule `users`, now in the variable `users_router`: + +```Python hl_lines="8" +{!./src/bigger_applications/app/main.py!} +``` + +With `app.include_router()` we can add an `APIRouter` to the main `FastAPI` application. + +It will include all the routes from that router as part of it. + +!!! note "Technical Details" + It will actually internally create a path operation for each path operation that was declared in the `APIRouter`. + + So, behind the scenes, it will actually work as if everything was the same single app. + + +!!! check + You don't have to worry about performance when including routers. + + This will take microseconds and will only happen at startup. + + So it won't affect performance. + + +### Include an `APIRouter` with a prefix + +Now, let's include the router form the `items` submodule, now in the variable `items_router`. + +But, remember that we were lazy and didn't add `/items/` to all the path operations? + +We can add a prefix to all the path operations using the parameter `prefix` of `app.include_router()`. + +As the path of each path operation has to start with `/`, like in: + +```Python hl_lines="1" +@router.get("/{item_id}", tags=["items"]) +async def read_item(item_id: str): + ... +``` + +...the prefix must not include a final `/`. + +So, the prefix in this case would be `/items`: + +```Python hl_lines="9" +{!./src/bigger_applications/app/main.py!} +``` + +The end result is that the item paths are now: + +* `/items/` +* `/items/{item_id}` + +...as we intended. + +!!! check + The `prefix` parameter is (as in many other cases) just a feature from **FastAPI** to help you avoid code duplication. + + +!!! tip + You could also add path operations directly, for example with: `@app.get(...)`. + + Apart from `app.include_router()`, in the same **FastAPI** app. + + It would still work the same. + + +!!! info "Very Technical Details" + **Note**: this is a very technical detail that you probably can **just skip**. + + --- + + The `APIRouter`s are not "mounted", they are not isolated from the rest of the application. + + This is because we want to include their path operations in the OpenAPI schema and the user interfaces. + + As we cannot just isolate them and "mount" them independently of the rest, the path operations are "cloned" (re-created), not included directly. + + +## Check the automatic API docs + +Now, run `uvicorn`, using the module `app.main` and the variable `app`: + +```bash +uvicorn app.main:app --debug ``` + +And open the docs at http://127.0.0.1:8000/docs. + +You will see the automatic API docs, including the paths from all the submodules: + + diff --git a/tests/test_tutorial/test_bigger_applications/test_tutorial003.py b/tests/test_tutorial/test_bigger_applications/test_main.py similarity index 96% rename from tests/test_tutorial/test_bigger_applications/test_tutorial003.py rename to tests/test_tutorial/test_bigger_applications/test_main.py index 680bc8efe..eb68c4492 100644 --- a/tests/test_tutorial/test_bigger_applications/test_tutorial003.py +++ b/tests/test_tutorial/test_bigger_applications/test_main.py @@ -1,7 +1,7 @@ import pytest from starlette.testclient import TestClient -from bigger_applications.app.tutorial003 import app +from bigger_applications.app.main import app client = TestClient(app) @@ -17,10 +17,24 @@ openapi_schema = { "content": {"application/json": {"schema": {}}}, } }, + "tags": ["users"], "summary": "Read Users Get", "operationId": "read_users_users__get", } }, + "/users/me": { + "get": { + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + } + }, + "tags": ["users"], + "summary": "Read User Me Get", + "operationId": "read_user_me_users_me_get", + } + }, "/users/{username}": { "get": { "responses": { @@ -39,6 +53,7 @@ openapi_schema = { }, }, }, + "tags": ["users"], "summary": "Read User Get", "operationId": "read_user_users__username__get", "parameters": [ @@ -51,18 +66,6 @@ openapi_schema = { ], } }, - "/users/me": { - "get": { - "responses": { - "200": { - "description": "Successful Response", - "content": {"application/json": {"schema": {}}}, - } - }, - "summary": "Read User Me Get", - "operationId": "read_user_me_users_me_get", - } - }, "/items/": { "get": { "responses": { @@ -71,6 +74,7 @@ openapi_schema = { "content": {"application/json": {"schema": {}}}, } }, + "tags": ["items"], "summary": "Read Items Get", "operationId": "read_items_items__get", } @@ -93,6 +97,7 @@ openapi_schema = { }, }, }, + "tags": ["items"], "summary": "Read Item Get", "operationId": "read_item_items__item_id__get", "parameters": [