📦 Guía para Crear Módulos
Visión General
Esta guía te enseñará cómo crear un nuevo módulo siguiendo el patrón estándar de PuntoHack.
Estructura de un Módulo
modules/[domain]/
├── queries.ts # Operaciones de lectura (SELECT)
├── actions.ts # Server Actions (INSERT, UPDATE, DELETE)
├── types.ts # TypeScript interfaces y tipos
└── validations.ts # Zod schemas para validación
Paso 1: Crear Estructura
1.1 Crear Directorio
mkdir -p src/modules/my-module
cd src/modules/my-module
1.2 Crear Archivos Base
touch queries.ts actions.ts types.ts validations.ts
Paso 2: Definir Types
Archivo: types.ts
import { MyModel } from '@prisma/client';
/**
* Tipo extendido con relaciones
*/
export type MyModelWithRelations = MyModel & {
relatedModel: RelatedModel;
_count: {
relatedItems: number;
};
};
/**
* Input para crear
*/
export type CreateMyModelInput = {
name: string;
description?: string;
// ...
};
/**
* Input para actualizar
*/
export type UpdateMyModelInput = Partial<CreateMyModelInput>;
Paso 3: Crear Validations
Archivo: validations.ts
import { z } from 'zod';
/**
* Schema para crear
*/
export const createMyModelSchema = z.object({
name: z.string().min(3, 'Name must be at least 3 characters'),
description: z.string().optional(),
// ...
});
/**
* Schema para actualizar (todos los campos opcionales)
*/
export const updateMyModelSchema = createMyModelSchema.partial();
/**
* Schema para IDs
*/
export const myModelIdSchema = z.object({
id: z.string().cuid(),
});
Paso 4: Crear Queries
Archivo: queries.ts
import { db } from '@/core/db';
import type { MyModelWithRelations } from './types';
/**
* Obtiene un modelo por ID
*/
export async function getMyModelById(id: string): Promise<MyModelWithRelations | null> {
return db.myModel.findUnique({
where: { id },
include: {
relatedModel: true,
_count: {
select: {
relatedItems: true,
},
},
},
});
}
/**
* Lista modelos con filtros opcionales
*/
export async function listMyModels(filters?: {
status?: string;
limit?: number;
}) {
const where: {
status?: string;
} = {};
if (filters?.status) {
where.status = filters.status;
}
return db.myModel.findMany({
where,
take: filters?.limit || 50,
orderBy: { createdAt: 'desc' },
});
}
/**
* Verifica si existe
*/
export async function myModelExists(id: string): Promise<boolean> {
const model = await db.myModel.findUnique({
where: { id },
select: { id: true },
});
return model !== null;
}
Convenciones:
- Nombres descriptivos:
get,list,find,count,exists - Incluir relaciones necesarias
- Ordenar por
createdAt: 'desc'por defecto - Limitar resultados (default 50)
Paso 5: Crear Actions
Archivo: actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { getCurrentUser } from '@/core/auth';
import { requireRole } from '@/core/rbac';
import { db } from '@/core/db';
import { captureError } from '@/core/errors';
import { createMyModelSchema, updateMyModelSchema } from './validations';
/**
* Crea un nuevo modelo
*/
export async function createMyModel(formData: FormData) {
try {
// 1. Auth check
const user = await getCurrentUser();
if (!user) {
throw new Error('Unauthorized');
}
// 2. RBAC check
requireRole(user, ['REQUIRED_ROLE']);
// 3. Validate input
const rawData = {
name: formData.get('name'),
description: formData.get('description'),
};
const validated = createMyModelSchema.parse(rawData);
// 4. Business logic
const model = await db.myModel.create({
data: validated,
});
// 5. Revalidate cache
revalidatePath('/my-models');
// 6. Return result
return { success: true, data: model };
} catch (error) {
captureError(error, { context: 'createMyModel' });
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Actualiza un modelo existente
*/
export async function updateMyModel(id: string, formData: FormData) {
try {
const user = await getCurrentUser();
if (!user) throw new Error('Unauthorized');
requireRole(user, ['REQUIRED_ROLE']);
// Verificar que existe
const existing = await db.myModel.findUnique({
where: { id },
});
if (!existing) {
throw new Error('Model not found');
}
// Validar
const rawData = {
name: formData.get('name'),
description: formData.get('description'),
};
const validated = updateMyModelSchema.parse(rawData);
// Actualizar
const updated = await db.myModel.update({
where: { id },
data: validated,
});
revalidatePath('/my-models');
revalidatePath(`/my-models/${id}`);
return { success: true, data: updated };
} catch (error) {
captureError(error, { context: 'updateMyModel' });
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Elimina un modelo
*/
export async function deleteMyModel(id: string) {
try {
const user = await getCurrentUser();
if (!user) throw new Error('Unauthorized');
requireRole(user, ['REQUIRED_ROLE']);
// Verificar que existe
const existing = await db.myModel.findUnique({
where: { id },
});
if (!existing) {
throw new Error('Model not found');
}
// Eliminar (cascade se maneja en Prisma schema)
await db.myModel.delete({
where: { id },
});
revalidatePath('/my-models');
return { success: true };
} catch (error) {
captureError(error, { context: 'deleteMyModel' });
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
Paso 6: Agregar al Schema Prisma
Si necesitas un nuevo modelo en la base de datos:
Archivo: prisma/schema.prisma
model MyModel {
id String @id @default(cuid())
name String
description String?
// Relaciones
relatedItems RelatedItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([name])
}
Luego:
pnpm db:generate
pnpm db:push
Paso 7: Crear Tests
Archivo: tests/modules/my-module/queries.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { getMyModelById, listMyModels } from '@/modules/my-module/queries';
import { db } from '@/core/db';
describe('MyModule Queries', () => {
beforeEach(async () => {
// Limpiar datos de prueba
await db.myModel.deleteMany();
});
describe('getMyModelById', () => {
it('should return model if exists', async () => {
const model = await db.myModel.create({
data: { name: 'Test Model' },
});
const result = await getMyModelById(model.id);
expect(result).not.toBeNull();
expect(result?.id).toBe(model.id);
});
it('should return null if not exists', async () => {
const result = await getMyModelById('non-existent-id');
expect(result).toBeNull();
});
});
describe('listMyModels', () => {
it('should return all models', async () => {
await db.myModel.createMany({
data: [
{ name: 'Model 1' },
{ name: 'Model 2' },
],
});
const result = await listMyModels();
expect(result).toHaveLength(2);
});
});
});
Archivo: tests/modules/my-module/actions.test.ts
import { describe, it, expect } from 'vitest';
import { createMyModel } from '@/modules/my-module/actions';
describe('MyModule Actions', () => {
describe('createMyModel', () => {
it('should create model with valid data', async () => {
const formData = new FormData();
formData.set('name', 'Test Model');
const result = await createMyModel(formData);
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
});
it('should fail with invalid data', async () => {
const formData = new FormData();
formData.set('name', ''); // ❌ Inválido
const result = await createMyModel(formData);
expect(result.success).toBe(false);
expect(result.error).toBeDefined();
});
});
});
Paso 8: Crear UI (Opcional)
Si necesitas UI para el módulo:
Archivo: app/my-models/page.tsx
import { listMyModels } from '@/modules/my-models/queries';
import { MyModelsList } from './my-models-list';
export default async function MyModelsPage() {
const models = await listMyModels();
return (
<div>
<h1>My Models</h1>
<MyModelsList initialData={models} />
</div>
);
}
Checklist de Módulo Completo
- Estructura de directorio creada
-
types.tscon interfaces definidas -
validations.tscon schemas Zod -
queries.tscon funciones de lectura -
actions.tscon Server Actions - RBAC implementado en actions
- Manejo de errores con
captureError - Revalidación de cache con
revalidatePath - Tests escritos (queries y actions)
- Tests pasando
- Documentación en código (JSDoc)
Ejemplo Completo: Módulo Simple
Ver módulos existentes como referencia:
modules/users/- Módulo básicomodules/hackathons/- Módulo complejo con relacionesmodules/teams/- Módulo con lógica de negocio
Próximos Pasos
- Testing Guide - Cómo escribir tests
- Coding Standards - Estándares de código
- Troubleshooting - Solución de problemas
¿Listo para crear tu módulo? → Sigue los pasos arriba