Saltar al contenido

Aplicaciones Más Grandes - Múltiples Archivos

Si estás construyendo una aplicación o una API web, rara vez es el caso de que puedas poner todo en un solo archivo.

FastAPI proporciona una herramienta conveniente para estructurar tu aplicación manteniendo toda la flexibilidad.

Nota

Si vienes de Flask, esto sería el equivalente a los Blueprints de Flask.

Una estructura de archivos de ejemplo

Digamos que tienes una estructura de archivos como esta:

.
├── app
│   ├── __init__.py
│   ├── main.py
│   ├── dependencies.py
│   └── routers
│   │   ├── __init__.py
│   │   ├── items.py
│   │   └── users.py
│   └── internal
│       ├── __init__.py
│       └── admin.py

Consejo

Hay varios archivos __init__.py: uno en cada directorio o subdirectorio.

Esto es lo que permite importar código de un archivo a otro.

Por ejemplo, en app/main.py podrías tener una línea como:

from app.routers import items
  • El directorio app contiene todo. Y tiene un archivo vacío app/__init__.py, por lo que es un "Python package" (una colección de "Python modules"): app.
  • Contiene un archivo app/main.py. Como está dentro de un Python package (un directorio con un archivo __init__.py), es un "module" de ese package: app.main.
  • También hay un archivo app/dependencies.py, al igual que app/main.py, es un "module": app.dependencies.
  • Hay un subdirectorio app/routers/ con otro archivo __init__.py, por lo que es un "Python subpackage": app.routers.
  • El archivo app/routers/items.py está dentro de un package, app/routers/, por lo que es un submodule: app.routers.items.
  • Lo mismo con app/routers/users.py, es otro submodule: app.routers.users.
  • También hay un subdirectorio app/internal/ con otro archivo __init__.py, por lo que es otro "Python subpackage": app.internal.
  • Y el archivo app/internal/admin.py es otro submodule: app.internal.admin.

La misma estructura de archivos con comentarios:

.
├── app                  # "app" is a Python package
   ├── __init__.py      # this file makes "app" a "Python package"
   ├── main.py          # "main" module, e.g. import app.main
   ├── dependencies.py  # "dependencies" module, e.g. import app.dependencies
   └── routers          # "routers" is a "Python subpackage"
      ├── __init__.py  # makes "routers" a "Python subpackage"
      ├── items.py     # "items" submodule, e.g. import app.routers.items
      └── users.py     # "users" submodule, e.g. import app.routers.users
   └── internal         # "internal" is a "Python subpackage"
       ├── __init__.py  # makes "internal" a "Python subpackage"
       └── admin.py     # "admin" submodule, e.g. import app.internal.admin

APIRouter

Digamos que el archivo dedicado a manejar solo users es el submodule en /app/routers/users.py.

Quieres tener las operaciones de ruta relacionadas con tus users separadas del resto del código, para mantenerlo organizado.

Pero sigue siendo parte de la misma aplicación/API web de FastAPI (es parte del mismo "Python Package").

Puedes crear las operaciones de ruta para ese módulo usando APIRouter.

Importar APIRouter

Lo importas y creas una "instancia" de la misma manera que lo harías con la clase FastAPI:

app/routers/users.py
from fastapi import APIRouter

router = APIRouter()


@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


@router.get("/users/me", tags=["users"])
async def read_user_me():
    return {"username": "fakecurrentuser"}


@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}

Operaciones de ruta con APIRouter

Y luego lo usas para declarar tus operaciones de ruta.

Úsalo de la misma manera que usarías la clase FastAPI:

app/routers/users.py
from fastapi import APIRouter

router = APIRouter()


@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


@router.get("/users/me", tags=["users"])
async def read_user_me():
    return {"username": "fakecurrentuser"}


@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}

Puedes pensar en APIRouter como una clase "mini FastAPI".

Todas las mismas opciones están soportadas.

Todos los mismos parameters, responses, dependencies, tags, etc.

Consejo

En este ejemplo, la variable se llama router, pero puedes nombrarla como quieras.

Vamos a incluir este APIRouter en la app principal de FastAPI, pero primero, revisemos las dependencias y otro APIRouter.

Dependencias

Vemos que vamos a necesitar algunas dependencias usadas en varios lugares de la aplicación.

