Imported from JoseWaldo/monorepo-boilerplate (
AGENTS.md). Install upstream withnpx skills add JoseWaldo/monorepo-boilerplate. Copyright stays with the author.
AGENTS.md — Boilerplate
0. Arquitectura general
Monorepo con arquitectura modular en backend:
apps/backend/src/
config/ — Variables de entorno (Zod), singleton de BD, configuracion de auth
modules/ — Rutas y middlewares organizados por feature (auth, health, ...)
shared/ — Tipos (PaginatedResult, AuthUser), errores, email, utilidades, middlewares
apps/frontend/src/
components/ — Componentes reutilizables (ui/, layout/, shared/)
features/ — Modulos de feature (paginas, hooks, componentes especificos)
routes/ — Definicion de rutas (TanStack Router)
stores/ — Estado global (Jotai atoms)
hooks/ — Hooks compartidos (use-auth)
lib/ — Utilidades (cliente API, utilidades de fecha, auth client)
api/ — Cliente HTTP
- Las rutas se montan bajo un prefijo versionado en
app.ts(/api/v1,/api/v2, etc.) usandoapp.route("/api/v1", featureRoutes). - Organizacion de directorios: los proyectos pueden mantener una estructura plana (
modules/<feature>/) o versionada (modules/v1/<feature>/). El boilerplate usa estructura plana por defecto. - Las rutas solo orquestan: parsean query params/body, llaman a la logica de negocio, devuelven JSON.
- La logica de acceso a datos (repositorios, llamadas a SPs) vive dentro de
modules/o enshared/si es reutilizable, nunca acoplada a las rutas.
1. Estandar de listados paginados, filtrados, buscables y ordenables
Todo listado del sistema usa stored procedures de PostgreSQL con el siguiente contrato unificado.
1.1 Contrato de request
| Parametro | Tipo | Default | Descripcion |
|---|---|---|---|
page |
number |
1 |
Pagina actual (minimo 1) |
limit |
number |
Varia | Tamano de pagina (maximo 100, varia por recurso) |
sortBy |
string? |
Depende | Columna por la que ordenar (whitelist por SP) |
sortDir |
"asc"|"desc" |
"desc" |
Direccion del ordenamiento |
search |
string? |
— | Busqueda por texto (ILIKE sobre columna principal) |
filters.* |
Varia | — | Filtros especificos del recurso |
1.2 Contrato de response
{
"data": [...],
"total": 100,
"page": 1,
"pageSize": 10,
"totalPages": 10
}
Este contrato lo implementa PaginatedResult<T> en shared/types/index.ts y lo serializa cada SP como JSONB.
1.3 Convencion de nombres de stored procedures
sp_list_tbl_<nombre_tabla>
Personalizar por proyecto: definir las SPs concretas aqui.
Parametros estandar que los SP de listado reciben:
p_search TEXT DEFAULT NULL— termino de busquedap_page INT DEFAULT 1p_page_size INT DEFAULT <depende>— default varia por recursop_sort_by TEXT DEFAULT NULL— columna (whitelist dentro del SP)p_sort_dir TEXT DEFAULT 'asc'|'desc'— direccion (whitelist dentro del SP)
Parametros opcionales segun el contexto:
p_user_id TEXT— ID del usuario autenticado (incluir solo si el listado requiere scope por usuario)
Parametros adicionales segun el recurso:
Personalizar por proyecto: listar aqui los parametros extra de cada SP.
Ejemplo:
- sp_list_tbl_{{recurso_a}}: p_type, p_{{filtro_b}}, p_date_from, p_date_to
- sp_list_tbl_{{recurso_b}}: p_status
- sp_list_tbl_{{recurso_c}}: (ninguno extra)
1.4 Seguridad: whitelist de columnas y direcciones
Todo SP implementa whitelist estricta para sortBy y sortDir usando CASE:
v_sort_expr := CASE p_sort_by
WHEN '{{columna_1}}' THEN 't.{{columna_db_1}}'
WHEN '{{columna_2}}' THEN 't.{{columna_db_2}}'
ELSE 't.{{columna_default}}'
END;
v_order_dir := CASE p_sort_dir
WHEN 'asc' THEN 'ASC NULLS LAST'
WHEN 'desc' THEN 'DESC NULLS LAST'
ELSE 'DESC NULLS LAST'
END;
Reglas:
- Nunca interpolar
p_sort_bydirectamente en el SQL (riesgo de SQL injection). - Siempre usar
CASE WHENpara mapear la entrada del usuario a una expresion SQL segura. page_sizemaximo capeado en 100 dentro del SP (IF p_page_size > 100 THEN p_page_size := 100).pageminimo capeado en 1.
1.5 Llamada al SP desde TypeScript (data access layer)
import type { SpListResult, {{Recurso}}ListRow } from "@/shared/types/sp-row-types";
const rows = await this.db.$queryRaw<[{ sp_list_tbl_{{recurso}}: SpListResult<{{Recurso}}ListRow> }]>`
SELECT sp_list_tbl_{{recurso}}(
${filters.userId}::TEXT,
${filters.search || null}::TEXT,
${filters.page}::INT,
${filters.limit}::INT,
${filters.sortBy || null}::TEXT,
${filters.sortDir || null}::TEXT
{{-- parametros adicionales aqui --}}
)
`;
const result = rows[0]?.sp_list_tbl_{{recurso}};
Notas:
- La funcion PostgreSQL se invoca con
SELECT sp_list_...(...). - Prisma devuelve un array de 1 fila con una columna nombrada igual que la funcion.
- El JSONB de PostgreSQL se parsea automaticamente a objeto JS por Prisma.
- Los tipos
SpListResult<T>y los*ListRowestan definidos enshared/types/sp-row-types.ts.
1.6 Checklist: migrar o crear un listado nuevo
Para migrar un listado existente
-
Crear el SP en una migracion SQL nueva (
prisma/schema/migrations/<timestamp>/migration.sql):- Usar
CREATE OR REPLACE FUNCTION sp_list_tbl_<nombre>(...). - Implementar whitelist de
sortByysortDir. - Capear
page_sizea maximo 100. - Retornar
JSONBconjsonb_build_object('data', ..., 'total', ..., 'page', ..., 'pageSize', ..., 'totalPages', ...). - La data debe usar
json_agg(row_to_json(q))con snake_case mapeado a camelCase.
- Usar
-
Definir/actualizar el Row type en
shared/types/sp-row-types.ts:- La interfaz debe coincidir exactamente con las keys camelCase del
json_build_objectdel SP.
- La interfaz debe coincidir exactamente con las keys camelCase del
-
Actualizar el repositorio (
modules/<recurso>/<recurso>.repository.ts):- Reemplazar la logica de
findMany/countcon una llamada$queryRawal SP. - Mapear las rows a entidades de dominio (conversion
Number(),new Date(), etc.). - Eliminar imports de
Prisma.{{Recurso}}WhereInputo similar si ya no se usan.
- Reemplazar la logica de
-
Actualizar el tipo de filtros si se agregaron
sortBy/sortDir:- Agregar
sortBy?: string; sortDir?: "asc" | "desc"a los filtros del repositorio.
- Agregar
-
Actualizar la ruta (
modules/<recurso>/<recurso>.routes.ts):- Parsear query params
sortBy,sortDir. - Pasar a los filtros del repositorio.
- Parsear query params
-
Verificar que no se rompio nada:
- Los filtros existentes (tipo, estado, busqueda, etc.) deben seguir funcionando igual.
- La respuesta mantiene el mismo formato (
data,total,page,limit,totalPages).
Para crear un listado nuevo
- Crear el SP con el estandar de arriba.
- Definir
*ListRowensp-row-types.ts. - Crear repositorio en
modules/<recurso>/. - Crear ruta en
modules/<recurso>/. - Crear DTO de validacion si el recurso acepta POST/PATCH.
- Agregar al
index.tsde rutas.
2. Stack tecnologico
Personalizar por proyecto: reemplazar con el stack real del proyecto.
| Capa | Tecnologia |
|---|---|
| Runtime | {{RUNTIME}} |
| Framework HTTP | {{FRAMEWORK_HTTP}} |
| ORM | {{ORM}} |
| Auth | {{AUTH}} |
| Validacion | {{VALIDACION}} |
| Frontend | {{FRONTEND_FRAMEWORK}} |
| Monorepo | {{MONOREPO_TOOLS}} |
3. Documentacion
Cada vez que se agregue una funcionalidad o se refactorice codigo, actualizar la documentacion correspondiente:
CONTEXT.md— si el cambio agrega/quita historias de usuario, modifica el stack tecnologico, o altera la tabla de rutas API.AGENTS.md— si el cambio introduce una nueva convencion, patron de arquitectura, o checklist que los agentes deben seguir.DESIGN.md— si el cambio modifica tokens de diseno, tipografia, o reglas de layout/componentes.docs/TESTING.md— si el cambio modifica el estandar de testing, cobertura esperada, o convenciones de mocks.
4. Convenciones de base de datos
Para reglas detalladas de modelos, migraciones, naming de tablas/columnas/enums y verificacion automatica, cargar la skill prisma-schema. Resumen rapido:
- Tablas → prefijo
tbl_via@@mapen schema.prisma. Todo modelo debe tener@@map. - Columnas → snake_case via
@mapen campos compuestos (multi-word). Las de una sola palabra no requieren@map. - Enums →
enum_+ lower_snake_case via@@mapen el enum y@mapen cada valor. El modelo Prisma usa PascalCase. - Stored procedures → prefijo
sp_+ snake_case, creados via SQL raw en migraciones - Migraciones →
bun run db:migrate -- --name <nombre>(desdeapps/backend). Prisma genera la migracion automaticamente. Nunca crear o modificar archivos de migracion manualmente, a menos que el usuario lo pida explicitamente. - Tablas maestras → son tablas que almacenan datos de referencia o catalogos (ej. tipos de documento, categorias, estados). Su informacion cambia con poca frecuencia y sirve para estandarizar valores. Toda tabla maestra debe tener su seed en
prisma/seed/tables/(ver flujo-de-trabajo.md para el procedimiento). - Cliente → importar de
@/prisma, nunca de@prisma/client - Verificacion automatica →
node .opencode/skills/prisma-schema/scripts/check-naming.js prisma/schema/schema.prisma
5. Convenciones de codigo
Idioma
Codigo en ingles, documentacion en español. Variables, funciones, tipos, tablas y columnas van en ingles. JSDoc, comentarios inline, commits, tests y strings de UI van en español.
JSDoc
Documentar funciones, clases e interfaces con JSDoc en español:
/**
* Obtiene un usuario por su ID.
*
* @param userId - ID del usuario a buscar.
* @returns El usuario encontrado o null si no existe.
* @throws {NotFoundError} Si el usuario no existe o esta inactivo.
*/
async function getUserById(userId: string): Promise<User | null> {
Tablas
Todas las tablas usan prefijo tbl_ via @@map en Prisma. Ver skill prisma-schema para el detalle completo.
Migraciones
bun run db:migrate -- --name <nombre>(desdeapps/backend) para desarrollo. Prisma genera la migracion automaticamente. Nunca crear o modificar archivos de migracion manualmente, a menos que el usuario lo pida explicitamente.prisma migrate deploy --schema=prisma/schema/schema.prismapara produccion.- Los SPs y cambios de schema que no puede representar Prisma van en SQL raw en el
migration.sql.
Estructura de archivos
- Un archivo de rutas por feature en
modules/<feature>/<feature>.routes.ts. - Middlewares especificos del feature en
modules/<feature>/<feature>.middleware.ts(si aplica). - Tipos, errores, utilidades y middlewares globales reutilizables en
shared/.
6. Referencia: SP de ejemplo
Personalizar por proyecto: agregar la ruta al SP mas completo del proyecto como referencia para nuevos desarrollos.
Ruta: prisma/schema/migrations/{{TIMESTAMP}}/migration.sql
SP: sp_list_tbl_{{recurso}}
Este SP es el mas completo porque incluye:
- Filtro por {{tipo_filtro_1}}
- Filtro por {{tipo_filtro_2}}
- Filtro por {{tipo_filtro_3}}
- Filtro por rango de fechas
- Busqueda por {{campo_busqueda}} (ILIKE)
- Ordenamiento por {{N}} columnas distintas
- JOIN con {{tablas_relacionadas}}
- JSON inline para cada relacion
7. Estandar de filtros en frontend (FilterSheet)
Toda pagina de listado usa el componente FilterSheet (components/ui/filter-sheet.tsx) para agrupar filtros, ordenamiento y busqueda avanzada fuera del area principal de datos.
7.1 Estructura de la pagina
┌─────────────────────────────────────────────────────┐
│ Titulo [+ Nuevo ...] │
├─────────────────────────────────────────────────────┤
│ [🔍 Buscar...] [⚙ Filtros(N)] [✕ Limpiar] │
├─────────────────────────────────────────────────────┤
│ Tabla / Cards / Grid │
│ │
│ (paginacion si aplica) │
└─────────────────────────────────────────────────────┘
7.2 Componente FilterSheet
Sheet lateral que se despliega desde la derecha al hacer clic en el boton "Filtros". Usa createPortal para montarse en document.body.
Props compuestas:
<FilterSheet open={sheetOpen} onClose={() => setSheetOpen(false)}>
<FilterSheet.Header onClose={() => setSheetOpen(false)} />
<FilterSheet.Body>
<FilterSheet.Section label="{{SECCION}}">{/* controles */}</FilterSheet.Section>
</FilterSheet.Body>
<FilterSheet.Footer>
<button>Limpiar filtros</button>
<button>Aplicar</button>
</FilterSheet.Footer>
</FilterSheet>
Animacion:
- Entrada: slide desde la derecha (300ms, cubic-bezier(0.16, 1, 0.3, 1))
- Salida: slide hacia la derecha (300ms) + fade del backdrop (200ms)
- Las transiciones se definen con
style={{ transition: "transform 300ms cubic-bezier(0.16, 1, 0.3, 1)" }}ystyle={{ transition: "opacity 200ms ease-out" }}(NO usar clases de Tailwind paratransition-*— usar el propstyledirectamente). - Desmontaje via
onTransitionEnd+ fallback de 400ms. - Focus trap, Escape para cerrar, clic en backdrop para cerrar.
Ancho: w-[340px] mobile, sm:w-[380px] desktop.
Scroll interno: usar clase scrollbar-thin (definida en globals.css) + fade masks con gradientes from-card to-transparent en bordes superior/inferior del area scrolleable.
7.3 Boton de filtros
<Button
variant="outline"
size="icon"
onClick={() => setSheetOpen(true)}
className="relative shrink-0"
>
<SlidersHorizontal className="h-4 w-4" />
{activeFilterCount > 0 && (
<span className="absolute -right-1 -top-1 flex h-4 min-w-[16px] items-center justify-center rounded-full bg-primary px-1 text-[10px] font-medium text-primary-foreground">
{activeFilterCount}
</span>
)}
</Button>
- Va al lado de la barra de busqueda, no en el header.
- Muestra un badge con el numero de filtros activos.
- El boton de "Limpiar" (
FilterX) aparece a su derecha solo cuando hay filtros activos.
7.4 Secciones del sheet (orden canonico)
Personalizar por proyecto: definir las secciones del FilterSheet segun los recursos del proyecto.
- Tipo / Estado — filtro principal del recurso. Segmented buttons (
border-primary bg-primary/10 text-primarycuando activo). - Periodo / Fecha — solo para recursos con fecha.
- Filtros especificos — filtros secundarios del recurso.
- Ordenar por — botones de columna + direccion (asc/desc).
7.5 Contador de filtros activos
Cada pagina calcula activeFilterCount con useMemo contando cuantos filtros difieren de su valor default. El badge del boton muestra este numero.
7.6 Checklist al crear/migrar un listado
- Importar
FilterSheetySlidersHorizontalde lucide-react - Agregar estado
sheetOpen - Agregar estados
sortBy,sortDir - Calcular
activeFilterCountconuseMemo - Boton
size="icon"al lado de la barra de busqueda - Sheet con
Header,BodyconSections,Footer - Las secciones usan
FilterSheet.Section label="..." - Footer con botones "Limpiar filtros" (outline) y "Aplicar" (primary, cierra el sheet)
- La barra de busqueda y el boton "Limpiar" quedan inline en el CardHeader
- Todos los filtros que estaban inline migran al sheet
- Los hooks del recurso aceptan y pasan
sortBy,sortDir