API REST profesional para gestión hotelera: habitaciones, huéspedes y reservas.
- ¿Qué es HotelAPI?
- Stack tecnológico
- Arquitectura
- Estructura del proyecto
- Instalación y ejecución
- Variables de entorno
- Endpoints de la API
- Formato de respuestas
- Scripts npm
- Testing
- Documentación interactiva (Swagger)
- Seguridad
HotelAPI es una API REST construida como proyecto de portafolio profesional backend. Gestiona el ciclo completo de un hotel:
| Módulo | Funcionalidades |
|---|---|
| 🛏️ Rooms | CRUD, filtros por tipo/precio/estado, cambio de estado en tiempo real |
| 👤 Guests | Registro de huéspedes con historial de reservas vinculado |
| 📅 Bookings | Reservas con validación de disponibilidad de fechas, cálculo automático de precio |
Destacados técnicos:
- TypeScript estricto sin
any— errores capturados en compilación - Validación de datos en todos los endpoints con schemas Zod
- Formato de respuesta consistente
{ success, data | error, timestamp }en todos los endpoints - Documentación interactiva auto-generada con Swagger UI en
/api-docs - Tests unitarios, de integración y de propiedades con Vitest + Supertest
- Containerización completa con Docker + Docker Compose (un solo comando para levantar todo)
- Seguridad HTTP con Helmet + CORS + Rate Limiting integrados
| Tecnología | Versión | Propósito |
|---|---|---|
| Node.js | 20+ | Runtime base |
| TypeScript | 5.x | Lenguaje (modo strict) |
| Express | 5.x | Framework HTTP |
| MongoDB | 7.x | Base de datos NoSQL |
| Mongoose | 8.x | ODM — modelos + queries |
| Zod | 3.x | Validación de schemas en runtime |
| Vitest | latest | Tests unitarios y de propiedades |
| Supertest | latest | Tests de integración HTTP |
| fast-check | latest | Property-based testing |
| Swagger UI + swagger-jsdoc | latest | Documentación auto-generada |
| Winston | latest | Logging estructurado |
| Helmet | latest | Headers de seguridad HTTP |
| express-rate-limit | latest | Rate limiting por IP |
| Docker + Compose | 24+ | Containerización |
| ESLint + Prettier | latest | Calidad y formato de código |
| Husky + lint-staged | latest | Pre-commit hooks automáticos |
El sistema implementa arquitectura en capas con separación estricta de responsabilidades:
Cliente HTTP
│
▼
┌─────────────────────────────────────────┐
│ Presentation Layer │
│ Middleware (Helmet, CORS, Rate, │
│ Logger, Validator) + Routes + │
│ Controllers │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Business Logic Layer │
│ Services (Room, Guest, Booking) │
│ Reglas de negocio + disponibilidad │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Data Access Layer │
│ Mongoose Models + Schemas + Hooks │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ MongoDB 7.x │
└─────────────────────────────────────────┘
Flujo de un request:
Helmet→ añade headers de seguridadCORS→ valida origen del requestRate Limiter→ rechaza si supera 100 req/15min por IPexpress.json()→ parsea el bodyrequestLogger→ asigna X-Request-ID único y registra en logsvalidateRequest(schema)→ valida body/query con Zod (→ 400 si falla)Controller→ extrae datos y llama al ServiceService→ aplica lógica de negocio (disponibilidad, unicidad, etc.)Model/DB→ operación en MongoDBerrorHandler→ captura cualquier error y formatea la respuesta
hotel-api/
├── src/
│ ├── config/
│ │ ├── env.ts # Carga y valida variables de entorno con Zod
│ │ ├── database.ts # Conexión MongoDB con retry logic (backoff 1s×2^n, 5 intentos)
│ │ └── swagger.ts # Configuración de Swagger UI
│ │
│ ├── models/
│ │ ├── room.model.ts # Schema Mongoose: roomNumber(único), type, price, status
│ │ ├── guest.model.ts # Schema Mongoose: email(único), firstName, lastName, phone
│ │ └── booking.model.ts # Schema Mongoose: refs Room+Guest, fechas, totalPrice auto
│ │
│ ├── validators/
│ │ ├── room.validator.ts # Schemas Zod: createRoom, updateRoom, roomFilters
│ │ ├── guest.validator.ts # Schemas Zod: createGuest, updateGuest, guestFilters
│ │ ├── booking.validator.ts # Schemas Zod: createBooking, updateBooking, bookingFilters
│ │ ├── common.validator.ts # Schema Zod: paginación (page, limit≤100), sortBy
│ │ └── env.validator.ts # Schema Zod: variables de entorno requeridas
│ │
│ ├── services/
│ │ ├── room.service.ts # CRUD + filtros + cambio de estado
│ │ ├── guest.service.ts # CRUD + búsqueda + historial
│ │ └── booking.service.ts # CRUD + checkRoomAvailability + auto-update estado habitación
│ │
│ ├── controllers/
│ │ ├── room.controller.ts # Handlers HTTP: extrae params y llama RoomService
│ │ ├── guest.controller.ts # Handlers HTTP: extrae params y llama GuestService
│ │ ├── booking.controller.ts # Handlers HTTP: extrae params y llama BookingService
│ │ └── health.controller.ts # GET /api/v1/health: status DB + versión + uptime
│ │
│ ├── routes/
│ │ ├── index.ts # Router principal bajo prefijo /api/v1
│ │ ├── room.routes.ts # Rutas + validateRequest(schema) por ruta
│ │ ├── guest.routes.ts # Rutas + validateRequest(schema) por ruta
│ │ └── booking.routes.ts # Rutas + validateRequest(schema) por ruta
│ │
│ ├── middleware/
│ │ ├── validate.middleware.ts # validateRequest(schema): extrae body/query, valida con Zod
│ │ ├── error.middleware.ts # errorHandler: captura todos los errores, formatea ApiResponse
│ │ ├── logger.middleware.ts # requestLogger: X-Request-ID, logs HTTP con timing
│ │ └── notFound.middleware.ts # notFoundHandler: 404 para rutas no registradas
│ │
│ ├── utils/
│ │ ├── logger.ts # Winston configurado: JSON en prod, pretty en dev
│ │ ├── pagination.ts # buildPaginatedResult(), calcSkip()
│ │ ├── apiError.ts # AppError, ValidationError, NotFoundError, ConflictError, DatabaseError
│ │ └── asyncHandler.ts # Wrapper: captura errores async y los pasa a next()
│ │
│ ├── types/
│ │ └── express.d.ts # Extensión de tipos Express (X-Request-ID)
│ │
│ ├── app.ts # Crea Express app: Helmet→cors→rateLimit→json→logger→router→404→errors
│ └── server.ts # Conecta DB → inicia servidor → maneja SIGTERM/SIGINT
│
├── tests/
│ ├── unit/
│ │ ├── services/ # Tests servicios con mocks de Mongoose
│ │ ├── validators/ # Tests schemas Zod
│ │ └── utils/ # Tests paginación
│ ├── integration/
│ │ ├── rooms.api.test.ts # Tests E2E endpoints /rooms con mongodb-memory-server
│ │ ├── guests.api.test.ts # Tests E2E endpoints /guests
│ │ └── bookings.api.test.ts # Tests E2E endpoints /bookings
│ ├── properties/
│ │ ├── room.properties.test.ts # Property tests con fast-check
│ │ ├── guest.properties.test.ts
│ │ ├── booking.properties.test.ts
│ │ ├── pagination.properties.test.ts
│ │ └── serialization.properties.test.ts
│ └── helpers/
│ ├── testDb.ts # setupTestDb, teardownTestDb, clearTestDb (mongodb-memory-server)
│ ├── generators.ts # roomArbitrary, guestArbitrary, bookingArbitrary (fast-check)
│ ├── fixtures.ts # Datos fijos de prueba
│ └── setup.ts # Setup global de Vitest
│
├── .env.example # Plantilla con TODAS las variables documentadas
├── .env # Variables locales — NO commitear — en .gitignore
├── Dockerfile # Multi-stage: builder (npm ci + tsc) → production (distroless)
├── docker-compose.yml # Servicios: app + mongodb con healthcheck y volumen persistente
├── .dockerignore # Excluye node_modules, dist, .env, tests, coverage
├── tsconfig.json # strict:true, target:ES2020, outDir:dist
├── vitest.config.ts # coverage v8, threshold 80%, reporters text+json+html
├── .eslintrc.json # @typescript-eslint + no-any + import order
├── .prettierrc # singleQuote:true, semi:true, tabWidth:2
├── .gitignore # node_modules, dist, .env, coverage, *.log
└── package.json # Scripts: dev, build, start, test, test:coverage, lint, format
- Con Docker: Solo necesitas Docker Desktop instalado
- Sin Docker: Node.js 20+ + MongoDB 7 corriendo localmente
La forma más rápida — levanta la API + MongoDB con un solo comando:
# 1. Clonar el repositorio
git clone <url-del-repositorio>
cd hotel-api
# 2. Configurar variables de entorno
cp .env.example .env
# (edita .env si quieres cambiar algún valor)
# 3. Levantar todos los servicios en background
docker-compose up -d
# ✅ La API estará disponible en:
# http://localhost:3000/api/v1/health
# http://localhost:3000/api-docsComandos útiles con Docker:
docker-compose logs -f app # Ver logs en tiempo real
docker-compose ps # Ver estado de los servicios
docker-compose down # Detener (mantiene datos)
docker-compose down -v # Detener y borrar volúmenes (borra datos)
docker-compose build --no-cache # Reconstruir imagen# 1. Clonar e instalar dependencias
git clone <url-del-repositorio>
cd hotel-api
npm install
# 2. Configurar variables de entorno
cp .env.example .env
# Editar .env con tu DATABASE_URL local
# 3. Iniciar en modo desarrollo (hot-reload con tsx)
npm run dev
# ✅ API disponible en: http://localhost:3000Nota: Asegúrate de que MongoDB esté corriendo localmente. Instalación rápida:
brew install mongodb-community(macOS) o sigue la guía oficial.
Copia .env.example como .env y ajusta los valores:
# ── Servidor ────────────────────────────
PORT=3000
NODE_ENV=development # development | production | test
# ── Base de Datos ────────────────────────
DATABASE_URL=mongodb://localhost:27017/hotel-api
# Con Docker: DATABASE_URL=mongodb://mongodb:27017/hotel-api
# ── CORS ─────────────────────────────────
CORS_ORIGIN=* # Ejemplo producción: https://mi-frontend.com
# ── Logging ──────────────────────────────
LOG_LEVEL=info # error | warn | info | debug| Variable | Requerida | Default | Descripción |
|---|---|---|---|
PORT |
No | 3000 |
Puerto del servidor HTTP |
NODE_ENV |
No | development |
Entorno de ejecución |
DATABASE_URL |
Sí | — | URI de conexión MongoDB |
CORS_ORIGIN |
No | * |
Origen(s) permitidos para CORS |
LOG_LEVEL |
No | info |
Nivel mínimo de logging |
⚠️ SiDATABASE_URLfalta o tiene formato inválido, la app falla al iniciar con un mensaje descriptivo.
Base URL: http://localhost:3000/api/v1
| Método | Ruta | Descripción | Cuerpo | Respuesta |
|---|---|---|---|---|
POST |
/rooms |
Crear habitación | CreateRoomDTO |
201 + habitación creada |
GET |
/rooms |
Listar con filtros y paginación | — | 200 + lista paginada |
GET |
/rooms/:id |
Obtener por ID | — | 200 + habitación |
PUT |
/rooms/:id |
Actualizar datos | UpdateRoomDTO |
200 + habitación actualizada |
DELETE |
/rooms/:id |
Eliminar | — | 204 sin body |
PATCH |
/rooms/:id/status |
Cambiar solo el estado | { status } |
200 + habitación actualizada |
Query params disponibles para GET /rooms:
?type=single|double|suite|deluxe
&status=disponible|ocupada|mantenimiento|limpieza
&minPrice=50 (precio ≥ valor)
&maxPrice=500 (precio ≤ valor)
&available=true (solo status='disponible')
&search=vista (búsqueda en número y descripción)
&sortBy=price (price | roomNumber | createdAt)
&sortOrder=asc|desc
&page=1 (default: 1)
&limit=10 (default: 10, máx: 100)
Ejemplo de cuerpo para POST /rooms:
{
"roomNumber": "101",
"type": "double",
"price": 150,
"capacity": 2,
"description": "Habitación doble con vista al jardín",
"amenities": ["WiFi", "TV", "Aire acondicionado"]
}| Método | Ruta | Descripción | Cuerpo | Respuesta |
|---|---|---|---|---|
POST |
/guests |
Registrar huésped | CreateGuestDTO |
201 + huésped creado |
GET |
/guests |
Listar con búsqueda y paginación | — | 200 + lista paginada |
GET |
/guests/:id |
Obtener + historial de reservas | — | 200 + huésped con bookings |
PUT |
/guests/:id |
Actualizar datos | UpdateGuestDTO |
200 + huésped actualizado |
DELETE |
/guests/:id |
Eliminar | — | 204 sin body |
Query params para GET /guests:
?search=Juan (busca en firstName, lastName, email)
&sortBy=lastName|email|createdAt
&sortOrder=asc|desc
&page=1&limit=10
| Método | Ruta | Descripción | Cuerpo | Respuesta |
|---|---|---|---|---|
POST |
/bookings |
Crear reserva | CreateBookingDTO |
201 + reserva creada |
GET |
/bookings |
Listar con filtros y paginación | — | 200 + lista paginada |
GET |
/bookings/:id |
Obtener con guest y room populados | — | 200 + reserva completa |
PUT |
/bookings/:id |
Actualizar estado | UpdateBookingDTO |
200 + reserva actualizada |
DELETE |
/bookings/:id |
Cancelar (soft delete) | — | 200 + reserva cancelada |
PATCH |
/bookings/:id/status |
Cambiar solo el estado | { status } |
200 + reserva actualizada |
Query params para GET /bookings:
?guestId=<ObjectId>
&roomId=<ObjectId>
&status=pendiente|confirmada|check-in|check-out|cancelada
&checkInDate=2024-04-01 (ISO 8601)
&checkOutDate=2024-04-05 (ISO 8601)
&sortBy=checkInDate|checkOutDate|totalPrice|createdAt
&sortOrder=asc|desc
&page=1&limit=10
Ejemplo de cuerpo para POST /bookings:
{
"guestId": "65f1a2b3c4d5e6f7a8b9c0d1",
"roomId": "65f1a2b3c4d5e6f7a8b9c0d2",
"checkInDate": "2024-04-01",
"checkOutDate": "2024-04-05",
"numberOfGuests": 2,
"specialRequests": "Cama king si está disponible"
}Nota:
totalPricese calcula automáticamente:precio_habitación_por_noche × número_de_noches.
Lógica de estados: Al cambiar una reserva a
check-in, el estado de la habitación cambia automáticamente aocupada. Al cambiar acheck-out, la habitación pasa alimpieza.
| Método | Ruta | Descripción |
|---|---|---|
GET |
/api/v1/health |
Health check — estado de MongoDB, versión, uptime |
GET |
/api-docs |
Swagger UI — documentación interactiva de todos los endpoints |
Respuesta de /api/v1/health cuando todo está OK:
{
"success": true,
"data": {
"status": "ok",
"database": "connected",
"version": "1.0.0",
"timestamp": "2024-03-12T18:00:00.000Z",
"uptime": 3600
},
"timestamp": "2024-03-12T18:00:00.000Z"
}Todos los endpoints siguen el mismo contrato ApiResponse<T>:
✅ Respuesta exitosa (201 / 200):
{
"success": true,
"data": { ... },
"timestamp": "2024-03-12T18:00:00.000Z"
}✅ Respuesta exitosa con paginación (200):
{
"success": true,
"data": [ ... ],
"pagination": {
"total": 42,
"page": 1,
"limit": 10,
"totalPages": 5,
"hasNextPage": true,
"hasPrevPage": false
},
"timestamp": "2024-03-12T18:00:00.000Z"
}❌ Respuesta de error (400 / 404 / 409 / 429 / 500):
{
"success": false,
"error": {
"message": "Descripción del error",
"code": "VALIDATION_ERROR | NOT_FOUND | CONFLICT | RATE_LIMIT_EXCEEDED | INTERNAL_ERROR",
"details": [
{ "path": "price", "message": "Must be > 0", "received": -100 }
]
}
}# ── Desarrollo ───────────────────────────
npm run dev # Modo desarrollo con hot-reload (tsx watch)
# ── Producción ──────────────────────────
npm run build # Compila TypeScript → dist/
npm start # Ejecuta dist/server.js (requiere build previo)
# ── Testing ─────────────────────────────
npm test # Ejecuta todos los tests (unit + integration + properties)
npm run test:watch # Tests en modo watch (para desarrollo)
npm run test:coverage # Tests + reporte de cobertura HTML en /coverage
npm run test:unit # Solo tests unitarios
npm run test:integration # Solo tests de integración
npm run test:properties # Solo property tests (fast-check)
# ── Calidad ──────────────────────────────
npm run lint # Verifica código con ESLint
npm run lint:fix # Corrige errores de ESLint automáticamente
npm run format # Formatea código con Prettier
npm run typecheck # Verifica tipos TypeScript sin compilarEl proyecto usa una estrategia triple de testing:
| Tipo | Herramienta | Propósito |
|---|---|---|
| Unit tests | Vitest + mocks | Servicios y middleware aislados |
| Integration tests | Supertest + mongodb-memory-server | Endpoints HTTP completos sin Docker |
| Property tests | fast-check | 44 propiedades del sistema con 100 iteraciones c/u |
Ejecutar tests:
npm test # Todos los tests
npm run test:coverage # Con reporte de cobertura
# → Objetivo: ≥ 80% en líneas, funciones, branches, statementsSetup de base de datos en tests:
// Los tests de integración usan mongodb-memory-server
// No necesitan ninguna base de datos externa
beforeAll(async () => await setupTestDb());
afterAll(async () => await teardownTestDb());
afterEach(async () => await clearTestDb());Con la API corriendo, abre en el navegador:
http://localhost:3000/api-docs
Desde Swagger UI puedes:
- Ver todos los endpoints con parámetros, tipos y ejemplos
- Probar endpoints directamente desde el navegador (botón "Try it out")
- Ver los schemas de validación Zod de cada request body
- Ver todos los posibles códigos de respuesta HTTP
| Capa | Biblioteca | Protección |
|---|---|---|
| Headers HTTP | helmet |
Content-Security-Policy, X-Frame-Options, HSTS, XSS protection |
| CORS | cors |
Solo orígenes en CORS_ORIGIN pueden hacer requests |
| Rate Limiting | express-rate-limit |
Máx. 100 requests / 15 min por IP → responde con 429 |
| Trazabilidad | uuid v4 | Cada request tiene un X-Request-ID único en headers |
| Usuario Docker | — | El contenedor corre con usuario no-root appuser (UID 1001) |
RoomType: single · double · suite · deluxe
RoomStatus: disponible · ocupada · mantenimiento · limpieza
BookingStatus: pendiente · confirmada · check-in · check-out · cancelada
- Fork y clona el repositorio
- Instala dependencias:
npm install - Crea un branch:
git checkout -b feature/mi-mejora - Implementa con tests incluidos
- Verifica que todo pase:
npm test && npm run lint && npm run typecheck - Commit (los pre-commit hooks verifican lint y formato automáticamente)
- Abre un Pull Request con descripción clara del cambio
Proyecto de portafolio — Stack: Node.js 20 · TypeScript 5 · Express 5 · MongoDB 7 · Zod 3