Crear un módulo
Esta guía explica cómo crear un módulo nuevo desde cero, siguiendo las convenciones reales del proyecto.
Supongamos que queremos añadir un módulo llamado contracts.
1. Backend: estructura básica
Sección titulada «1. Backend: estructura básica»Crea el directorio y los ficheros mínimos:
mkdir backend/app/modules/contractstouch backend/app/modules/contracts/__init__.pytouch backend/app/modules/contracts/api.pytouch backend/app/modules/contracts/models.pytouch backend/app/modules/contracts/schemas.pytouch backend/app/modules/contracts/service.py2. Modelo SQLAlchemy (models.py)
Sección titulada «2. Modelo SQLAlchemy (models.py)»from __future__ import annotations
from sqlalchemy import Stringfrom sqlalchemy.orm import Mapped, mapped_column
from app.core.models import BaseModel
class Contract(BaseModel): __tablename__ = "contracts"
code: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True) name: Mapped[str] = mapped_column(String(200), nullable=False) is_active: Mapped[bool] = mapped_column(default=True, nullable=False)BaseModel ya aporta id (UUID), created_at y updated_at.
3. Esquemas Pydantic (schemas.py)
Sección titulada «3. Esquemas Pydantic (schemas.py)»from uuid import UUIDfrom pydantic import BaseModel
class ContractBase(BaseModel): code: str name: str is_active: bool = True
class ContractCreate(ContractBase): pass
class ContractUpdate(BaseModel): code: str | None = None name: str | None = None is_active: bool | None = None
class ContractResponse(ContractBase): id: UUID
class Config: from_attributes = True4. Servicio (service.py)
Sección titulada «4. Servicio (service.py)»from sqlalchemy.ext.asyncio import AsyncSessionfrom app.modules.contracts.models import Contractfrom app.modules.contracts.schemas import ContractCreate, ContractUpdate
class ContractService: def __init__(self, db: AsyncSession): self.db = db
async def create(self, body: ContractCreate) -> Contract: contract = Contract(**body.model_dump()) self.db.add(contract) await self.db.flush() return contract5. API (api.py)
Sección titulada «5. API (api.py)»from uuid import UUIDfrom fastapi import APIRouter, Dependsfrom sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth.dependencies import get_current_userfrom app.core.auth.models import Userfrom app.core.database import get_dbfrom app.modules.contracts.schemas import ContractCreate, ContractResponse, ContractUpdatefrom app.modules.contracts.service import ContractService
router = APIRouter(prefix="", tags=["contracts"])
@router.get("", response_model=dict)async def list_contracts( skip: int = 0, limit: int = 50, db: AsyncSession = Depends(get_db), _current_user: User = Depends(get_current_user),): service = ContractService(db) items, total = await service.list(skip=skip, limit=limit) return { "data": [ContractResponse.model_validate(c) for c in items], "total": total, "skip": skip, "limit": limit, }
@router.post("", response_model=ContractResponse, status_code=201)async def create_contract( body: ContractCreate, db: AsyncSession = Depends(get_db), _current_user: User = Depends(get_current_user),): service = ContractService(db) contract = await service.create(body) await db.commit() return contractImportante: el prefix del APIRouter debe ser ''. El module_loader se encargará de montar el módulo en /api/contracts.
6. Migración Alembic
Sección titulada «6. Migración Alembic»Genera y aplica la migración:
cd backendalembic revision --autogenerate -m "add contracts table"alembic upgrade head7. Frontend: vista y ruta
Sección titulada «7. Frontend: vista y ruta»Crea la vista:
mkdir frontend/src/modules/contractstouch frontend/src/modules/contracts/ContractList.vueAñade la ruta en frontend/src/router/index.ts dentro de MainLayout:
{ path: 'contracts', name: 'contracts', component: () => import('@/modules/contracts/ContractList.vue'), meta: { module: 'contracts' },},8. Frontend: llamada a la API
Sección titulada «8. Frontend: llamada a la API»Desde la vista usa apiClient (baseURL /api):
import apiClient from '@/core/api/client'
const response = await apiClient.get('/contracts?limit=50')const contracts = response.data.dataNunca incluyas /api dos veces (apiClient.get('/api/contracts') duplicaría el prefijo).
9. Tour, guía e i18n obligatorios
Sección titulada «9. Tour, guía e i18n obligatorios»Como la nueva vista es hija de MainLayout, debes crear:
- Tour:
frontend/src/core/tours/contracts.ts
import type { TourStep } from '@/core/types/tour'
export const contractsTour: TourStep[] = [ { element: '[data-tour="contracts.header"]', popover: { titleKey: 'tour.contracts.s1.title', descKey: 'tour.contracts.s1.desc', side: 'bottom', align: 'start' }, },]- Registry: añade la entrada en
frontend/src/core/tours/registry.ts:
{ routeName: 'contracts', labelKey: 'tour.idx.contracts', module: 'contracts', icon: 'pi pi-file' }-
Guía: añade una sección en
frontend/src/core/guide/guide-content.ts. -
i18n: añade las claves en
frontend/src/locales/es.jsonyen.json:
{ "tour": { "contracts": { "s1": { "title": "Contratos", "desc": "Gestiona tus contratos activos." } } }, "guide": { "contracts": { "title": "Contratos", "description": "Crea y gestiona contratos con clientes y proveedores." } }}- Añade atributos
data-tour="contracts.header",data-tour="contracts.new-button", etc., en la vista. Los tours nunca usan selectores CSS directamente.
10. Verificación
Sección titulada «10. Verificación»Antes de dar por terminado el módulo:
- Lanza el backend y comprueba que Swagger lista
/api/contracts. - Lanza el frontend y navega a
/contracts. - Ejecuta
vue-tsc --noEmitenfrontend/. - Comprueba que no hay warnings de tour faltante en consola.
Convenciones rápidas
Sección titulada «Convenciones rápidas»| Regla | Correcto | Incorrecto |
|---|---|---|
| Prefijo router backend | prefix="" |
prefix="/contracts" |
| Llamada frontend | apiClient.get('/contracts') |
apiClient.get('/api/contracts') |
| Componente PrimeVue | import Select from 'primevue/select' |
import Dropdown from 'primevue/dropdown' |
| Toggle | ToggleSwitch |
InputSwitch |
| Tabs | Tabs/TabList/Tab |
TabView |
Con estos pasos el módulo queda integrado, documentado y listo para usar.