Files
hackathon-opti-1a67c9077937…/backend
2026-05-23 12:36:09 -06:00
..
2026-05-22 15:27:58 -06:00
2026-05-23 12:36:09 -06:00
2026-05-22 15:27:58 -06:00
2026-05-22 15:27:58 -06:00
2026-05-22 15:27:58 -06:00
2026-05-22 17:13:24 -06:00
2026-05-22 17:13:24 -06:00
2026-05-22 19:26:38 -06:00
2026-05-23 12:24:52 -06:00
2026-05-22 20:53:39 -06:00

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

  • 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

  1. El controller solo recibe req, llama al use-case, responde JSON, maneja errores con handleError().
  2. Los use-cases llevan toda la lógica de negocio.
  3. Los DTOs validan datos de entrada con métodos estáticos static validate(data: unknown).
  4. Los repositories son interfaces en domain/repositories/; las implementaciones reales viven en data/.
  5. Los errores se manejan con CustomError: badRequest, unauthorized, forbidden, notFound, conflict, internalServer.
  6. JWT solo se maneja desde JwtAdapter. Bcrypt solo desde BcryptAdapter.
  7. 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. Address está 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 con routeId default = RUTA-01. Siempre rol USER.
  • Login: compara password con bcrypt, genera JWT con JwtAdapter.generate({ id, email }). El login response incluye el role del 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 en route-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 setInterval cada 30 segundos
  • Solo simula RUTA-01 por defecto (configurable en SIMULATED_ROUTE_IDS)
  • Cada tick: avanza el positionIndex de la ruta, dispara ProcessGpsUpdateUseCase con 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 cancelled del 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:

  1. No lógica pesada en controllers. Solo: recibir, validar, llamar use-case, responder.
  2. Lógica de negocio en use-cases.
  3. Prisma solo en data/repositories o data/postgres.
  4. Use-cases dependen de interfaces (domain/repositories/*), nunca de Prisma.
  5. DTOs validan entrada con static validate().
  6. Errores con CustomError.
  7. JWT solo via JwtAdapter. Bcrypt solo via BcryptAdapter.
  8. No regresar passwords en ninguna respuesta.
  9. Las rutas en presentation. Middlewares en presentation/middlewares.
  10. Variables de entorno solo en config/env.ts.
  11. 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 en AppRoutes)
  • 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 NotificationCacheRepository y RouteStateRepository, cambia el new InMemory... por new Redis... en el controller