Skip to content

9. Organización de proyectos FastAPI

📁 9.1 Estructura profesional de carpetas

Section titled “📁 9.1 Estructura profesional de carpetas”
TamañoEstructuraUso
PequeñoUn solo archivoPrototipos, APIs simples
MedianoCarpetas básicasAPIs con varios endpoints
GrandeArquitectura completaAplicaciones empresariales
mi_api/
├── main.py # Punto de entrada
├── requirements.txt # Dependencias
└── README.md # Documentación
mi_api/
├── app/
│ ├── __init__.py
│ ├── main.py # Punto de entrada
│ ├── routers/ # Rutas organizadas
│ │ ├── __init__.py
│ │ ├── usuarios.py
│ │ └── productos.py
│ ├── models/ # Modelos Pydantic
│ │ ├── __init__.py
│ │ ├── usuario.py
│ │ └── producto.py
│ └── config.py # Configuración
├── requirements.txt
└── README.md
mi_api/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── core/ # Configuración central
│ │ ├── __init__.py
│ │ ├── config.py
│ │ └── security.py
│ ├── api/ # Endpoints
│ │ ├── __init__.py
│ │ ├── v1/
│ │ │ ├── __init__.py
│ │ │ ├── endpoints/
│ │ │ │ ├── usuarios.py
│ │ │ │ └── productos.py
│ │ │ └── router.py
│ │ └── deps.py # Dependencias comunes
│ ├── models/ # Modelos Pydantic
│ │ ├── __init__.py
│ │ ├── usuario.py
│ │ └── producto.py
│ ├── schemas/ # Esquemas de BD
│ │ ├── __init__.py
│ │ └── base.py
│ ├── services/ # Lógica de negocio
│ │ ├── __init__.py
│ │ └── usuario_service.py
│ └── db/ # Base de datos
│ ├── __init__.py
│ ├── session.py
│ └── base.py
├── tests/
│ ├── __init__.py
│ └── test_usuarios.py
├── requirements.txt
├── .env
└── README.md

  • Organización: Código más fácil de encontrar
  • Mantenimiento: Cambios aislados por módulo
  • Colaboración: Varios desarrolladores pueden trabajar en paralelo
  • Escalabilidad: Agregar nuevas rutas sin afectar las existentes
app/routers/usuarios.py
# app/routers/usuarios.py
from fastapi import APIRouter, HTTPException
from typing import List
router = APIRouter()
usuarios_db = []
@router.get("/", response_model=List[dict])
def listar_usuarios():
"""Lista todos los usuarios."""
return usuarios_db
@router.get("/{usuario_id}")
def obtener_usuario(usuario_id: int):
"""Obtiene un usuario por ID."""
for usuario in usuarios_db:
if usuario["id"] == usuario_id:
return usuario
raise HTTPException(status_code=404, detail="Usuario no encontrado")
@router.post("/", status_code=201)
def crear_usuario(nombre: str, email: str):
"""Crea un nuevo usuario."""
nuevo = {"id": len(usuarios_db) + 1, "nombre": nombre, "email": email}
usuarios_db.append(nuevo)
return nuevo
app/routers/productos.py
# app/routers/productos.py
from fastapi import APIRouter, HTTPException
from typing import List
router = APIRouter()
productos_db = []
@router.get("/", response_model=List[dict])
def listar_productos():
"""Lista todos los productos."""
return productos_db
@router.get("/{producto_id}")
def obtener_producto(producto_id: int):
"""Obtiene un producto por ID."""
for producto in productos_db:
if producto["id"] == producto_id:
return producto
raise HTTPException(status_code=404, detail="Producto no encontrado")
@router.post("/", status_code=201)
def crear_producto(nombre: str, precio: float):
"""Crea un nuevo producto."""
nuevo = {"id": len(productos_db) + 1, "nombre": nombre, "precio": precio}
productos_db.append(nuevo)
return nuevo

