Saltar al contenido principal

📦 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.ts con interfaces definidas
  • validations.ts con schemas Zod
  • queries.ts con funciones de lectura
  • actions.ts con 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ásico
  • modules/hackathons/ - Módulo complejo con relaciones
  • modules/teams/ - Módulo con lógica de negocio

Próximos Pasos


¿Listo para crear tu módulo? → Sigue los pasos arriba