Saltar al contenido principal

Arquitectura del Backend

El backend sigue una arquitectura por capas. El API de GraphQL de cada funcionalidad está construido de la misma forma, así que una vez que entiendes este patrón puedes orientarte en el código de backend de cualquier feature solo con ver la ruta y el sufijo del archivo — consulta la sección "Dónde vive esto" de cada feature para ver los archivos reales.

Flujo de una solicitud

GraphQL request


Resolver apps/backend/graphql/resolvers/*.resolver.js
│ (reads the auth context, calls a manager method, returns the result)

Manager apps/backend/managers/**/*.manager.js
│ (business logic: validates input, applies business rules, orchestrates)
├──▶ Validator apps/backend/validators/*.validator.js
├──▶ Access Service apps/backend/data-access-services/*.access-service.js
│ │
│ ▼
│ Model (Sequelize) apps/backend/database/models/*.js

└──▶ External Service apps/backend/services/*.service.js
(Firebase, Apple, SMS, Stripe, LiveKit, translations, push, etc.)

Las capas

1. Capa de GraphQL (graphql/)

  • graphql/typeDefs.js + graphql/types/*.type.js — definiciones de schema (SDL), un archivo por dominio
  • graphql/resolvers.js + graphql/resolvers/*.resolver.js — funciones resolver. Deben ser delgadas: revisar el contexto de auth si hace falta, llamar al método correspondiente del manager, devolver el resultado. La lógica de negocio no va aquí.
  • graphql/context/ — construye el contexto de cada solicitud. auth-helper.js resuelve el JWT del encabezado Authorization hacia el usuario autenticado; admin-auth-helper.js hace lo mismo para sesiones de admin; ws-context.js hace lo mismo para las suscripciones por WebSocket.

Ejemplo: graphql/resolvers/user.resolver.js expone register, login, loginWithApple, y demás — cada función resolver son solo unas líneas que delegan a AuthenticationManager.

2. Capa de Manager (managers/)

Lógica de negocio. Una clase por dominio, a veces agrupadas en subcarpetas (por ejemplo managers/user-managers/). Un manager:

  • Valida la entrada, normalmente mediante un validators/*.validator.js correspondiente
  • Lee y escribe datos a través de uno o más access services
  • Llama a services/ externos cuando hace falta (enviar un email, verificar un token con Firebase, cobrar una tarjeta, etc.)
  • Aplica las reglas de negocio reales — por ejemplo "rechazar el registro si el nombre de usuario ya está en uso"

Un manager nunca debería consultar un modelo de Sequelize directamente — eso es trabajo del access service.

Ejemplo: managers/user-managers/authentication.manager.js#register llama a data-access-services/user.access-service.js para revisar si el username/email ya está en uso, a validators/user.validator.js para validar el payload, encripta la contraseña, y luego guarda el nuevo usuario a través del access service.

3. Capa de Access Service (data-access-services/)

La única capa autorizada a hablar directamente con la base de datos. Aproximadamente una clase por modelo, que envuelve llamadas de Sequelize (findByPk, findOne, create, update, …) detrás de métodos simples y con nombre claro como findByUsername o create. Aquí no hay lógica de negocio ni validación — solo acceso a datos.

Ejemplo: data-access-services/user.access-service.js#findByUsername es esencialmente User.findOne({ where: { username } }).

4. Capa de Modelo (database/models/)

Definiciones de modelos de Sequelize — el schema real de la base de datos: campos, asociaciones, índices. Aproximadamente un archivo por tabla, por ejemplo database/models/user.js, database/models/UserSession.js, database/models/CoinPackage.js.

5. Servicios externos (services/)

Integraciones con proveedores externos que no son parte de los datos propios de Closegram: Firebase, Apple Sign-In, envío de SMS/OTP, Stripe, LiveKit, traducciones, notificaciones push, y similares. Los managers llaman a estos directamente — los access services nunca lo hacen.

6. Validators (validators/)

Validación de entrada usada por los métodos de los managers, un archivo por dominio, por ejemplo validators/user.validator.js.

Convenciones de nombres

SufijoCapaEjemplo
.resolver.jsResolver de GraphQLuser.resolver.js
.type.jsSchema de GraphQL (SDL)user.type.js
.manager.jsLógica de negocioauthentication.manager.js
.access-service.jsAcceso a base de datosuser.access-service.js
.service.jsIntegración externafirebase.service.js
.validator.jsValidación de entradauser.validator.js
(sin sufijo)Modelo de Sequelizedatabase/models/user.js

Pruebas

Las pruebas del backend viven en apps/backend/tests/, divididas en dos proyectos de Jest (ver jest.config.js): unit (tests/unit-test/**, resolvers/managers con todo lo que está debajo simulado con mocks, sin base de datos) e integration (tests/integration/**, el stack completo resolver → manager → access-service → Postgres contra una base de datos de prueba real + Redis, levantados con docker compose).

npm run test:unit # fast, DB-free — mocked managers/access-services
npm run test:integration # full stack against the Postgres test DB (manages Docker + migrations)
npm test # both projects

La cobertura se recolecta de graphql/resolvers/, managers/, validators/, services/ y data-access-services/ — la capa de modelo (database/models/) y el schema (graphql/types/) no están incluidos, ya que son mayormente declarativos.

Para la configuración completa — el ciclo de vida de la base de datos de prueba/Docker (scripts/test.sh, .env.test, los contenedores postgres-test/redis-test), los helpers del servidor de prueba de Apollo, las convenciones de nombres, los reporters/umbrales de cobertura, y la referencia completa de comandos — consulta la Guía de pruebas.

Una limitación específica del backend que vale la pena conocer: como las pruebas unitarias simulan (mock) el manager, no pueden detectar un método de manager que ningún resolver llega a llamar realmente — el problema de las "funcionalidades no conectadas" de la sección anterior. Solo una prueba de integración que de verdad invoque el resolver, o una auditoría manual como la que está detrás de este sitio de documentación, deja ver ese vacío.

Detectar automáticamente el desfase entre schema y resolvers

npm run check:schema (apps/backend/scripts/check-schema-resolvers.js) compara de forma estática cada campo Query/Mutation declarado en graphql/types/*.js contra las claves de resolver que realmente se exportan desde graphql/resolvers/*.js, y reporta cualquier campo sin resolver correspondiente — exactamente el tipo de vacío detrás de los mismatches de nombres setupTwoFactor / terminateSession / generateBackupCodes mencionados antes. Es un script de Node simple (no necesita base de datos ni test runner), así que es seguro correrlo en cualquier momento.

Viene con un archivo de baseline (scripts/schema-resolver-baseline.json) que guarda los vacíos ya conocidos al momento de esta auditoría (2026-07-10) — el script solo falla (código de salida 1) con vacíos que NO estén ya en ese baseline, así que hoy no bloquea nada, pero sí detecta desfases nuevos de aquí en adelante. Si arreglas uno de los items del baseline, quítalo del JSON en el mismo PR.

Una advertencia: esto solo detecta campos que SÍ están declarados en el schema pero sin resolver. No puede detectar una funcionalidad que falta por completo del schema — varias funcionalidades cubiertas en esta documentación (Ubicación, Colaboradores de posts, Historias y En vivo, entre otras) tienen un manager y un access service implementados pero nunca se agregaron al schema de GraphQL en absoluto, así que no hay ningún campo de schema con el cual este script pueda comparar. Esos casos por ahora solo se detectan con una auditoría como la que está detrás de estos docs, o comparando a mano la lista de archivos de manager/access-service contra el schema.

No todas las funcionalidades tienen todas las capas

Una funcionalidad completamente conectada normalmente tiene un resolver + manager + access service (+ modelo). Varias funcionalidades documentadas en este sitio están solo parcialmente conectadas: tienen un manager y un access service que implementan la lógica, pero ningún resolver la expone en el schema de GraphQL — así que la funcionalidad existe en el código pero no es alcanzable desde el API en absoluto. El "Checklist de implementación" y el "Checklist de implementación técnica" de cada funcionalidad señalan esto en cada item; cuando ves una nota como "la lógica del manager existe, pero no hay resolver," es exactamente este tipo de vacío.