app/models/usuario.py
# app/models/usuario.py
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
from datetime import datetime
class UsuarioBase(BaseModel):
nombre: str = Field(..., min_length=2, max_length=100)
email: EmailStr
class UsuarioCrear(UsuarioBase):
password: str = Field(..., min_length=8)
class UsuarioActualizar(BaseModel):
nombre: Optional[str] = Field(None, min_length=2, max_length=100)
email: Optional[EmailStr] = None
class UsuarioRespuesta(UsuarioBase):
id: int
fecha_registro: datetime
activo: bool = True
class Config:
orm_mode = True
app/models/producto.py
# app/models/producto.py
from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum
class CategoriaEnum(str, Enum):
electronica = "electronica"
ropa = "ropa"
hogar = "hogar"
otros = "otros"
class ProductoBase(BaseModel):
nombre: str = Field(..., min_length=2, max_length=200)
precio: float = Field(..., gt=0)
categoria: CategoriaEnum
class ProductoCrear(ProductoBase):
stock: int = Field(0, ge=0)
class ProductoActualizar(BaseModel):
nombre: Optional[str] = None
precio: Optional[float] = Field(None, gt=0)
categoria: Optional[CategoriaEnum] = None
stock: Optional[int] = Field(None, ge=0)
class ProductoRespuesta(ProductoBase):
id: int
stock: int
activo: bool = True
class Config:
orm_mode = True
app/models/__init__.py
# app/models/__init__.py
from .usuario import UsuarioBase, UsuarioCrear, UsuarioActualizar, UsuarioRespuesta
from .producto import ProductoBase, ProductoCrear, ProductoActualizar, ProductoRespuesta
# Ahora puedes importar así:
# from app.models import UsuarioCrear, ProductoRespuesta

El APIRouter permite agrupar endpoints relacionados y luego incluirlos en la aplicación principal.

CaracterísticaDescripción
PrefijoURL base para todas las rutas
TagsAgrupación en documentación
DependenciasCompartir lógica común
Router de usuarios
# app/routers/usuarios.py
from fastapi import APIRouter
from app.models import UsuarioCrear, UsuarioRespuesta
from typing import List
router = APIRouter(
prefix="/usuarios",
tags=["Usuarios"],
responses={404: {"description": "No encontrado"}}
)
@router.get("/", response_model=List[UsuarioRespuesta])
def listar():
return []
@router.post("/", response_model=UsuarioRespuesta, status_code=201)
def crear(usuario: UsuarioCrear):
return {"id": 1, **usuario.dict()}
Router de productos
# app/routers/productos.py
from fastapi import APIRouter
from app.models import ProductoCrear, ProductoRespuesta
from typing import List
router = APIRouter(
prefix="/productos",
tags=["Productos"]
)
@router.get("/", response_model=List[ProductoRespuesta])
def listar():
return []
@router.post("/", response_model=ProductoRespuesta, status_code=201)
def crear(producto: ProductoCrear):
return {"id": 1, **producto.dict()}
app/main.py
# app/main.py
from fastapi import FastAPI
from app.routers import usuarios, productos
app = FastAPI(
title="Mi API",
description="API organizada con routers",
version="1.0.0"
)
# Incluir routers
app.include_router(usuarios.router)
app.include_router(productos.router)
@app.get("/")
def inicio():
return {"mensaje": "Bienvenido a la API"}
# Endpoints resultantes:
# GET /usuarios
# POST /usuarios
# GET /productos
# POST /productos

🧩 9.5 Modularización de la aplicación

