Proyecto universitario para la asignatura Laboratorio 2 (Universidad de La Punta, San Luis, Argentina). Es un sistema de gestión de turnos médicos que permite a pacientes autogestionar sus reservas y a la administración gestionar agendas, profesionales y coberturas médicas.
El stack fue impuesto en parte por la cátedra (Node.js, Express, MariaDB, motor de plantillas SSR) y complementado con decisiones propias documentadas en los ADRs.
Monolito con SSR. La estructura interna sigue un Vertical Slicing por módulo de dominio con influencia de Clean Architecture: cada feature (users, patients, schedules, slots, etc.) tiene sus propias capas de dominio, aplicación, infraestructura y vistas. No hay microservicios ni separación frontend/backend — todo convive en el mismo proceso Node.js.
src/
├── _shared/ # Código transversal (errores, middleware, layouts base)
├── [modulo]/ # Un directorio por dominio
│ ├── domain/ # Entidades y contratos de repositorio (JSDoc)
│ ├── application/ # Servicios y casos de uso
│ ├── infrastructure/ # Controllers, routes, repositorios Prisma
│ └── views/ # Templates Nunjucks del módulo
├── app.js # Setup de Express
└── server.js # Entry point
El flujo por request es siempre: Router → Controller → Service → Repository → Prisma. Los servicios retornan Result<T, E> usando neverthrow; los errores fatales se propagan al globalErrorHandler de Express.
Cada decisión de diseño relevante está documentada como un ADR en docs/adr/. El resumen:
| ADR | Decisión | Alternativa descartada |
|---|---|---|
| 001 | Prisma como ORM | mysql2 (driver base) |
| 002 | Nunjucks como motor de plantillas | EJS, Handlebars, Pug |
| 003 | JWT en cookie HttpOnly | Sesiones en DB, OAuth |
| 004 | Modular Clean (Vertical Slicing) | MVC clásico |
| 005 | Estrategia híbrida: neverthrow + excepciones |
Solo excepciones |
| 006 | Slots pre-generados en BD | Rule Pattern en memoria |
erDiagram
users {
INT user_id PK
VARCHAR email "UK"
VARCHAR password_hash
ENUM role "ADMIN, SECRETARY, PROFESSIONAL, PATIENT"
VARCHAR national_id
VARCHAR first_names
VARCHAR last_names
VARCHAR phone
VARCHAR address
VARCHAR national_id_image_url "Nullable"
DATETIME registered_at
DATETIME deleted_at "Nullable"
}
health_insurances {
INT insurance_id PK
VARCHAR name "UK"
DATETIME deleted_at "Nullable"
}
patient_health_insurance {
INT user_id PK, FK
INT insurance_id PK, FK
VARCHAR member_number
}
specialties {
INT specialty_id PK
VARCHAR name "UK"
DATETIME deleted_at "Nullable"
}
professional_specialty {
VARCHAR license_number PK
INT user_id FK
INT specialty_id FK
}
locations {
INT location_id PK
VARCHAR name
VARCHAR address
VARCHAR phone "Nullable"
DATETIME deleted_at "Nullable"
}
classifications {
INT classification_id PK
VARCHAR name "UK"
}
schedules {
INT schedule_id PK
VARCHAR professional_license FK
INT location_id FK
INT classification_id FK
INT slot_duration_minutes
INT max_overbooks_per_day
INT max_overbooks_per_slot
BOOLEAN is_paused
DATETIME deleted_at "Nullable"
}
schedule_configs {
INT config_id PK
INT schedule_id FK
ENUM day_of_week
VARCHAR start_time
VARCHAR end_time
DATE valid_from
DATE valid_until
}
schedule_blocks {
INT block_id PK
INT schedule_id FK "Nullable"
DATE start_date
DATE end_date
TEXT reason
}
slots {
INT slot_id PK
INT schedule_id FK
INT patient_id FK "Nullable"
DATETIME starts_at
ENUM status "FREE, PROPOSED, BOOKED, CANCELLED, NO_SHOW, ARRIVED, IN_PROGRESS, FULFILLED"
BOOLEAN is_overbook
TEXT consultation_reason "Nullable"
}
waiting_list {
INT waitlist_id PK
INT patient_id FK
INT specialty_id FK "Nullable"
INT professional_id FK "Nullable"
DATETIME request_date
}
users ||--o{ patient_health_insurance : "has coverage"
health_insurances ||--o{ patient_health_insurance : "insures"
users ||--o{ professional_specialty : "has credentials"
specialties ||--o{ professional_specialty : "belongs to"
professional_specialty ||--o{ schedules : "manages"
locations ||--o{ schedules : "hosts"
classifications ||--o{ schedules : "categorizes"
schedules ||--|{ schedule_configs : "configured by"
schedules ||--o{ schedule_blocks : "has specific blocks"
schedules ||--o{ slots : "generates"
users ||--o{ slots : "requests"
users ||--o{ waiting_list : "waits for"
specialties ||--o{ waiting_list : "for specialty"
users ||--o{ waiting_list : "with professional"
Tipado estático sin TypeScript (restricción de la cátedra): se usó JSDoc para definir los contratos de repositorios y servicios, con reglas ESLint que validan que las implementaciones cumplan con las interfaces documentadas. No es equivalente a TypeScript, pero captura los errores más comunes en tiempo de edición y en CI.
Pre-commit automatizado: husky + lint-staged ejecutan ESLint y Prettier antes de cada commit; commitlint fuerza el formato Conventional Commits.
Tests: integración con Jest + Supertest contra una base de datos de test real, más unit tests para lógica de dominio pura (máquina de estados de turnos, filtros de templates). 84.5% de cobertura de statements sobre el código de aplicación.
Requisitos: Node.js v22+, MariaDB/MySQL, pnpm.
# 1. Instalar dependencias
pnpm install
# 2. Configurar entorno
cp .env.example .env
# Editar .env con las credenciales de la base de datos
# 3. Migrar la base de datos
pnpm exec prisma migrate dev
# 4. Seed inicial (opcional)
pnpm run db:seed
# 5. Compilar CSS y arrancar
pnpm run css:build
pnpm run dev| Variable | Descripción | Ejemplo |
|---|---|---|
PORT |
Puerto de escucha | 3000 |
MYSQL_CONNECTION_STRING |
URL de conexión Prisma | mysql://root:@localhost:3306/clinica_angel |
JWT_SECRET |
Secreto para firmar tokens (mín. 32 chars) | — |
JWT_EXPIRES |
Duración del token | 1h, 7d |
pnpm test # Suite completa
pnpm test:cov # Con reporte de cobertura
pnpm lint:fix # ESLint + Prettier- 📋 Especificaciones, requerimientos, alcance y límite, casos de uso, etc — Notion
- 🏛️ Decisiones de arquitectura (ADRs) — en este repositorio
Desarrollado por Emanuel Angel — Universidad de La Punta, San Luis, Argentina.