📝 Estándares de Código
Visión General
Estos estándares aseguran consistencia y mantenibilidad del código en PuntoHack.
Convenciones de Nombres
Archivos y Directorios
-
Archivos TypeScript:
kebab-case.tsokebab-case.tsx- ✅
user-profile.tsx - ✅
hackathon-queries.ts - ❌
UserProfile.tsx - ❌
hackathonQueries.ts
- ✅
-
Componentes React:
PascalCase.tsx- ✅
UserProfile.tsx - ✅
HackathonCard.tsx
- ✅
-
Directorios:
kebab-case- ✅
user-flows/ - ✅
hackathon-detail/
- ✅
Variables y Funciones
-
Variables:
camelCaseconst userName = 'John';
const hackathonList = []; -
Funciones:
camelCasefunction getUserProfile() {}
async function createHackathon() {} -
Constantes:
SCREAMING_SNAKE_CASEconst MAX_TEAM_SIZE = 5;
const MIN_TEAM_SIZE = 1;
const MAX_EXTENSION_DAYS = 7; -
Tipos e Interfaces:
PascalCasetype UserProfile = {};
interface HackathonData {}
Estructura de Módulos
Cada módulo sigue esta estructura estándar:
modules/[domain]/
├── queries.ts # Operaciones de lectura
├── actions.ts # Server Actions (escritura)
├── types.ts # TypeScript interfaces
└── validations.ts # Zod schemas
queries.ts
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,
},
},
},
});
}
Convenciones:
- Funciones async por defecto
- Nombres descriptivos:
get,list,find,count - Incluir relaciones necesarias
- Ordenar resultados cuando aplique
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 { 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'),
// ...
};
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, hackathon };
} catch (error) {
captureError(error, { context: 'createHackathon' });
return { success: false, error: error.message };
}
}
Patrón estándar:
'use server'al inicio- Auth check
- RBAC check
- Validación con Zod
- Lógica de negocio
- Revalidar cache
- Manejo de errores
validations.ts
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(),
// ...
}).refine(
(data) => {
// Validación custom de fechas
return data.registrationOpensAt < data.registrationClosesAt;
},
{
message: 'registrationOpensAt must be before registrationClosesAt',
path: ['registrationOpensAt'],
}
);
Convenciones:
- Usar Zod para todas las validaciones
- Mensajes de error descriptivos
- Validaciones custom con
.refine() - Coerción de tipos cuando sea necesario
types.ts
import { Profile, Hackathon, Team } from '@prisma/client';
export type ProfileWithParticipations = Profile & {
participations: HackathonParticipation[];
};
export type HackathonWithStats = Hackathon & {
_count: {
participations: number;
teams: number;
submissions: number;
};
};
export type TeamWithMembers = Team & {
members: TeamMember[];
submissions: Submission[];
};
Server Actions Pattern
Estructura Estándar
'use server';
export async function myAction(formData: FormData) {
try {
// 1. Auth
const user = await getCurrentUser();
if (!user) throw new Error('Unauthorized');
// 2. RBAC
requireRole(user, ['REQUIRED_ROLE']);
// 3. Validate
const validated = schema.parse(data);
// 4. Business logic
const result = await db.model.create({ data: validated });
// 5. Revalidate
revalidatePath('/path');
// 6. Return
return { success: true, data: result };
} catch (error) {
captureError(error, { context: 'myAction' });
return { success: false, error: error.message };
}
}
Manejo de Errores
try {
// ...
} catch (error) {
// Siempre capturar con contexto
captureError(error, {
context: 'functionName',
userId: user?.profile.id,
hackathonId: hackathonId,
});
// Retornar error user-friendly
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
Client Components Pattern
Actualizaciones Optimistas
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { myServerAction } from '@/modules/my-module/actions';
export default function MyComponent({ initialData }: { initialData: Data[] }) {
const router = useRouter();
const [data, setData] = useState<Data[]>(initialData);
// Sincronizar con props
useEffect(() => {
setData(initialData);
}, [initialData]);
async function handleUpdate(id: string, newValue: string) {
// Guardar valor anterior
const previousItem = data.find((item) => item.id === id);
const previousValue = previousItem?.value;
// ACTUALIZACIÓN OPTIMISTA
setData((prev) =>
prev.map((item) =>
item.id === id ? { ...item, value: newValue } : item
)
);
try {
const result = await myServerAction(id, newValue);
if (!result.success) {
// ROLLBACK
if (previousValue) {
setData((prev) =>
prev.map((item) =>
item.id === id ? { ...item, value: previousValue } : item
)
);
}
// Mostrar error (usar toast)
} else {
// SINCRONIZAR
router.refresh();
}
} catch (error) {
// ROLLBACK en caso de error
if (previousValue) {
setData((prev) =>
prev.map((item) =>
item.id === id ? { ...item, value: previousValue } : item
)
);
}
}
}
return <div>{/* Renderizar */}</div>;
}
TypeScript
Configuración
- Strict mode: Habilitado
- No implicit any: Habilitado
- Strict null checks: Habilitado
Verificaciones de Null/Undefined
PuntoHack usa strict null checks, lo que significa que debemos verificar explícitamente valores que pueden ser null o undefined.
Patrón Estándar
// ✅ Correcto: Verificar antes de acceder
if (hackathon && hackathon.status === 'JUDGING') {
// Usar hackathon
}
// ❌ Incorrecto: Acceder sin verificar
if (hackathon.status === 'JUDGING') { // Error si hackathon es null
// ...
}
En Queries y API Routes
// ✅ Filtrar nulls antes de procesar
const validAssignments = assignments
.filter((assignment) => assignment.hackathon) // Filtrar nulls
.map((assignment) => {
if (!assignment.hackathon) return null; // Verificación adicional
return {
id: assignment.id,
hackathonName: assignment.hackathon.name,
};
})
.filter((item) => item !== null); // Filtrar resultados null
En Componentes React
// ✅ Usar optional chaining y nullish coalescing
{assignment.hackathon?.name || 'N/A'}
// ✅ Verificar antes de renderizar
{submission.team && (
<div>
<p>Equipo: {submission.team.name}</p>
{submission.team.members && (
<ul>
{submission.team.members.map((member) => (
<li key={member.id}>{member.name}</li>
))}
</ul>
)}
</div>
)}
En Server Actions
// ✅ Verificar relaciones antes de acceder
if (!challenge.sponsorship.organization) {
return { success: false, error: 'Organización no encontrada' };
}
const isMember = await isOrganizationMember(
challenge.sponsorship.organization.id,
profile.id
);
Type Guards
// ✅ Crear type guards para verificación
function hasHackathon(
assignment: JudgeAssignment
): assignment is JudgeAssignment & { hackathon: NonNullable<JudgeAssignment['hackathon']> } {
return assignment.hackathon !== null && assignment.hackathon !== undefined;
}
// Uso
if (hasHackathon(assignment)) {
// TypeScript sabe que assignment.hackathon existe
console.log(assignment.hackathon.name);
}
Tipos
// ✅ Preferir tipos explícitos
function getUser(id: string): Promise<Profile | null> {
return db.profile.findUnique({ where: { id } });
}
// ❌ Evitar any
function processData(data: any) {} // ❌
// ✅ Usar tipos específicos
function processData(data: UserData) {} // ✅
Interfaces vs Types
-
Interfaces: Para objetos que pueden extenderse
interface User {
id: string;
name: string;
} -
Types: Para uniones, intersecciones, etc.
type Status = 'ACTIVE' | 'INACTIVE';
type UserWithRole = User & { role: Role };
Imports
Orden de Imports
// 1. React y Next.js
import { useState } from 'react';
import { useRouter } from 'next/navigation';
// 2. Librerías externas
import { z } from 'zod';
import { db } from '@prisma/client';
// 3. Core (infraestructura)
import { getCurrentUser } from '@/core/auth';
import { requireRole } from '@/core/rbac';
import { db } from '@/core/db';
// 4. Módulos
import { getHackathon } from '@/modules/hackathons/queries';
// 5. Componentes
import { Button } from '@/components/button';
// 6. Tipos
import type { Hackathon } from '@prisma/client';
Path Aliases
Usar @/ para imports absolutos:
// ✅ Correcto
import { db } from '@/core/db';
import { getHackathon } from '@/modules/hackathons/queries';
// ❌ Incorrecto
import { db } from '../../../core/db';
Comentarios y Documentación
JSDoc para Funciones
/**
* Obtiene un hackathon por su slug
*
* @param slug - Slug único del hackathon
* @returns Hackathon con criterios y contadores, o null si no existe
* @throws Error si slug es inválido
*/
export async function getHackathon(slug: string) {
// ...
}
Comentarios Inline
// ✅ Explicar "por qué", no "qué"
// Validar que el juez no participa para evitar conflicto de interés
if (judgeParticipates) {
throw new Error('Judge cannot participate');
}
// ❌ Evitar comentarios obvios
// Incrementar contador
counter++;
Testing
Estructura de Tests
import { describe, it, expect } from 'vitest';
import { validateHackathonDates } from '@/core/validations';
describe('validateHackathonDates', () => {
it('should validate correct date order', () => {
const dates = {
registrationOpensAt: new Date('2025-01-01'),
registrationClosesAt: new Date('2025-01-15'),
// ...
};
expect(() => validateHackathonDates(dates)).not.toThrow();
});
it('should throw error for invalid date order', () => {
const dates = {
registrationOpensAt: new Date('2025-01-15'),
registrationClosesAt: new Date('2025-01-01'), // ❌ Invertido
// ...
};
expect(() => validateHackathonDates(dates)).toThrow();
});
});
Próximos Pasos
- Module Guide - Cómo crear nuevos módulos
- Testing Guide - Guía completa de testing
- Troubleshooting - Solución de problemas
Siguiente: Module Guide