Section titled “🧩 9.5 Modularización de la aplicación”
app/routers/__init__.py
# app/routers/__init__.py
from .usuarios import router as usuarios_router
from .productos import router as productos_router
# Lista de todos los routers para incluir fácilmente
all_routers = [
usuarios_router,
productos_router
]
Incluir routers automáticamente
# app/main.py
from fastapi import FastAPI
from app.routers import all_routers
app = FastAPI(title="Mi API")
# Incluir todos los routers automáticamente
for router in all_routers:
app.include_router(router)
app/core/config.py
# app/core/config.py
from pydantic import BaseSettings
class Settings(BaseSettings):
app_name: str = "Mi API"
debug: bool = False
database_url: str = "sqlite:///./app.db"
secret_key: str = "tu-clave-secreta"
class Config:
env_file = ".env"
settings = Settings()
.env
# .env
APP_NAME=Mi API de Producción
DEBUG=false
DATABASE_URL=postgresql://user:pass@localhost/db
SECRET_KEY=clave-super-secreta-123
Usar configuración
# app/main.py
from fastapi import FastAPI
from app.core.config import settings
app = FastAPI(
title=settings.app_name,
debug=settings.debug
)
@app.get("/config")
def ver_config():
return {
"app_name": settings.app_name,
"debug": settings.debug
}

✅ 9.6 Buenas prácticas en proyectos grandes

Section titled “✅ 9.6 Buenas prácticas en proyectos grandes”
PrincipioDescripción
Separación de responsabilidadesCada módulo hace una cosa
DRYNo repetir código
ConvencionesNombres consistentes
DocumentaciónDocstrings y README
app/main.py
# app/main.py
from fastapi import FastAPI
from app.core.config import settings
from app.api.v1.router import api_router
app = FastAPI(
title=settings.app_name,
description="API profesional con FastAPI",
version="1.0.0",
docs_url="/docs" if settings.debug else None
)
# Incluir API v1
app.include_router(api_router, prefix="/api/v1")
@app.get("/health")
def health_check():
"""Endpoint de salud para verificar que la API está funcionando."""
return {"status": "ok"}
app/api/v1/router.py
# app/api/v1/router.py
from fastapi import APIRouter
from app.api.v1.endpoints import usuarios, productos
api_router = APIRouter()
api_router.include_router(
usuarios.router,
prefix="/usuarios",
tags=["Usuarios"]
)
api_router.include_router(
productos.router,
prefix="/productos",
tags=["Productos"]
)
app/api/v1/endpoints/usuarios.py
# app/api/v1/endpoints/usuarios.py
from fastapi import APIRouter, Depends, HTTPException
from app.models.usuario import UsuarioCrear, UsuarioRespuesta
from app.services.usuario_service import UsuarioService
from typing import List
router = APIRouter()
@router.get("/", response_model=List[UsuarioRespuesta])
def listar_usuarios(service: UsuarioService = Depends()):
"""Lista todos los usuarios registrados."""
return service.listar_todos()
@router.get("/{usuario_id}", response_model=UsuarioRespuesta)
def obtener_usuario(usuario_id: int, service: UsuarioService = Depends()):
"""Obtiene un usuario por su ID."""
usuario = service.obtener_por_id(usuario_id)
if not usuario:
raise HTTPException(status_code=404, detail="Usuario no encontrado")
return usuario
@router.post("/", response_model=UsuarioRespuesta, status_code=201)
def crear_usuario(usuario: UsuarioCrear, service: UsuarioService = Depends()):
"""Crea un nuevo usuario."""
return service.crear(usuario)
app/services/usuario_service.py
# app/services/usuario_service.py
from app.models.usuario import UsuarioCrear, UsuarioRespuesta
from typing import List, Optional
from datetime import datetime
class UsuarioService:
def __init__(self):
self.usuarios_db = []
def listar_todos(self) -> List[dict]:
return self.usuarios_db
def obtener_por_id(self, usuario_id: int) -> Optional[dict]:
for usuario in self.usuarios_db:
if usuario["id"] == usuario_id:
return usuario
return None
def crear(self, usuario: UsuarioCrear) -> dict:
nuevo = {
"id": len(self.usuarios_db) + 1,
"nombre": usuario.nombre,
"email": usuario.email,
"fecha_registro": datetime.now(),
"activo": True
}
self.usuarios_db.append(nuevo)
return nuevo

🐝