Crear un módulo
This content is not available in your language yet.
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.