📦 Estructura de Módulos
Visión General
PuntoHack organiza el código en módulos de dominio siguiendo un patrón consistente. Cada módulo encapsula toda la lógica relacionada con una entidad de negocio específica.
Estructura Estándar
Cada módulo sigue esta estructura:
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
Patrón de Módulo
1. queries.ts - Lectura de Datos
Responsabilidad: Obtener datos de la base de datos sin modificarlos.
Características:
- ✅ Funciones async
- ✅ Nombres descriptivos:
get,list,find,count - ✅ Incluir relaciones necesarias
- ✅ Ordenar resultados cuando aplique
Ejemplo:
import { db } from '@/core/db';
/**
* Obtiene un hackathon por su slug
* Incluye criterios y contador de participaciones
*/
export async function getHackathon(slug: string) {
return db.hackathon.findUnique({
where: { slug },
include: {
criteria: {
orderBy: { createdAt: 'asc' },
},
_count: {
select: {
participations: true,
criteria: true,
teams: true,
},
},
},
});
}
/**
* Lista hackathons con filtros opcionales
*/
export async function listHackathons(filters?: {
status?: HackathonStatus;
organizerId?: string;
limit?: number;
}) {
const where: {
status?: HackathonStatus;
organizerId?: string;
} = {};
if (filters?.status) {
where.status = filters.status;
}
if (filters?.organizerId) {
where.organizerId = filters.organizerId;
}
return db.hackathon.findMany({
where,
take: filters?.limit || 50,
orderBy: { createdAt: 'desc' },
include: {
organizer: {
select: {
id: true,
name: true,
email: true,
},
},
_count: {
select: {
participations: true,
teams: true,
},
},
},
});
}
2. actions.ts - Mutaciones de Datos
Responsabilidad: Modificar datos mediante Server Actions.
Características:
- ✅
'use server'al inicio - ✅ Auth check obligatorio
- ✅ RBAC check obligatorio
- ✅ Validación con Zod
- ✅ Revalidación de cache
- ✅ Manejo de errores
Ejemplo:
'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 { createHackathonSchema } from './validations';
export async function createHackathon(formData: FormData) {
try {
// 1. Auth check
const user = await getCurrentUser();
if (!user) {
throw new Error('Unauthorized');
}
// 2. RBAC check
requireRole(user, ['ORGANIZER', 'ADMIN']);
// 3. Validate input
const rawData = {
name: formData.get('name'),
slug: formData.get('slug'),
description: formData.get('description'),
// ... otros campos
};
const validated = createHackathonSchema.parse(rawData);
// 4. Business logic
const hackathon = await db.hackathon.create({
data: validated,
});
// 5. Revalidate cache
revalidatePath('/hackathons');
// 6. Return result
return { success: true, data: hackathon };
} catch (error) {
captureError(error, { context: 'createHackathon' });
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
3. validations.ts - Validación de Datos
Responsabilidad: Definir schemas Zod para validar inputs.
Características:
- ✅ Schemas descriptivos
- ✅ Mensajes de error claros
- ✅ Validaciones custom con
.refine() - ✅ Coerción de tipos cuando sea necesario
Ejemplo:
import { z } from 'zod';
export const createHackathonSchema = z.object({
name: z.string().min(3, 'Name must be at least 3 characters'),
slug: z.string().regex(/^[a-z0-9-]+$/, 'Slug must be lowercase alphanumeric with hyphens'),
description: z.string().min(10, 'Description must be at least 10 characters'),
registrationOpensAt: z.coerce.date(),
registrationClosesAt: z.coerce.date(),
startsAt: z.coerce.date(),
endsAt: z.coerce.date(),
submissionDeadline: z.coerce.date(),
judgingStartsAt: z.coerce.date(),
judgingEndsAt: z.coerce.date(),
maxTeamSize: z.coerce.number().int().min(1).max(10),
minTeamSize: z.coerce.number().int().min(1).max(5),
}).refine(
(data) => data.registrationOpensAt < data.registrationClosesAt,
{
message: 'registrationOpensAt must be before registrationClosesAt',
path: ['registrationOpensAt'],
}
).refine(
(data) => data.submissionDeadline < data.judgingStartsAt,
{
message: 'submissionDeadline must be before judgingStartsAt',
path: ['submissionDeadline'],
}
);
4. types.ts - Tipos TypeScript
Responsabilidad: Definir tipos e interfaces específicos del módulo.
Características:
- ✅ Tipos extendidos con relaciones
- ✅ Input types para crear/actualizar
- ✅ Response types para queries
Ejemplo:
import { Hackathon, Criterion, Profile } from '@prisma/client';
export type HackathonWithCriteria = Hackathon & {
criteria: Criterion[];
};
export type HackathonWithStats = Hackathon & {
_count: {
participations: number;
teams: number;
submissions: number;
criteria: number;
};
};
export type CreateHackathonInput = {
name: string;
slug: string;
description: string;
registrationOpensAt: Date;
registrationClosesAt: Date;
startsAt: Date;
endsAt: Date;
submissionDeadline: Date;
judgingStartsAt: Date;
judgingEndsAt: Date;
maxTeamSize: number;
minTeamSize: number;
};
export type UpdateHackathonInput = Partial<CreateHackathonInput>;
Interacción con RBAC
Uso de RBAC en Actions
Cada Server Action debe verificar permisos:
export async function deleteHackathon(hackathonId: string) {
const user = await getCurrentUser();
if (!user) throw new Error('Unauthorized');
// RBAC check específico
requireRole(user, ['ORGANIZER', 'ADMIN']);
// Verificar propiedad (si aplica)
const hackathon = await db.hackathon.findUnique({
where: { id: hackathonId },
});
if (!hackathon) {
throw new Error('Hackathon not found');
}
// Solo el organizador o admin puede eliminar
if (hackathon.organizerId !== user.profile.id && user.profile.role !== 'ADMIN') {
throw new Error('Unauthorized: Only organizer or admin can delete');
}
// Continuar con eliminación...
}
RBAC en Queries
Las queries pueden filtrar por permisos:
export async function listHackathonsForUser(userId: string) {
const user = await getCurrentUser();
if (!user) return [];
const where: {
status?: HackathonStatus;
organizerId?: string;
} = {};
// Si es ORGANIZER, puede ver sus DRAFT hackathons
if (user.profile.role === 'ORGANIZER') {
where.OR = [
{ status: { not: 'DRAFT' } }, // Públicos
{ organizerId: user.profile.id }, // Sus propios
];
} else {
// Otros roles solo ven públicos
where.status = { not: 'DRAFT' };
}
return db.hackathon.findMany({ where });
}
Módulos Existentes
1. users/
Responsabilidad: Gestión de usuarios y perfiles.
Archivos:
queries.ts- Obtener perfiles, verificar rolesactions.ts- Crear/actualizar perfiles, cambiar roles (ADMIN)validations.ts- Schemas para perfilestypes.ts- Tipos de usuario
2. hackathons/
Responsabilidad: CRUD de hackathons, gestión de estados.
Archivos:
queries.ts- Obtener hackathons, filtrar por estadoactions.ts- Crear, actualizar, publicar hackathonsvalidations.ts- Validación de fechas, estadostypes.ts- Tipos de hackathon
3. teams/
Responsabilidad: Formación y gestión de equipos.
Archivos:
queries.ts- Obtener equipos, miembros, verificar membresíaactions.ts- Crear equipos, unirse, salir, invitarvalidations.ts- Validación de códigos, tamañostypes.ts- Tipos de equipo
4. submissions/
Responsabilidad: Envío y gestión de proyectos.
Archivos:
queries.ts- Obtener submissions, filtrar por hackathon/equipoactions.ts- Crear, actualizar submissions (hasta deadline)validations.ts- Validación de URLs, descripcionestypes.ts- Tipos de submission
5. evaluation/
Responsabilidad: Sistema de evaluación y scoring.
Archivos:
queries.ts- Obtener scores, calcular leaderboardactions.ts- Asignar scores, evaluar challengesvalidations.ts- Validación de scores (0-maxScore)types.ts- Tipos de evaluación
6. sponsors/
Responsabilidad: Gestión de sponsors, challenges, shortlist.
Archivos:
queries.ts- Obtener organizaciones, sponsorships, challengesactions.ts- Crear organizaciones, sponsorships, challenges, shortlistvalidations.ts- Validación de tiers, challengestypes.ts- Tipos de sponsor
Diagrama de Interacción
Mejores Prácticas
1. Separación de Responsabilidades
- ✅ Queries solo leen
- ✅ Actions solo escriben
- ✅ Validations solo validan
- ✅ Types solo definen tipos
2. Reutilización
- ✅ Usar queries dentro de actions cuando sea necesario
- ✅ Compartir validations entre actions similares
- ✅ Tipos compartidos en
types.ts
3. Testing
- ✅ Testear queries con datos reales
- ✅ Testear actions con mocks
- ✅ Testear validations con casos límite
4. Documentación
- ✅ JSDoc en funciones públicas
- ✅ Comentarios en lógica compleja
- ✅ Ejemplos en tipos complejos
Próximos Pasos
- Design Patterns - Patrones utilizados
- Development Guide - Cómo crear módulos
- RBAC System - Sistema de permisos
Siguiente: Design Patterns