Este es un backend modular construido con Express.js y TypeScript, diseñado para soportar aplicaciones multi-tenant (SaaS) con arquitectura escalable. El sistema maneja usuarios, aplicaciones, autenticación, correos electrónicos y cursos educativos en un contexto de multi-tenancia.
- Modularidad: Cada módulo es independiente y reutilizable
- Escalabilidad: Fácil de agregar nuevos módulos sin afectar existentes
- Mantenibilidad: Separación clara de responsabilidades (Controllers → Services → Database)
- Seguridad: Autenticación JWT, encriptación de contraseñas, validación de API keys
- Type Safety: TypeScript para prevenir errores en tiempo de desarrollo
backend/
├── src/
│ ├── app.ts # Punto de entrada de Express
│ ├── server.ts # Servidor Node.js
│ ├── router/
│ │ └── index.ts # Definición de rutas principales
│ │
│ ├── core/ # Funcionalidad compartida
│ │ ├── config/
│ │ │ └── env.ts # Variables de entorno
│ │ ├── db/
│ │ │ ├── index.ts # Conexión a base de datos
│ │ │ └── schema.ts # Definición de tablas (Drizzle ORM)
│ │ ├── middlewares/
│ │ │ ├── auth.middleware.ts # Validación de JWT
│ │ │ └── resolve-app.ts # Resolución de app por API key
│ │ └── types/
│ │ └── request.ts # Tipos de solicitud personalizados
│ │
│ ├── modules/ # Módulos de negocio
│ │ ├── auth/ # Autenticación
│ │ │ ├── auth.controller.ts
│ │ │ ├── auth.service.ts
│ │ │ ├── auth.routes.ts
│ │ │ ├── auth.middleware.ts
│ │ │ └── auth.types.ts
│ │ ├── mailer/ # Sistema de correos
│ │ │ ├── mailer.service.ts
│ │ │ ├── resend.provider.ts
│ │ │ ├── sendMail.controller.ts
│ │ │ └── queue/
│ │ │ ├── queue.ts # Cola de trabajos
│ │ │ └── worker.ts # Worker de procesamiento
│ │ ├── courses/ # Gestión de cursos
│ │ │ ├── courses.controller.ts
│ │ │ ├── courses.routes.ts
│ │ │ └── courses.service.ts
│ │ └── contact/ # Formulario de contacto
│ │ ├── contact.controller.ts
│ │ └── contact.routes.ts
│ │
│ └── types/
│ ├── auth-request.ts # Tipos para solicitud autenticada
│ └── express/
│ └── express.d.ts # Extensiones de tipos Express
│
├── drizzle/ # Migraciones de base de datos
│ ├── 0000_*.sql
│ ├── 0001_*.sql
│ └── meta/
│
├── package.json # Dependencias
├── tsconfig.json # Configuración TypeScript
└── drizzle.config.ts # Configuración de ORM
| Tecnología | Versión | Propósito |
|---|---|---|
| Node.js | LTS | Runtime de JavaScript |
| TypeScript | ^5.9.3 | Type safety y mejor experiencia de desarrollo |
| Express.js | ^5.2.1 | Framework web HTTP |
| PostgreSQL | - | Base de datos relacional |
| Librería | Versión | Propósito |
|---|---|---|
| Drizzle ORM | ^0.45.1 | ORM type-safe, migraciones |
| Drizzle Kit | ^0.31.10 | Herramientas CLI para Drizzle |
| pg | ^8.20.0 | Driver PostgreSQL |
| Librería | Versión | Propósito |
|---|---|---|
| bcrypt | ^6.0.0 | Hashing seguro de contraseñas |
| jsonwebtoken | ^9.0.3 | Generación y validación de JWT |
| cors | ^2.8.6 | Control de origen cruzado |
| Librería | Versión | Propósito |
|---|---|---|
| resend | ^6.12.3 | Proveedor de correos transaccionales |
| nodemailer | ^8.0.4 | Soporte alternativo para correos |
| Librería | Versión | Propósito |
|---|---|---|
| zod | ^4.3.6 | Validación de esquemas en runtime |
| dotenv | ^17.3.1 | Gestión de variables de entorno |
| Herramienta | Versión | Propósito |
|---|---|---|
| ts-node-dev | ^2.0.0 | Desarrollo con hot reload |
| @types/* | - | Tipos TypeScript para librerías |
Responsabilidad: Gestión de autenticación y autorización
auth.controller.ts- Controladores de login/registerauth.service.ts- Lógica de negocio (hash, JWT)auth.routes.ts- Rutas HTTPauth.middleware.ts- Validaciones personalizadasauth.types.ts- Tipos TypeScript
POST /api/auth/register
// Body: { email, password, appSlug }
// Retorna: { id, email, name, createdAt, token? }
POST /api/auth/login
// Body: { email, password, appSlug }
// Retorna: { token, user: { id, email, name } }# Registrar nuevo usuario
curl -X POST http://localhost:5000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"secure123","appSlug":"my-app"}'
# Response:
# {
# "id": 1,
# "email": "user@example.com",
# "name": "default",
# "createdAt": "2024-01-15T10:30:00Z"
# }Responsabilidad: Sistema de envío de correos electrónicos
mailer.service.ts- Lógica de creación y envío de emailsresend.provider.ts- Proveedor de email (Resend)sendMail.controller.ts- Controlador HTTPqueue/- Sistema de cola de trabajos
1. Cliente solicita envío de email
↓
2. Service guarda email como "pending" en DB
↓
3. Intenta enviar a través de Resend
↓
4. Si éxito: actualiza estado a "sent"
Si error: actualiza estado a "failed" con detalles del error
curl -X POST http://localhost:5000/api/send \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"to":"recipient@example.com",
"subject":"Bienvenido",
"body":"<h1>Hola!</h1>"
}'Responsabilidad: Gestión de cursos y lecciones educativas
courses.controller.ts- Controladores (get, create)courses.service.ts- Lógica de negociocourses.routes.ts- Rutas HTTP
GET /api/courses
// Retorna: Array de cursos
POST /api/courses
// Body: { title, description, appSlug }
// Retorna: { id, title, description, createdAt }curl http://localhost:5000/api/courses
# Response:
# [
# {
# "id": 1,
# "title": "Introducción a TypeScript",
# "description": "Aprende TypeScript desde cero",
# "appId": 1,
# "createdAt": "2024-01-15T10:30:00Z"
# }
# ]Responsabilidad: Gestión de formularios de contacto
contact.controller.ts- Controlador del formulariocontact.routes.ts- Rutas HTTP
POST /api/contact
// Body: { name, email, message }
// Integración: Envía email automáticamente via mailer.servicecurl -X POST http://localhost:5000/api/contact \
-H "Content-Type: application/json" \
-d '{
"name":"Juan Pérez",
"email":"juan@example.com",
"message":"Tengo una pregunta sobre su servicio"
}'id (PK) → Identificador único
name → Nombre del usuario
email (UNIQUE) → Email único
password → Contraseña hasheada
created_at → Timestamp de creaciónid (PK) → Identificador único
name → Nombre de la aplicación
slug (UNIQUE) → Identificador amigable (ej: "my-app")
created_at → Timestamp de creaciónid (PK) → Identificador único
user_id (FK) → Referencia a usuario
app_id (FK) → Referencia a aplicación
role → Rol del usuario (admin, user, etc)
created_at → Timestamp de creaciónid (PK) → Identificador único
user_id (FK) → Referencia a usuario
theme → Tema (light/dark)
language → Idioma (es/en)
shortcuts (JSONB) → Accesos directos personalizados
preferences (JSONB) → Preferencias adicionales
two_factor_enabled → 2FA habilitado
created_at → Timestamp de creaciónid (PK) → Identificador único
app_id (FK) → Referencia a aplicación
key (UNIQUE) → Clave API
name → Nombre descriptivo
active → Estado de la clave
created_at → Timestamp de creaciónid (PK) → Identificador único
app_id (FK) → Referencia a aplicación
to → Destinatario
subject → Asunto
body → Cuerpo del email
status → Estado (pending/sent/failed)
error → Mensaje de error (si aplica)
created_at → Timestamp de creaciónid (PK) → Identificador único
app_id (FK) → Referencia a aplicación
title → Título del curso
description → Descripción
created_at → Timestamp de creaciónid (PK) → Identificador único
course_id (FK) → Referencia a curso
title → Título de la lección
content → Contenido de la lección
created_at → Timestamp de creaciónHTTP Request
↓
Route Handler
↓
Controller (Validación básica, parseo)
↓
Service (Lógica de negocio)
↓
Database (Drizzle ORM)
↓
Response JSON
Ejemplo en auth.routes.ts:
router.post("/register", register); // ← RouteEjemplo en auth.controller.ts:
export const register = async (req: Request, res: Response) => {
const { email, password, appSlug } = req.body; // ← Parsing
const user = await registerUser(email, password, appSlug); // ← Service call
res.json(user);
};Ejemplo en auth.service.ts:
export const registerUser = async (email, password, appSlug) => {
const hashed = await bcrypt.hash(password, 10); // ← Business logic
const result = await db.insert(users).values({...}); // ← DB query
return result[0];
};// auth.middleware.ts - Validar JWT
export const authMiddleware = (req, res, next) => {
const token = req.headers["authorization"]?.split(" ")[1];
if (!token) res.status(401).json({ error: "Unauthorized" });
next();
};
// resolve-app.ts - Resolver app por API key
export const resolveApp = async (req, res, next) => {
const apiKey = req.headers["x-api-key"];
const key = await db.query.apiKeys.findFirst({...});
req.context = { appId: key.appId };
next();
};
// En router:
router.post("/send", resolveApp, sendMailController); // ← Middleware aplicado# .env
PORT=5000
DATABASE_URL=postgresql://user:password@localhost:5432/backend_db
JWT_SECRET=your-super-secret-key-change-in-production
CONTACT_EMAIL=contacto@tudominio.com
RESEND_API_KEY=your-resend-api-keygit clone <repo-url>
cd backendnpm installcp .env.example .env
# Editar .env con tus valores# Crear base de datos PostgreSQL
createdb backend_db
# Ejecutar migraciones
npm run db:migratenpm run dev
# El servidor inicia en http://localhost:5000npm run build
npm startsrc/modules/mi-modulo/
├── mi-modulo.controller.ts
├── mi-modulo.service.ts
├── mi-modulo.routes.ts
├── mi-modulo.types.ts
└── mi-modulo.middleware.ts (opcional)
export interface MiModuloData {
id: number;
nombre: string;
appId: number;
createdAt: Date;
}
export interface CreateMiModuloInput {
nombre: string;
appId: number;
}import { db } from "../../core/db";
import { miModuloTable } from "../../core/db/schema";
export const miModuloService = {
async getAll() {
return await db.select().from(miModuloTable);
},
async create(data: CreateMiModuloInput) {
const [result] = await db
.insert(miModuloTable)
.values(data)
.returning();
return result;
}
};import { Request, Response } from "express";
import { miModuloService } from "./mi-modulo.service";
export const getAll = async (req: Request, res: Response) => {
try {
const data = await miModuloService.getAll();
res.json(data);
} catch (err: any) {
res.status(500).json({ error: err.message });
}
};import { Router } from "express";
import { getAll } from "./mi-modulo.controller";
const router = Router();
router.get("/", getAll);
export default router;import miModuloRoutes from "../modules/mi-modulo/mi-modulo.routes";
router.use("/mi-modulo", miModuloRoutes);export const miModuloTable = pgTable("mi_modulo", {
id: serial("id").primaryKey(),
nombre: text("nombre").notNull(),
appId: integer("app_id").notNull().references(() => apps.id),
createdAt: timestamp("created_at").defaultNow(),
});npm run db:generate # Genera migración automáticamente
npm run db:migrate # Ejecuta la migracióncurl -X POST http://localhost:5000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "dev@codetlab.com",
"password": "SecurePass123!",
"appSlug": "main-app"
}'curl -X POST http://localhost:5000/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "dev@codetlab.com",
"password": "SecurePass123!",
"appSlug": "main-app"
}'curl -X POST http://localhost:5000/api/send \
-H "Content-Type: application/json" \
-H "X-API-Key: sk_test_xyz123" \
-d '{
"to": "usuario@example.com",
"subject": "Bienvenido a CodeTLab",
"body": "<h1>¡Hola!</h1><p>Gracias por registrarte</p>"
}'curl http://localhost:5000/api/coursescurl -X POST http://localhost:5000/api/contact \
-H "Content-Type: application/json" \
-d '{
"name": "Carlos López",
"email": "carlos@example.com",
"message": "Me gustaría saber más sobre sus servicios"
}'curl http://localhost:5000/api/health
# Response: { "status": "ok" }- Tokens JWT en cada solicitud protegida
- Header:
Authorization: Bearer <token> - Validado en
auth.middleware.ts
- Claves API por aplicación
- Header:
X-API-Key: <key> - Validado en
resolve-app.ts
- bcrypt con salt rounds: 10
- Nunca se envía la contraseña en respuestas
- Configurado en
app.ts - Permite solicitudes desde cualquier origen (ajustar en producción)
El sistema soporta múltiples aplicaciones (tenants) compartiendo una base de datos:
Una Aplicación (App) = Un Tenant
├── Múltiples Usuarios
├── Propia configuración
├── Propios cursos
└── Propias API keys
Flujo Multi-Tenancia:
Cliente solicita acceso
↓
Proporciona: email + appSlug (en auth)
O: X-API-Key (en requests)
↓
Sistema verifica app existe
↓
Sistema crea context con appId
↓
Todas las operaciones filtran por appId
# Desarrollo
npm run dev # Inicia servidor con hot reload
# Build
npm run build # Compila TypeScript a JavaScript
# Producción
npm start # Inicia servidor compilado
# Base de datos
npm run db:generate # Genera migraciones
npm run db:migrate # Ejecuta migraciones
npm run db:studio # Abre Drizzle Studio (GUI)R: Importa authMiddleware y úsalo en tu ruta:
router.get("/private", authMiddleware, miControlador);R: El resolveApp middleware agrega req.context.appId:
const appId = (req as any).context?.appId;R: En el service, lanza throw new Error() y en el controller capture:
catch (err: any) {
res.status(400).json({ error: err.message });
}R: Define un schema y úsalo en el controller:
const schema = z.object({ email: z.string().email() });
const { email } = schema.parse(req.body);- Crea una rama:
git checkout -b feature/nueva-funcionalidad - Haz commit de cambios:
git commit -am 'Agregar nueva funcionalidad' - Haz push:
git push origin feature/nueva-funcionalidad - Abre un Pull Request
ISC
Para preguntas o problemas, contacta al equipo de desarrollo o abre un issue en el repositorio.
Última actualización: Junio 2026 Versión de Documentación: 1.0.0