Ir al contenido

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.

Crea el directorio y los ficheros mínimos:

Ventana de terminal
mkdir backend/app/modules/contracts
touch backend/app/modules/contracts/__init__.py
touch backend/app/modules/contracts/api.py
touch backend/app/modules/contracts/models.py
touch backend/app/modules/contracts/schemas.py
touch backend/app/modules/contracts/service.py
from __future__ import annotations
from sqlalchemy import String
from 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.

from uuid import UUID
from 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 = True
from sqlalchemy.ext.asyncio import AsyncSession
from app.modules.contracts.models import Contract
from 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 contract
from uuid import UUID
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth.dependencies import get_current_user
from app.core.auth.models import User
from app.core.database import get_db
from app.modules.contracts.schemas import ContractCreate, ContractResponse, ContractUpdate
from 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 contract

Importante: el prefix del APIRouter debe ser ''. El module_loader se encargará de montar el módulo en /api/contracts.

Genera y aplica la migración:

Ventana de terminal
cd backend
alembic revision --autogenerate -m "add contracts table"
alembic upgrade head

Crea la vista:

Ventana de terminal
mkdir frontend/src/modules/contracts
touch frontend/src/modules/contracts/ContractList.vue

Añ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' },
},

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.data

Nunca incluyas /api dos veces (apiClient.get('/api/contracts') duplicaría el prefijo).

Como la nueva vista es hija de MainLayout, debes crear:

  1. 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' },
},
]
  1. Registry: añade la entrada en frontend/src/core/tours/registry.ts:
{ routeName: 'contracts', labelKey: 'tour.idx.contracts', module: 'contracts', icon: 'pi pi-file' }
  1. Guía: añade una sección en frontend/src/core/guide/guide-content.ts.

  2. i18n: añade las claves en frontend/src/locales/es.json y en.json:

{
"tour": {
"contracts": {
"s1": { "title": "Contratos", "desc": "Gestiona tus contratos activos." }
}
},
"guide": {
"contracts": {
"title": "Contratos",
"description": "Crea y gestiona contratos con clientes y proveedores."
}
}
}
  1. Añade atributos data-tour="contracts.header", data-tour="contracts.new-button", etc., en la vista. Los tours nunca usan selectores CSS directamente.

Antes de dar por terminado el módulo:

  1. Lanza el backend y comprueba que Swagger lista /api/contracts.
  2. Lanza el frontend y navega a /contracts.
  3. Ejecuta vue-tsc --noEmit en frontend/.
  4. Comprueba que no hay warnings de tour faltante en consola.
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.