Backend — OptiRuta
API REST en Node.js + Express + TypeScript con Prisma 7 sobre PostgreSQL.
Para el README general del proyecto, ver ../README.md.
Tabla de contenidos
- Stack
- Estructura
- Arquitectura Clean
- Variables de entorno
- Modelo de datos (Prisma)
- Cómo correr
- Módulos
- El simulador
- Reglas de arquitectura
- Troubleshooting
Stack
- TypeScript estricto (
exactOptionalPropertyTypes: true) - Node.js 20+, modules ESM
- Express 5 para HTTP
- Prisma 7 (cliente generado en
src/generated/prisma) - PostgreSQL 15 vía Docker Compose
- jsonwebtoken para JWT, bcryptjs para hashes
- tsx como dev runner (sin Babel, sin reload manual)
Estructura
backend/
├── docker-compose.yml ← Postgres en 5433:5432
├── prisma/
│ ├── schema.prisma
│ └── migrations/
├── prisma.config.ts
└── src/
├── app.ts ← entry point: conecta DB, seed, lanza simulador, server
├── config/
│ ├── env.ts ← lectura de variables del .env
│ ├── jwt.ts ← JwtAdapter (sign / validate)
│ └── bcrypt.ts ← BcryptAdapter (hash / compare)
├── data/
│ ├── cache/ ← caches en memoria (notification-cache, route-state, inbox)
│ ├── mocks/ ← rutas, colonias, users de prueba
│ ├── postgres/ ← Prisma client singleton
│ ├── repositories/ ← impls reales (Prisma)
│ ├── seed/ ← seed del admin
│ └── simulation/ ← RouteSimulator (cron interno)
├── domain/
│ ├── dtos/ ← DTOs con validators estáticos
│ ├── errors/ ← CustomError
│ ├── repositories/ ← interfaces abstractas
│ └── use-cases/ ← lógica de negocio
└── presentation/
├── admin/ ← controller + routes
├── auth/
├── addresses/
├── feedback/
├── middlewares/ ← AuthMiddleware (validate + requireAdmin)
├── tracking/
├── routes.ts ← AppRoutes: une todos los módulos
└── server.ts ← clase Server (Express setup)
Arquitectura Clean
HTTP request
▼
presentation (Express controller)
▼
domain (use-case + DTO validator)
▼
data (repository impl / mock / cache)
Reglas
- El controller solo recibe
req, llama al use-case, responde JSON, maneja errores conhandleError(). - Los use-cases llevan toda la lógica de negocio.
- Los DTOs validan datos de entrada con métodos estáticos
static validate(data: unknown). - Los repositories son interfaces en
domain/repositories/; las implementaciones reales viven endata/. - Los errores se manejan con
CustomError:badRequest,unauthorized,forbidden,notFound,conflict,internalServer. - JWT solo se maneja desde
JwtAdapter. Bcrypt solo desdeBcryptAdapter. - Variables de entorno solo en
config/env.ts(excepción:.env).
Variables de entorno
Archivo .env (no se sube al repo):
PORT=8080
NODE_ENV=development
DATABASE_URL=postgresql://user:password@localhost:5433/optihack
POSTGRES_USER=user
POSTGRES_PASSWORD=password
POSTGRES_DB=optihack
JWT_SEED=your_super_secret_jwt_seed_here_change_in_production
JWT_EXPIRES_IN=7d
BCRYPT_ROUNDS=10
| Variable | Default | Notas |
|---|---|---|
PORT |
3000 |
Aquí usamos 8080 porque otra app ocupa 3000 |
DATABASE_URL |
— | Apunta a Postgres en 5433 (Docker mapping) |
JWT_SEED |
— | Requerido. Cambiar en producción |
JWT_EXPIRES_IN |
7d |
Duración del token |
BCRYPT_ROUNDS |
10 |
Costo del hash |
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB |
— | Usados por docker-compose.yml |
Modelo de datos (Prisma)
enum UserRole { USER ADMIN }
model User {
id Int @id @default(autoincrement())
name String
email String @unique
password String
phone String? @unique
role UserRole @default(USER)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
addresses Address[]
@@map("users")
}
model Address {
id Int @id @default(autoincrement())
userId Int
label String
street String
neighborhood String?
postalCode String?
latitude Float?
longitude Float?
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("addresses")
}
En el MVP solo usamos
User.Addressestá modelada pero los domicilios se guardan en mocks por ahora — se puede migrar sin tocar los controllers.
Cómo correr
1. Postgres con Docker
docker compose up -d
docker ps # verifica que postgres:15.3 esté Up
2. Migraciones + cliente
npm install
npx prisma migrate deploy
npx prisma generate
3. Dev
npm run dev
Output esperado:
Database connected
Seed admin: creado admin@test.com con password "admin123" (cambiar en producción)
RouteSimulator started — tick every 30s
Server is running on port 8080
4. Production build
npm run build # compila a dist/
npm start # corre node dist/app.js
Scripts disponibles
| Script | Hace |
|---|---|
npm run dev |
tsx watch src/app.ts — hot reload |
npm run build |
tsc — emite a dist/ |
npm start |
node dist/app.js |
Módulos
Auth (/api/auth/*)
- Registro: hashea password con
BcryptAdapter, crea con Prisma, agrega al mock de service conrouteIddefault =RUTA-01. Siempre rolUSER. - Login: compara password con bcrypt, genera JWT con
JwtAdapter.generate({ id, email }). El login response incluye elroledel DB. - GetMe: protegido con
AuthMiddleware.validate, devuelve datos del user logueado. - Si un user legacy no estaba en el mock, el login lo agrega (idempotente con
upsertUser).
Tracking (/api/tracking/*)
- POST
/gps-update: punto de entrada del simulador (o admin manual). Calcula ETA, evalúa notificaciones, escribe enroute-state+inbox. Respeta cancelación. - GET
/status: protegido. Devuelve la visión de túnel del user — solo su ruta, su ETA, sus notificaciones. - POST
/reset-demo: limpia caches y estados en memoria sin reiniciar el server.
Direcciones (/api/addresses/*)
- GET
/colonias: catálogo público (mock). - GET
/me: dirección actual del user. - PUT
/me: cambia la colonia del user. Valida contra el catálogo; rechaza colonias inexistentes (cumple "validación de zona permitida" del reto).
Feedback (/api/feedback/*)
- POST: envía feedback con tipo + mensaje + rating opcional. Tipos:
TRUCK_DID_NOT_PASS,RATING,SUGGESTION,OTHER. - GET
/me: feedbacks del user logueado (RBAC).
Admin (/api/admin/*)
Todos requieren AuthMiddleware.requireAdmin.
- GET
/routes: lista todas las rutas con estado actual (status, currentPositionId, arrivalResult, cancelled). - POST
/routes/:routeId/cancel: cancela la ruta. El simulador la pausa. Los users de esa ruta reciben notificación. - POST
/routes/:routeId/resume: reanuda. Simulator vuelve a positionId 1 y se limpia el dedup cache.
El simulador
data/simulation/route-simulator.ts
- Corre
setIntervalcada 30 segundos - Solo simula
RUTA-01por defecto (configurable enSIMULATED_ROUTE_IDS) - Cada tick: avanza el positionIndex de la ruta, dispara
ProcessGpsUpdateUseCasecon la posición correspondiente del mock - Cuando el ciclo se completa (positionIndex wrap a 0), limpia el dedup cache para que la siguiente vuelta vuelva a emitir notificaciones
- Respeta el flag
cancelleddel estado de ruta: si está cancelada, no avanza - El admin puede llamar
resetPosition(routeId)para volver el ciclo a 0
positionId 1 → (sin notif, salida del relleno)
positionId 2 → ROUTE_START "¡Ruta Iniciada!"
positionId 3 → (sin notif, en camino)
positionId 4 → TRUCK_PROXIMITY "Camión Cercano" (≈ 12 min de llegada)
positionId 5 → TRUCK_ARRIVED "El camión ya está aquí" (ETA = 0, arrivalResult = ARRIVED)
positionId 8 → ROUTE_COMPLETED "Servicio Finalizado"
wrap → cache cleared, loop back
Reglas de arquitectura
Las reglas que mantenemos a lo largo del proyecto:
- No lógica pesada en controllers. Solo: recibir, validar, llamar use-case, responder.
- Lógica de negocio en use-cases.
- Prisma solo en
data/repositoriesodata/postgres. - Use-cases dependen de interfaces (
domain/repositories/*), nunca de Prisma. - DTOs validan entrada con
static validate(). - Errores con
CustomError. - JWT solo via
JwtAdapter. Bcrypt solo viaBcryptAdapter. - No regresar passwords en ninguna respuesta.
- Las rutas en
presentation. Middlewares enpresentation/middlewares. - Variables de entorno solo en
config/env.ts. - No editar archivos generados (
src/generated/prisma/*).
Troubleshooting
| Síntoma | Causa | Fix |
|---|---|---|
Can't reach database server at localhost:5432 |
DATABASE_URL apunta a 5432 pero Docker mapea 5433 | Cambiar puerto en .env a 5433 |
Prisma client not found |
Cliente no generado | npx prisma generate |
Server arranca pero /api/... da 404 |
Olvidaste registrar la ruta en presentation/routes.ts |
Agregar router.use('/api/X', XRoutes.routes) |
| Login devuelve 400 con error de Prisma | Tabla users no existe |
npx prisma migrate deploy |
| Admin tab no aparece en el front | Login devolvió role distinto a "ADMIN" | Verifica que el seed haya corrido (mira el log de arranque) |
| Notificaciones repetidas no salen | Dedup cache | Esperar al wrap del simulador o llamar POST /api/tracking/reset-demo |
Para extender
- Añadir nuevo módulo X: copia la estructura de
feedback/(DTO → repo → use-case → controller → route → registrar enAppRoutes) - Migrar un mock a DB real: cambia solo la impl en
data/, las interfaces y use-cases no se tocan - Conectar Redis: nueva impl de
NotificationCacheRepositoryyRouteStateRepository, cambia elnew InMemory...pornew Redis...en el controller