┌─────────────────────────────────────────────────────┐
│ Cliente HTTP │
└────────────────────┬────────────────────────────────┘
│
↓
┌───────────────────────┐
│ Express.js Server │
│ (Puerto 5000) │
└──────────┬────────────┘
│
┌────────────┼────────────┐
↓ ↓ ↓
┌────────┐ ┌────────┐ ┌────────┐
│ Authen │ │ Mailer │ │Courses │
│ Module │ │ Module │ │ Module │
└────┬───┘ └────┬───┘ └────┬───┘
│ │ │
└───────────┼───────────┘
↓
┌─────────────────┐
│ Service Layer │
│ (Lógica) │
└────────┬────────┘
↓
┌─────────────────────┐
│ Drizzle ORM │
│ (Query Builder) │
└────────┬────────────┘
↓
┌─────────────────────┐
│ PostgreSQL DB │
│ (Persistencia) │
└─────────────────────┘
- Minimalista: No impone demasiadas reglas, flexible
- Comunidad: Enorme ecosistema de middleware
- Performance: Muy rápido y ligero
- Madurez: Probado en producción por miles de empresas
- Fácil de aprender: Curva de aprendizaje muy baja
- NestJS: Muy pesado para este caso, overhead innecesario
- Fastify: Bueno pero menos comunidad que Express
- Hono: Moderno pero comunidad muy pequeña
Express.js es el estándar de facto para APIs REST en Node.js. Ideal para MVPs y aplicaciones medianas.
- Errores en tiempo de compilación: Atrapa bugs antes de producción
- Autocompletado: IDE proporciona sugerencias precisas
- Documentación viva: Los tipos sirven como documentación
- Refactorización segura: Cambiar código sin quebrar todo
- Mejor experiencia de equipo: Menos sorpresas en PRs
// SIN TypeScript - Bug invisible
const user = await db.query("SELECT * FROM users");
console.log(user.nombre); // ¿Existe? ¿Cuál es el nombre?
// CON TypeScript - Error en compilación
const user: User = await userService.getById(1);
console.log(user.name); // ✅ IDE sabe exactamente qué esTypeScript previene una clase entera de bugs en equipos grandes. Mejora mantenibilidad a largo plazo.
- ACID Compliance: Garantías de consistencia
- Relaciones complejas: Soporta N:M, jerarquías, etc.
- Tipos de datos avanzados: JSON, Arrays, UUID, etc.
- Full-text search: Búsqueda integrada
- Extensiones: PostGIS, UUID, etc.
- Open Source: Sin licencias costosas
- Escalabilidad: Replica, particionamiento
- MySQL: Más simple pero menos características
- MongoDB: Flexible pero sin transacciones ACID
- DynamoDB: Serverless pero vendor lock-in
Para aplicaciones con datos relacionados y requisitos de consistencia, PostgreSQL es superior. Mejor inversión a largo plazo.
- Type-safe queries: Validación de tipos en queries
- SQL legible: Genera SQL limpio y eficiente
- Migraciones: Versionado de esquema
- Sin decoradores: Menos "magia", más transparencia
- Lightweight: Mucho más pequeño que Prisma/TypeORM
// Drizzle - Type-safe
const users = await db
.select()
.from(users)
.where(eq(users.email, email));
// ✅ TypeScript sabe qué columnas existen
// Prisma - Type-safe pero con decoradores
@Entity()
class User {
@Column()
email: string;
}- Prisma: Más popular pero más overhead
- TypeORM: Muy complejo, muchas decoradores
- Sequelize: Sin type-safety real
Drizzle es la mejor opción para nuevos proyectos con TypeScript. Mejor balance entre potencia y simplicidad.
┌─────────────────┐
│ CONTROLLER │ ← HTTP, Parseo de parámetros
│ (Thin) │ Delegación al service
├─────────────────┤
│ SERVICE │ ← Lógica de negocio
│ (Fat) │ Validaciones complejas
│ │ Transacciones
├─────────────────┤
│ REPOSITORY │ ← Acceso a datos
│ (Thin) │ Queries SQL
└─────────────────┘
-
Testabilidad: Cada layer se prueba independiente
// Probar service sin HTTP const user = await registerUser("email", "pass", "app"); expect(user.id).toBeDefined();
-
Reutilización: Un service puede ser usado por múltiples controllers
// Email service usado por: auth, contact, admin await mailerService.create({...});
-
Mantenibilidad: Cambios en DB no afectan HTTP
-
Escalabilidad: Fácil agregar caché, queues, etc.
MVC es el patrón más probado en web. El Service Layer adicional es clave para código mantenible.
-- Una DB para todas las apps
users (id, email, password, ...)
apps (id, name, slug) ← Identificador del tenant
user_apps (user_id, app_id) ← Nexo
courses (id, app_id, ...) ← Todas referencian app_id
emails (id, app_id, ...)- Costo: Una sola DB reducida
- Mantenimiento: Actualizar schema una sola vez
- Datos compartidos: Reportes cross-tenant
- Respaldo: Una sola política de backup
- Queries complejas: Siempre filtrar por app_id
- Seguridad: Riesgo de data leaks si falla autorización
- Performance: Menos optimización que DB por tenant
// Cada query automáticamente filtra por app
const courses = await db
.select()
.from(courses)
.where(eq(courses.appId, req.context.appId)) // ← SIEMPRE
// Si olvidas esto... data leak
// Pero con TypeScript/arquitectura es difícil olvidarPara MVP/startups, una DB compartida es pragmática. Cuando crecimiento justifique, migrar a DB-per-tenant es posible.
// Flujo JWT
1. Cliente hace login
→ Server genera token + envía al cliente
→ Cliente almacena token (localStorage, cookie)
→ Cliente incluye token en cada request
→ Server valida token sin consultar DB- Stateless: No necesitas session storage
- Escalable: Múltiples servidores, sin sincronizar
- Mobile-friendly: Funciona con apps nativas
- CORS-friendly: No hay problemas de cookies
- Revocación: No puedes revocar un token antes de expiry
- Payload: Token viaja en cada request
| Feature | JWT | Sessions |
|---|---|---|
| Stateless | ✅ | ❌ |
| Escalable | ✅ | ❌ |
| Almacenamiento | ❌ | ✅ |
| Revocación | ❌ | ✅ |
Para API REST moderna, JWT es estándar. Sessions son para MVC tradicional.
const hashed = await bcrypt.hash(password, 10);
// 10 = salt rounds = tiempo de cálculo- Estándar: Usado en casi todo lado
- Lento: Resiste fuerza bruta (intencionalmente)
- Adecuado: Para contraseñas, no requiere hardware especial
| Algoritmo | Velocidad | Adopción | Uso |
|---|---|---|---|
| bcrypt | Lento ✅ | Altísima ✅ | Contraseñas |
| scrypt | Lento ✅ | Alta | Derivación clave |
| argon2 | Muy lento | Media | Criptografía seria |
bcrypt es suficiente y está probado. No hay razón para algo más complejo en este proyecto.
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
const validated = schema.parse(req.body);
// ✅ TypeScript sabe el tipo de 'validated'- Type-safe: Validación y tipos juntos
- Sincrónico: Más rápido que Joi/Yup
- Composable: Schemas reutilizables
- DX: Mejor experiencia de desarrollo
Zod es la mejor opción para TypeScript moderno.
// mailer.service.ts
try {
await sendEmail({...}); // Resend
status = "sent";
} catch {
// Fallback a Nodemailer
await nodemailer.send({...});
}- Resend: Moderno, excelente DX, gratis primeros emails
- Nodemailer: Fallback local (testing, desarrollo)
Combinación pragmática: Resend para producción, Nodemailer para desarrollo/pruebas.
// resolve-app.ts - Middleware que enriquece request
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 }; // ← Enriquecimiento
next();
};
// En rutas
router.post("/send", resolveApp, sendMailController);
// ↑ Middleware
// ↑ Ahora req.context.appId existe- DRY: No repetir lógica de resolución
- Centralizado: Un lugar para cambiar
- Composable: Múltiples middlewares en cadena
export const mailerService = {
async create(data) {
// 1. Guardar como pending
const email = await db.insert(emails).values({...});
try {
// 2. Intentar enviar
await sendEmail({...});
// 3. Actualizar estado
await db.update(emails).set({ status: "sent" });
return email;
} catch (err) {
// 4. Guardar fallo
await db.update(emails).set({ status: "failed", error: err.message });
throw err;
}
}
};- Audit trail: Todos los intentos guardados
- Recuperable: Reintentación de fallos fácil
- Observable: Reportes de email
// Middleware enriquece request
req.context = { appId: 123 };
// Todos los services acceden a context
const courses = await db.select()
.from(courses)
.where(eq(courses.appId, req.context.appId));- Seguridad: Imposible olvidar el filtro
- Coherencia: Un solo lugar define app actual
- Testing: Fácil mockear context
// ✅ CORRECTO
const hashed = await bcrypt.hash(password, 10);
await db.insert(users).values({ password: hashed });
// ❌ INCORRECTO (Nunca hagas esto)
await db.insert(users).values({ password: password });| Caso de Uso | Método |
|---|---|
| Usuario en frontend | JWT (stateless, CORS) |
| Servidor a servidor | API Key (identificación) |
| Admin en backend | JWT (con roles) |
// Frontend: JWT
fetch("/api/courses", {
headers: { "Authorization": "Bearer " + token }
});
// Server: API Key
fetch("/api/send", {
headers: { "X-API-Key": apiKey }
});// app.ts
app.use(cors()); // ← Permite CUALQUIER origen
// Producción: Restringir
app.use(cors({
origin: ["https://example.com", "https://app.example.com"]
}));// Hoy: Query a DB
const courses = await db.select().from(courses);
// Mañana: Redis
const courses = await cache.get("courses:app:123")
|| await db.select().from(courses);// Hoy: Síncrono
await mailerService.create({...});
// Mañana: Cola (Bull/BullMQ)
await emailQueue.add({ to, subject, body });// Hoy: Monolito
- auth module
- mailer module
- courses module
// Mañana: Microservicios
- auth-service (puerto 3001)
- mailer-service (puerto 3002)
- courses-service (puerto 3003)// NestJS = Over-engineered para este caso
@Controller('auth')
@Injectable()
export class AuthController {
@Post('register')
register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}
}
// EXPRESS = Simple y directo
router.post('/register', register);Veredicto: NestJS es overkill. Express es suficiente.
// GraphQL
{
user(id: 123) {
name
email
courses { title }
}
}
// REST
GET /api/users/123
GET /api/users/123/coursesVeredicto: REST es estándar, más simple de mantener. GraphQL para consultas complejas después.
Ventajas:
- Sin gestión de servidores
- Auto-scaling
- Pay per use
Desventajas:
- Cold starts (latencia inicial)
- Vendor lock-in
- Debugging más difícil
- Costo impredecible
Veredicto: Para MVP/pequeño, Express es más sencillo. Serverless después si lo necesitas.
- ✅ Express + TypeScript
- ✅ PostgreSQL + Drizzle
- ✅ JWT + bcrypt
- ✅ Multi-tenancia simple
- 🔜 Redis caché
- 🔜 Bull queues
- 🔜 Logging centralizado (Winston/Pino)
- 🔜 Trazabilidad distribuida (OpenTelemetry)
- 🔜 Microservicios
- 🔜 GraphQL optativo
- 🔜 Event streaming (Kafka)
- 🔜 DB sharding
Versión: 1.0.0 Última actualización: Enero 2024 Autor: CodetLab Team