Scoutcamp es el proyecto de fin de grado (TFG) con el que digitalicé el ciclo completo de reserva y gestión de campamentos scout: descubrir alojamientos, comprobar disponibilidad por fechas, reservar parcelas o bungalows, y que el propietario gestione solicitudes desde un panel propio. Sustituye hojas de cálculo, correos sueltos y confirmaciones manuales por un flujo centralizado en la web.
Demo en producción: scoutcamp.alejandroquintana.dev. Código en frontend y backend.
Dominio y actores
El modelo de datos gira en torno a:
- Camping — ficha del campamento (ubicación con índice
2dsphere, imágenes, normas, horarios, métodos de pago, propietario). - CampingLodging — tipos de alojamiento: parcela (
campsite), bungalow uother, con capacidad y tarifa por noche. - CampingUnit — unidades reservables concretas dentro de cada lodging.
- Booking — reserva con fechas, unidades, responsable,
coste total, método de pago y estado (
pending,accepted,rejected,cancelled). - CampingRelation — favoritos y reseñas por usuario/camping.
Roles de usuario (RBAC sencillo pero efectivo):
- user — busca campings, reserva, consulta sus bookings, conversaciones y perfil.
- manager — crea y edita sus campings, gestiona lodgings/unidades, acepta o rechaza reservas, mensajería.
- admin — panel global: usuarios, campings, reservas y conversaciones.
El reto técnico
- Búsqueda contextual: filtrar por fechas de entrada/salida,
ubicación geográfica (
$geoNear) y disponibilidad real de unidades en ese rango. - Evitar sobreventa: al crear una reserva, validar que hay suficientes unidades libres antes de persistir.
- Listados ricos: mostrar campings con alojamientos, capacidad agregada y valoraciones sin disparar consultas N+1 ingenuas.
- Autorización por propietario: solo el
ownerdel camping modifica fichas o cambia el estado de reservas ajenas. - Internacionalización: API con
i18n(es, en, fr, de) para emails; frontend con@ngx-translate. - Medios: subida de imágenes a Google Cloud Storage vía
modelo
Documenty Multer.
Arquitectura del sistema
SPA Angular 18 contra API REST Express 5
con prefijo /api/v1. Autenticación JWT: middleware
checkUser decodifica el token en cada petición;
authMiddleware protege rutas que exigen sesión.
flowchart TB
subgraph client [Angular 18 SPA]
Public[Listado y reserva]
Manager[Panel manager]
Admin[Panel admin]
end
subgraph api [Node Express 5]
Auth[JWT checkUser]
Routes[api/v1 routes]
Ctrl[Controllers y validators]
end
subgraph data [Persistencia]
Mongo[(MongoDB Mongoose 9)]
GCS[Google Cloud Storage]
Mail[Nodemailer]
end
Public --> Routes
Manager --> Routes
Admin --> Routes
Routes --> Auth
Routes --> Ctrl
Ctrl --> Mongo
Ctrl --> GCS
Ctrl --> Mail
Stack tecnológico
- Frontend: Angular 18, TypeScript 5.4, Angular Material,
Bootstrap 5, RxJS,
@ngx-translate, Google Maps, JWT con@auth0/angular-jwt. - Backend: Express 5, Mongoose 9, JWT, bcrypt,
express-validator, Multer,
@google-cloud/storage, i18n, Morgan, CORS.
Backend: API y lógica de negocio
Rutas principales montadas en index.js:
/api/v1/— login y signup (auth.routes).-
/api/v1/campings— CRUD de campings, lodgings, bookings, reseñas (público y autenticado según endpoint). /api/v1/users— perfiles y reservas de usuario./api/v1/conversations— mensajería entre usuarios./api/v1/documents— subida y descarga de ficheros en GCS.
Todos los modelos extienden un databaseSchema base con método
estático search(): paginación, ordenación, filtros y populate
reutilizables en listados de admin y usuario.
Flujo de reserva (createBooking)
- El cliente envía fechas, lodgings solicitados, datos del responsable y método de pago.
-
El servidor obtiene lodgings disponibles con
CampingLodging.getAvailableLodgingsy unidades libres conCampingUnit.getAvailableUnits. - Si no hay stock suficiente para algún lodging, responde con error (no se guarda la reserva).
-
Calcula
totalCost(noches × tarifa × unidades) y crea el booking en estadopending. -
Envía email al propietario del camping en su idioma (
i18n.setLocale+ Nodemailer).
Agregaciones en listado de campings
Camping.getCampings construye un pipeline de agregación con
$lookup a lodgings, units y relations; calcula valoración media;
opcionalmente aplica $geoNear si hay coordenadas; y filtra campings
con bookings que solapan las fechas buscadas. Así el listado público llega
enriquecido en una sola ida a la base de datos.
Frontend: módulos y rutas
La app Angular se organiza por dominios:
- camping —
campings-list, fichacamping-view(galería, mapa, reseñas, booking), paso finalcamping-booking. - manager — mis campings, crear/editar camping, gestión de alojamientos y conversaciones.
- admin — usuarios, campings, reservas y conversaciones globales.
- auth / user — registro, login, perfil y área de usuario.
Componentes compartidos: buscador de campings, tablas reutilizables, panel
lateral (left-menu), selector de idioma y barra de navegación.
Retos resueltos
1. Disponibilidad sin sobreventa
Problema: dos usuarios podrían intentar reservar la última parcela en las mismas fechas.
Solución: antes de booking.save(), comprobar
que cada lodging solicitado tiene availables mayor o igual a
la cantidad pedida; asignar solo unidades devueltas por
getAvailableUnits. Si falla, lanzar Unauthorized
sin escribir en base de datos.
2. Listados sin N+1 naive
Problema: mostrar campings con alojamientos, unidades y ratings en listados paginados con muchas consultas encadenadas.
Solución: aggregation pipeline en getCampings
con $lookup, $addFields para capacidad total y
media de reseñas, y filtro de bookings solapados por fechas.
3. Autorización y ownership
Problema: un usuario no debe modificar campings o reservas ajenas.
Solución: JWT en cabecera Authorization;
comprobaciones camping.owner.equals(req.user._id) en gestión
de bookings y edición de fichas; rol admin con acceso ampliado
en listados de usuario.
4. Multilenguaje
Problema: propietarios y usuarios con distintos idiomas, especialmente en notificaciones por email.
Solución: paquete i18n en el backend (locales
es, en, fr, de) para asuntos y cuerpos de correo; frontend con
@ngx-translate y selector de idioma en la UI.
5. Imágenes y documentos
Problema: cada camping necesita galería de fotos almacenada de forma fiable.
Solución: subida con Multer al buffer, stream a bucket GCS
(scoutcamp_bucket), metadatos en colección Document
referenciados desde el camping.
Conclusión
Scoutcamp consolidó el diseño de una API REST con Mongoose, agregaciones para consultas complejas y una SPA Angular modular por roles. Como TFG, demostró que se puede cubrir búsqueda, reserva, gestión del propietario y administración global en un solo producto coherente, sin inflar el modelo con jerarquías organizativas que el dominio no requería.
Aprendizajes clave: modelar disponibilidad en el servidor, enriquecer listados con pipelines en lugar de joins manuales en el cliente, y separar claramente los módulos público, manager y admin en el frontend.