Saltar al contenido principal

📝 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.ts o kebab-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: camelCase

    const userName = 'John';
    const hackathonList = [];
  • Funciones: camelCase

    function getUserProfile() {}
    async function createHackathon() {}
  • Constantes: SCREAMING_SNAKE_CASE

    const MAX_TEAM_SIZE = 5;
    const MIN_TEAM_SIZE = 1;
    const MAX_EXTENSION_DAYS = 7;
  • Tipos e Interfaces: PascalCase

    type 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:

  1. 'use server' al inicio
  2. Auth check
  3. RBAC check
  4. Validación con Zod
  5. Lógica de negocio
  6. Revalidar cache
  7. 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


Siguiente: Module Guide