← index

MODULES

docs/MODULES.md

Módulos

Cómo está organizado el código por feature, y cómo agregar un módulo nuevo.

Filosofía

Cada módulo es feature-based y autocontenido. Si abrís src/lib/modules/patients/, tenés TODO lo del módulo pacientes: sus componentes, su lógica de negocio, sus schemas, sus tipos. Otros módulos y routes importan solo desde el barrel index.ts del módulo.

Estructura estándar

src/lib/modules/<nombre>/
├── components/      # Componentes Svelte específicos del módulo
├── services/        # Lógica de negocio (queries, mutations, validaciones server-side)
├── schema.ts        # Schemas Zod (input/output de forms y API)
├── types.ts         # Tipos app-level (DTOs, view models)
├── index.ts         # Barrel — solo exporta lo público
└── README.md        # (opcional) Notas específicas

Reglas de oro

  1. Autocontenido. Una utilidad usada solo en patients/ vive en patients/. Si dos+ módulos la usan, promover a $lib/utils o $lib/services.
  2. Queries a la BD solo en services/. Los componentes NO importan $server/db directo. Piden datos al service del módulo.
  3. Schema Zod se reutiliza client+server. Definido en schema.ts, usado con sveltekit-superforms y en actions/load.
  4. Types app-level (DTOs) en types.ts. NO exportar tipos puros de Drizzle desde el módulo.
  5. index.ts es la API pública. Otros módulos importan from '$modules/<nombre>', nunca de archivos internos.

Catálogo actual (los 9 del MVP)

# Módulo Carpeta Estado
1 Gestión de pacientes modules/patients/ ✅ Scaffolded
2 Planificación alimentaria modules/meal-plans/ ✅ Scaffolded
3 Agenda y gestión de citas modules/appointments/ ✅ Scaffolded
4 Pagos y facturación modules/payments/ ✅ Scaffolded
5 Comunicación profesional–paciente modules/communication/ ✅ Scaffolded
6 App del paciente (diario) modules/patient-app/ ✅ Scaffolded
7 Gamificación modules/gamification/ ✅ Scaffolded
8 Página web pública modules/public-pages/ ✅ Scaffolded
9 Planes y acceso modules/subscriptions/ ✅ Scaffolded

"Scaffolded" = tiene la estructura, types, schemas Zod y barrel listos. Faltan implementar services, components y routes (eso es el trabajo post-MVP-base).

Patrón de service

// modules/patients/services/patient.service.ts
import { db, tables } from '$server/db';
import { eq, and, desc } from 'drizzle-orm';
import type { PatientWithProfile } from '../types';

export async function listMyPatients(
  nutritionistId: string,
  options: { onlyActive?: boolean } = {}
): Promise<PatientWithProfile[]> {
  const conditions = [eq(tables.patients.nutritionistId, nutritionistId)];
  if (options.onlyActive) {
    conditions.push(eq(tables.patients.isActive, true));
  }

  return db
    .select({
      id: tables.patients.id,
      // ... pick columns + join profile
    })
    .from(tables.patients)
    .innerJoin(tables.profiles, eq(tables.profiles.id, tables.patients.profileId))
    .where(and(...conditions))
    .orderBy(desc(tables.patients.createdAt));
}

export async function createPatient(
  nutritionistId: string,
  input: CreatePatientInput
): Promise<PatientWithProfile> {
  // Use a transaction because we touch 2 tables (auth.users via supabase, profiles, patients)
  return db.transaction(async (tx) => {
    // ... insert logic
  });
}

Patrón de schema Zod

// modules/patients/schema.ts
import { z } from 'zod';

export const createPatientSchema = z.object({
  fullName: z.string().min(2),
  email: z.string().email(),
  birthDate: z.coerce.date().optional(),
  gender: z.enum(['male', 'female', 'other', 'prefer_not_to_say']).optional()
});

export type CreatePatientInput = z.infer<typeof createPatientSchema>;

Patrón de component

<!-- modules/patients/components/PatientCard.svelte -->
<script lang="ts">
  import type { PatientWithProfile } from '../types';
  let { patient }: { patient: PatientWithProfile } = $props();
</script>

<a href={`/patients/${patient.id}`} class="hover:bg-muted block rounded-lg border p-4">
  <h3 class="font-medium">{patient.profile.fullName}</h3>
  <p class="text-muted-foreground text-sm">{patient.profile.email}</p>
</a>

Patrón de route (en (app) o (patient))

// src/routes/(app)/patients/+page.server.ts
import { listMyPatients } from '$modules/patients';
import { requireUser } from '$lib/services/auth';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async (event) => {
  const user = await requireUser(event, { role: 'nutritionist' });
  const patients = await listMyPatients(user.id);
  return { patients };
};
<!-- src/routes/(app)/patients/+page.svelte -->
<script lang="ts">
  import PatientCard from '$modules/patients/components/PatientCard.svelte';
  import type { PageData } from './$types';
  let { data }: { data: PageData } = $props();
</script>

<h2>Pacientes</h2>
{#each data.patients as patient (patient.id)}
  <PatientCard {patient} />
{/each}

Agregar un módulo nuevo

  1. Crear src/lib/modules/<nombre>/ con la estructura estándar
  2. Si el módulo necesita tablas nuevas: agregalas en src/lib/server/db/schema.ts y exporta el tipo inferido
  3. Crear types.ts con los DTOs
  4. Crear schema.ts con los Zod schemas
  5. Implementar services/<x>.service.ts con las queries
  6. Crear components/ cuando los necesites
  7. index.ts con barrel
  8. Crear los routes en src/routes/(app)/<x>/ o (patient)/<x>/
  9. Si el módulo necesita UI en el sidebar, agregar a src/routes/(app)/+layout.svelte y src/routes/(patient)/+layout.svelte

Cuando un módulo crece

Si un módulo tiene > 10 components, considerar subdividir:

modules/patients/
├── components/
│   ├── list/        # vista de lista
│   ├── detail/      # vista de detalle
│   └── form/        # formularios
├── services/
│   ├── patients.service.ts
│   └── measurements.service.ts  # sub-recurso
└── ...

Pero NO hacer esto prematuramente. La estructura plana se mantiene mientras sea manejable.