Así que los ponemos en su propio módulo de dependencies (app/dependencies.py).

Ahora usaremos una dependencia simple para leer un header personalizado X-Token:

app/dependencies.py
from typing import Annotated

from fastapi import Header, HTTPException


async def get_token_header(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")

Consejo

Estamos usando un header inventado para simplificar este ejemplo.

Pero en casos reales obtendrás mejores resultados usando las Security utilities integradas.

Otro módulo con APIRouter

Digamos que también tienes los endpoints dedicados a manejar "items" de tu aplicación en el módulo en app/routers/items.py.

Tienes operaciones de ruta para:

  • /items/
  • /items/{item_id}

Es toda la misma estructura que con app/routers/users.py.

Pero queremos ser más inteligentes y simplificar el código un poco.

Sabemos que todas las operaciones de ruta en este módulo tienen el mismo:

  • prefix de ruta: /items.
  • tags: (solo un tag: items).
  • responses adicionales.
  • dependencies: todas necesitan esa dependencia X-Token que creamos.

Así que, en lugar de añadir todo eso a cada operación de ruta, podemos añadirlo al APIRouter.

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

Como la ruta de cada operación de ruta tiene que empezar con /, como en:

@router.get("/{item_id}")
async def read_item(item_id: str):
    ...

...el prefix no debe incluir un / final.

Así que, el prefix en este caso es /items.

También podemos añadir una lista de tags y responses adicionales que se aplicarán a todas las operaciones de ruta incluidas en este router.

Y podemos añadir una lista de dependencies que se añadirán a todas las operaciones de ruta en el router y se ejecutarán/resolverán para cada petición hecha a ellas.

Consejo

Ten en cuenta que, al igual que con las dependencies en los path operation decorators, no se pasará ningún valor a tu path operation function.

El resultado final es que las rutas de items ahora son:

  • /items/
  • /items/{item_id}

...como queríamos.

  • They will be marked with a list of tags that contain a single string "items".
    • Estos "tags" son especialmente útiles para los sistemas automáticos de documentación interactiva (usando OpenAPI).
  • Todas incluirán los responses predefinidos.
  • All these path operations will have the list of dependencies evaluated/executed before them.

Consejo

Tener dependencies en el APIRouter puede ser usado, por ejemplo, para requerir autenticación para todo un grupo de operaciones de ruta. Incluso si las dependencies no se añaden individualmente a cada una de ellas.

Consejo

Los parámetros prefix, tags, responses, y dependencies son (como en muchos otros casos) simplemente una característica de FastAPI para ayudarte a evitar la duplicación de código.

Importar las dependencias

Este código vive en el módulo app.routers.items, el archivo app/routers/items.py.

Y necesitamos obtener la función dependency del módulo app.dependencies, el archivo app/dependencies.py.

Así que usamos un import relativo con .. para las dependencies:

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

Cómo funcionan los imports relativos

Consejo

Si sabes perfectamente cómo funcionan los imports, continúa a la siguiente sección más abajo.

Un solo punto ., como en:

from .dependencies import get_token_header

significaría:

  • Empezando en el mismo package en el que vive este módulo (el archivo app/routers/items.py) (el directorio app/routers/)...
  • buscar el módulo dependencies (un archivo imaginario en app/routers/dependencies.py)...
  • y de él, importar la función get_token_header.

Pero ese archivo no existe, nuestras dependencies están en un archivo en app/dependencies.py.

Recuerda cómo se ve nuestra estructura de app/archivos:


Los dos puntos .., como en:

from ..dependencies import get_token_header

significar:

  • Empezando en el mismo package en el que vive este módulo (el archivo app/routers/items.py) (el directorio app/routers/)...
  • ir al package padre (el directorio app/)...
  • y ahí, buscar el módulo dependencies (el archivo en app/dependencies.py)...
  • y de él, importar la función get_token_header.

¡Eso funciona correctamente! 🎉


De la misma manera, si hubiéramos usado tres puntos ..., como en:

from ...dependencies import get_token_header

eso significaría:

  • Empezando en el mismo package en el que vive este módulo (el archivo app/routers/items.py) (el directorio app/routers/)...
  • ir al package padre (el directorio app/)...
  • luego ir al padre de ese package (no hay package padre, app es el nivel superior 😱)...
  • y ahí, buscar el módulo dependencies (el archivo en app/dependencies.py)...
  • y de él, importar la función get_token_header.

Eso se referiría a algún package por encima de app/, con su propio archivo __init__.py, etc. Pero no tenemos eso. Así que, eso lanzaría un error en nuestro ejemplo. 🚨

Pero ahora ya sabes cómo funciona, así que puedes usar imports relativos en tus propias apps no importa qué tan complejas sean. 🤓

Agregar algunos tags, responses y dependencies personalizados

No estamos añadiendo el prefix /items ni los tags=["items"] a cada operación de ruta porque los añadimos al APIRouter.

Pero todavía podemos añadir más tags que se aplicarán a una operación de ruta específica, y también algunos responses adicionales específicos para esa operación de ruta:

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

Consejo

Esta última operación de ruta tendrá la combinación de tags: ["items", "custom"].

Y también tendrá ambos responses en la documentación, uno para 404 y uno para 403.

El FastAPI principal

Ahora, veamos el módulo en app/main.py.

Aquí es donde importas y usas la clase FastAPI.

Este será el archivo principal de tu aplicación que une todo.

Y como la mayor parte de tu lógica ahora vivirá en su propio módulo específico, el archivo principal será bastante simple.

Importar FastAPI

Importas y creas una clase FastAPI como normalmente.

Y podemos incluso declarar dependencies globales que se combinarán con las dependencies de cada APIRouter:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Importar el APIRouter

Ahora importamos los otros submódulos que tienen APIRouters:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Como los archivos app/routers/users.py y app/routers/items.py son submódulos que son parte del mismo Python package app, podemos usar un solo punto . para importarlos usando "imports relativos".

Cómo funciona la importación

La sección:

from .routers import items, users

significa:

  • Empezando en el mismo package en el que vive este módulo (el archivo app/main.py) (el directorio app/)...
  • buscar el subpackage routers (el directorio en app/routers/)...
  • y de él, importar el submodule items (el archivo en app/routers/items.py) y users (el archivo en app/routers/users.py)...

El módulo items tendrá una variable router (items.router). Esta es la misma que creamos en el archivo app/routers/items.py, es un objeto APIRouter.

Y luego hacemos lo mismo para el módulo users.

También podríamos importarlos así:

from app.routers import items, users

Nota

La primera versión es un "import relativo":

from .routers import items, users

La segunda versión es un "import absoluto":

from app.routers import items, users

Para aprender más sobre Python Packages y Modules, lee la documentación oficial de Python sobre Modules.

Evitar colisiones de nombres

Estamos importando el submodule items directamente, en lugar de importar solo su variable router.

Esto es porque también tenemos otra variable llamada router en el submodule users.

Si hubiéramos importado uno después del otro, así:

from .routers.items import router
from .routers.users import router

el router de users sobrescribiría el de items y no podríamos usarlos al mismo tiempo.

Así que, para poder usar ambos en el mismo archivo, importamos los submódulos directamente:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Incluir los APIRouters para users y items

Ahora, incluyamos los routers de los submódulos users y items:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Nota

users.router contiene el APIRouter dentro del archivo app/routers/users.py.

Y items.router contiene el APIRouter dentro del archivo app/routers/items.py.

Con app.include_router() podemos añadir cada APIRouter a la aplicación principal de FastAPI.

Incluirá todas las rutas de ese router como parte de él.

Detalles Técnicos

FastAPI mantiene el APIRouter original y sus APIRoutes activos cuando el router se incluye en la aplicación principal.

Eso significa que las subclases personalizadas de APIRouter y APIRoute pueden seguir participando después de que el router se incluya.

Consejo

No tienes que preocuparte por el rendimiento al incluir routers.

Esto está diseñado para ser ligero y evitar añadir overhead a cada petición.

Así que no afectará el rendimiento. ⚡

Incluir un APIRouter con un prefix, tags, responses y dependencies personalizados

Ahora, imaginemos que tu organización te dio el archivo app/internal/admin.py.

Contiene un APIRouter con algunas operaciones de ruta de admin que tu organización comparte entre varios proyectos.

Para este ejemplo será super simple. Pero digamos que como se comparte con otros proyectos de la organización, no podemos modificarlo y añadir un prefix, dependencies, tags, etc. directamente al APIRouter:

app/internal/admin.py
from fastapi import APIRouter

router = APIRouter()


@router.post("/")
async def update_admin():
    return {"message": "Admin getting schwifty"}

Pero todavía queremos establecer un prefix personalizado al incluir el APIRouter para que todas sus operaciones de ruta empiecen con /admin, queremos asegurararlo con las dependencies que ya tenemos para este proyecto, y queremos incluir tags y responses.

Podemos declarar todo eso sin tener que modificar el APIRouter original pasando esos parámetros a app.include_router():

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

De esa manera, el APIRouter original se quedará sin modificar, así que podemos seguir compartiendo ese mismo archivo app/internal/admin.py con otros proyectos de la organización.

El resultado es que en nuestra app, cada una de las operaciones de ruta del módulo admin tendrá:

  • El prefix /admin.
  • El tag admin.
  • La dependency get_token_header.
  • El response 418. 🍵

Pero eso solo afectará a ese APIRouter en nuestra app, no en ningún otro código que lo use.

Así que, por ejemplo, otros proyectos podrían usar el mismo APIRouter con un método de autenticación diferente.

Incluir una operación de ruta

También podemos añadir operaciones de ruta directamente a la app de FastAPI.

Aquí lo hacemos... solo para mostrar que podemos 🤷:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

y funcionará correctamente, junto con todas las otras operaciones de ruta añadidas con app.include_router().

Detalles Muy Técnicos

Nota: este es un detalle muy técnico que probablemente puedes simplemente saltar.


Los APIRouters no están "montados", no están aislados del resto de la aplicación.

Esto es porque queremos incluir sus operaciones de ruta en el esquema OpenAPI y en las interfaces de usuario.

FastAPI mantiene los routers y las operaciones de ruta originales activos, y combina los prefijos del router, dependencias, tags, responses y otros metadatos al manejar peticiones y generar OpenAPI.

Configurar el entrypoint en pyproject.toml

Como tu objeto app de FastAPI vive en app/main.py, puedes configurar el entrypoint en tu archivo pyproject.toml así:

[tool.fastapi]
entrypoint = "app.main:app"

eso es equivalente a importar así:

from app.main import app

De esa manera el comando fastapi sabrá dónde encontrar tu app.

Nota

También podrías pasar la ruta al comando, así:

$ fastapi dev app/main.py

Pero tendrías que recordar pasar la ruta correcta cada vez que llames al comando fastapi.

Además, es posible que otras herramientas no puedan encontrarlo, por ejemplo la Extensión de VS Code o FastAPI Cloud, por lo que se recomienda usar el entrypoint en pyproject.toml.

Revisar la documentación automática de la API

Ahora, ejecuta tu app:

fast →fastapi dev
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

restart ↻

Y abre la documentación en http://127.0.0.1:8000/docs.

Verás la documentación automática de la API, incluyendo las rutas de todos los submódulos, usando las rutas correctas (y prefijos) y los tags correctos:

Incluir el mismo router múltiples veces con diferentes prefix

También puedes usar .include_router() múltiples veces con el mismo router usando diferentes prefijos.

Esto podría ser útil, por ejemplo, para exponer la misma API bajo diferentes prefijos, ej. /api/v1 y /api/latest.

Esto es un uso avanzado que quizás no necesites realmente, pero está ahí por si lo necesitas.

Incluir un APIRouter en otro

De la misma manera que puedes incluir un APIRouter en una aplicación FastAPI, puedes incluir un APIRouter en otro APIRouter usando:

router.include_router(other_router)

Puedes hacer esto antes o después de incluir router en la app de FastAPI. FastAPI seguirá incluyendo las operaciones de ruta de other_router en el routing y en OpenAPI.

Lo mismo aplica a las operaciones de ruta añadidas posteriormente a los routers. También serán visibles a través de la inclusión anterior.

Detalles Técnicos

Evita mutar directamente router.routes después de incluir un router. FastAPI trata la inclusión del router como en vivo, por lo que el router original y sus rutas permanecen como parte del routing y de la generación de OpenAPI.

Usa APIs documentadas como los path operation decorators y .include_router() para añadir rutas y routers.

Trata router.routes como un árbol de rutas de bajo nivel que puede contener definiciones de rutas y routers incluidos, y evita depender de él como una lista plana de operaciones de ruta finales.