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
- Autocontenido. Una utilidad usada solo en
patients/vive enpatients/. Si dos+ módulos la usan, promover a$lib/utilso$lib/services. - Queries a la BD solo en
services/. Los componentes NO importan$server/dbdirecto. Piden datos al service del módulo. - Schema Zod se reutiliza client+server. Definido en
schema.ts, usado consveltekit-superformsy en actions/load. - Types app-level (DTOs) en
types.ts. NO exportar tipos puros de Drizzle desde el módulo. index.tses la API pública. Otros módulos importanfrom '$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
- Crear
src/lib/modules/<nombre>/con la estructura estándar - Si el módulo necesita tablas nuevas: agregalas en
src/lib/server/db/schema.tsy exporta el tipo inferido - Crear
types.tscon los DTOs - Crear
schema.tscon los Zod schemas - Implementar
services/<x>.service.tscon las queries - Crear
components/cuando los necesites index.tscon barrel- Crear los routes en
src/routes/(app)/<x>/o(patient)/<x>/ - Si el módulo necesita UI en el sidebar, agregar a
src/routes/(app)/+layout.svelteysrc/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.