Proyecto 02: Reservas de pistas de pádel
TFG · DEMO PUBLICADAReservas de pistas con duraciones de 60 y 90 minutos sin solapes, reglas del club configurables y cuentas con roles. Proyecto final de DAW desarrollado con Jose Jandula.
- Tipo
- Proyecto final de grado (DAW)
- Año
- 2026
- Equipo
- Erik Portillo y Jose Jandula
- Estado
- Demo publicada; no está en producción
React 19 · Vite · React Router · Node.js · Express 5 · PostgreSQL (Supabase) · JWT · Nodemailer
El problema
Un club necesita que nadie reserve una pista ya ocupada, aunque las reservas no duren lo mismo. Comprobar solo la hora de inicio no basta: una reserva de 90 minutos a las 10:00 también ocupa las 11:00.
Arquitectura
Frontend en React con Vite, desplegado en Vercel. Backend en Node.js con Express, desplegado en Railway y organizado en rutas, controladores, servicios y middlewares; solo la capa de servicios accede a la base de datos.
PostgreSQL en Supabase, que también guarda las imágenes de las pistas. El correo (verificación de cuentas y recuperación de contraseña) sale por SMTP con Nodemailer.
Cómo se valida una reserva
Antes de guardar, el servidor comprueba, en este orden:
- 1
Formato
Fecha y hora válidas; solo se empieza en punto o y media.
- 2
Calendario del club
Que el día no esté bloqueado, que el club abra ese día de la semana y que la hora esté dentro de su horario.
- 3
Tiempo
Que no sea una hora pasada y que se respete la antelación mínima (la administración puede saltársela al reservar por un cliente).
- 4
Duración
Que la duración esté permitida y que la reserva termine antes del cierre.
- 5
Pista
Que la pista exista y esté activa.

Decisiones técnicas
La regla de solape, en el servidor
Dos intervalos se cruzan si cada uno empieza antes de que termine el otro. La comprobación se hace contra las reservas de esa pista y ese día, que se cargan antes de insertar.
function haySolape(inicioNueva, finNueva, inicioExistente, finExistente) {
const nuevaInicio = horaAMinutos(inicioNueva);
const nuevaFin = horaAMinutos(finNueva);
const existenteInicio = horaAMinutos(inicioExistente);
const existenteFin = horaAMinutos(finExistente);
return nuevaInicio < existenteFin && nuevaFin > existenteInicio;
}La base de datos como última defensa
Un índice único sobre pista, fecha y hora de inicio impide dos reservas que empiecen a la vez. Si dos peticiones simultáneas pasan la comprobación, la segunda falla en la base de datos y el servidor lo traduce a un 409 en lugar de un error genérico.
Como el índice incluye las reservas canceladas, una franja cancelada se reutiliza actualizando su fila en vez de insertar otra.
if (error) {
// 23505 = condición de carrera: dos peticiones simultáneas pasaron el check
if (error.code === '23505') {
return { ok: false, status: 409, error: 'COURT_NOT_AVAILABLE',
message: 'Esa pista ya está reservada en ese horario' };
}
}Las reglas del club, en configuración
Horario semanal por día, días bloqueados, duraciones permitidas y antelación mínima se leen de la configuración y se cambian desde el panel, sin tocar código.
Cuentas y acceso
Registro con verificación de email obligatoria, sesiones con JWT de dos horas y recuperación de contraseña con tokens de un solo uso.
Tres roles (usuario, recepción y administración) aplicados con middlewares, y limitación de peticiones: 100 cada 15 minutos en general, 5 en autenticación y 10 en las rutas sensibles.
Límites conocidos
- La base de datos solo garantiza que no haya dos reservas con la misma hora de inicio. Dos peticiones simultáneas que se solapen con horas de inicio distintas dependen de la comprobación del servidor.
- El proyecto no tiene tests automatizados.