Desarrollo Web Backend
Parte 2
Clase 7 — Bases de datos, JWT, seguridad y renderizado (SPA · SSR · SSG)
Objetivos de aprendizaje
Tu backend de la Clase 6 vivía en un array volátil. Hoy aprendemos apersistir datos, identificar usuarios y cerrar los huecos de seguridad más obvios.
SQL vs NoSQL
Diferenciar bases de datos relacionales y documentales con criterio para elegir.
ORM con Sequelize
Conectar una base de datos real a tu API y modelar relaciones con código.
JWT
Implementar autenticación stateless con tokens firmados y bcrypt.
Seguridad
Variables de entorno, validación, rate limiting y headers seguros.
Renderizado
Comparar SPA,SSR ySSG — y por qué el SSR depende de la BD.
Bases de datos SQL (relacionales)
Datos en tablas con un esquema fijo. La fuerza son lasrelaciones: conectar tablas con JOIN para responder preguntas complejas.
-- Definir la tablaCREATE TABLE usuarios ( id SERIAL PRIMARY KEY, nombre VARCHAR(100) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, edad INT, creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP);-- CRUD básicoINSERT INTO usuarios (nombre, email, edad)VALUES ('Ana', 'ana@email.com', 25);SELECT nombre, email FROM usuariosWHERE edad > 20;UPDATE usuarios SET edad = 26WHERE email = 'ana@email.com';DELETE FROM usuarios WHERE id = 1;Ventaja principal
Relaciones: con un JOIN puedes responder "todos los pedidos de Ana con sus productos y precios" en una sola consulta.
Esquema explícito
La estructura está fijada por adelantado. Cambiarla en producción requiere migraciones.
Implementaciones
PostgreSQL (la más recomendada), MySQL, SQLite, SQL Server.
Bases de datos NoSQL (documentales)
Datos guardados como documentoscon forma libre. Cada documento puede contener arrays y objetos anidados.
// Un documento en MongoDB{ "_id": "64a5b3c2...", "nombre": "Ana", "email": "ana@email.com", "direcciones": [ { "calle": "Av. Principal 123", "ciudad": "Lima" } ], "preferencias": { "tema": "oscuro", "idioma": "es" }}Esquema flexible
Cada documento puede tener campos distintos. Ideal cuando los datos cambian a menudo o varían entre registros.
Lectura en una sola operación
Como las direcciones viven dentro del usuario, no necesitas JOIN para verlas.
Implementaciones
MongoDB (documental), Redis (clave-valor), Cassandra (columnar), DynamoDB.
En SQL necesitarías tablas separadas para las direcciones y un JOIN; aquí están dentro del propio documento.
¿Cuándo elegir cada una?
Una decisión que define cómo modelas el dominio durante meses. Hay reglas prácticas.
| Criterio | SQL | NoSQL |
|---|---|---|
| Datos altamente relacionados | Sí | No ideal |
| Esquema estable | Sí | No necesario |
| Datos flexibles | Rígido | Sí |
| Consultas complejas | Excelente | Limitadas |
Relaciones claras → SQL
Empieza por PostgreSQL. Cubre el 80% de los proyectos.
Datos documentales → NoSQL
Catálogos heterogéneos, logs, feeds sociales: MongoDB brilla.
¿Dudas? → PostgreSQL
También guarda JSON nativo. Te llevas lo mejor de los dos mundos.
Dominio: usuarios y sus direcciones. Mismo dato, dos formas de modelarlo.
-- Dos tablas relacionadasCREATE TABLE usuarios ( id SERIAL PRIMARY KEY, nombre VARCHAR(100), email VARCHAR(255) UNIQUE); CREATE TABLE direcciones ( id SERIAL PRIMARY KEY, usuario_id INT REFERENCES usuarios(id), calle VARCHAR(200), ciudad VARCHAR(100));-- Consulta con JOINSELECT u.nombre, d.ciudadFROM usuarios uJOIN direcciones d ON d.usuario_id = u.idWHERE u.email = 'ana@email.com'; -- Resultado nombre | ciudad --------+-------- Ana | Lima// Un único documento{ "_id": "64a5b3c2...", "nombre": "Ana", "email": "ana@email.com", "direcciones": [ { "calle": "Av. Principal 123", "ciudad": "Lima" } ], "preferencias": { "tema": "oscuro" }}// Consulta directadb.usuarios.findOne({ email: "ana@email.com"}) // El documento ya trae// sus direcciones. Sin JOIN. usuario.direcciones[0].ciudad// → "Lima"SQL — fortalezas
Esquema estricto, integridad referencial, consultas analíticas potentes con JOIN, agregaciones y subqueries.
NoSQL — fortalezas
Esquema flexible, lectura en una sola operación, ideal para datos jerárquicos o documentales que cambian con el tiempo.
📚Material complementario▾
ORMs: la base de datos hablada en código
Un ORM(Object-Relational Mapper) te deja escribir consultas en JavaScript/TypeScript en vez de SQL crudo, manipulando modelos como objetos.
Por qué un ORM
Evitas concatenar SQL (y con ello inyecciones), trabajas con objetos, gestionas relaciones y migraciones desde el propio código.
Sequelize
El ORM clásico de Node, maduro y muy usado en producción. Soporta PostgreSQL, MySQL, SQLite y SQL Server con la misma API.
Otros nombres
Prisma (schema declarativo), TypeORM, Drizzle (más bajo nivel), Mongoose (para MongoDB).
# Instala Sequelize y el drivernpm install sequelize sqlite3# Para PostgreSQL en producción:npm install sequelize pg pg-hstore# CLI opcional para migraciones:npm install -D sequelize-clinpx sequelize-cli initLos modelos se definen en código
A diferencia de Prisma, Sequelize no usa un schema declarativo aparte: cada modelo es un objeto JavaScript creado con sequelize.define(). Las relaciones se enlazan después con hasMany, belongsTo, etc.
// src/db.jsconst { Sequelize, DataTypes } = require('sequelize');const sequelize = new Sequelize({ dialect: 'sqlite', storage: './dev.db'});const Usuario = sequelize.define('Usuario', { nombre: { type: DataTypes.STRING, allowNull: false }, email: { type: DataTypes.STRING, unique: true, allowNull: false }, password:{ type: DataTypes.STRING, allowNull: false }});const Tarea = sequelize.define('Tarea', { titulo: { type: DataTypes.STRING, allowNull: false }, completada: { type: DataTypes.BOOLEAN, defaultValue: false }});// Relación 1-N: un usuario tiene muchas tareasUsuario.hasMany(Tarea, { foreignKey: 'usuarioId' });Tarea.belongsTo(Usuario, { foreignKey: 'usuarioId' });module.exports = { sequelize, Usuario, Tarea };Sequelize añade automáticamente id, createdAt yupdatedAt a cada modelo.
Sequelize en tu API Express
Importas los modelos y los usas como objetos. sequelize.sync()crea las tablas si no existen — útil en desarrollo; en producción se usan migraciones.
// Crear usuario con tareasconst { sequelize, Usuario, Tarea } = require('./db');await sequelize.sync(); // devapp.post('/api/usuarios', async (req, res) => { const usuario = await Usuario.create({ nombre: req.body.nombre, email: req.body.email, Tareas: [{ titulo: 'Tarea de ejemplo' }] }, { include: [Tarea] }); res.status(201).json(usuario);});// Listar tareas con filtrosapp.get('/api/tareas', async (req, res) => { const where = {}; if (req.query.completada === 'true') { where.completada = true; } const tareas = await Tarea.findAll({ where, include: Usuario, order: [['createdAt', 'DESC']] }); res.json(tareas);});📚Material complementario▾
Autenticación vs Autorización
Autenticación
Verificar QUIÉN eres. El login. "¿Eres realmente Ana?"
Autorización
Verificar QUÉ puedes hacer. Los permisos. "¿Ana puede borrar este post?"
JWT — JSON Web Tokens
Un token firmado que contiene información del usuario. El servidor lo emite en el login y el cliente lo presenta en cada petición.
// Estructura: tres partes separadas por puntos[Header].[Payload].[Signature]eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.firma_aqui// ^header ^payload ^firmaEl token es legible (base64), no cifrado. Por esonunca pongas secretos en el payload — sólo identificadores.
Flujo completo de autenticación
Pulsa Iniciar sesión y observa cómo viaja el token entre el cliente y el servidor.
POST /api/auth/login
Envía { email, password } al servidor.
bcrypt.compare(password, hash)
Verifica que la contraseña coincide con el hash guardado.
jwt.sign({ userId }, SECRET)
Firma y emite un token con expiración (ej. 24h).
Guarda el token
Lo guarda en memoria o cookie httpOnly y se lo lleva a cada petición.
GET /api/perfil
Authorization: Bearer <token>
jwt.verify(token, SECRET)
Si el token es válido, deja pasar; si no, responde 401.
El token viaja en cada petición: el servidor no guarda sesión en memoria — por eso JWT esstateless.
Registro y login con bcrypt
Las contraseñas nunca se guardan en texto plano: se hashean con bcrypt y al hacer login se comparan hash contra hash.
// Registroconst jwt = require('jsonwebtoken');const bcrypt = require('bcrypt');const { Usuario } = require('./db');const SECRET = process.env.JWT_SECRET;app.post('/api/auth/registro', async (req, res) => { const { nombre, email, password } = req.body; const passwordHash = await bcrypt.hash(password, 10); const usuario = await Usuario.create({ nombre, email, password: passwordHash }); res.status(201).json({ mensaje: 'Usuario creado', id: usuario.id });});// Loginapp.post('/api/auth/login', async (req, res) => { const { email, password } = req.body; const usuario = await Usuario.findOne({ where: { email } }); if (!usuario) return res.status(401).json({ error: 'Credenciales invalidas' }); const valido = await bcrypt.compare(password, usuario.password); if (!valido) return res.status(401).json({ error: 'Credenciales invalidas' }); const token = jwt.sign( { userId: usuario.id, email: usuario.email }, SECRET, { expiresIn: '24h' } ); res.json({ token });});⚠️ Regla de oro
NUNCA guardes contraseñas en texto plano. Siemprebcrypt.hash(password, 10). Y nunca devuelvas el hash al cliente.
Middleware autenticar y rutas protegidas
Un middleware que se enchufa antes de la ruta. Si el token es válido, deja pasar; si no, corta con 401.
// Middleware de autenticaciónconst autenticar = (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Token requerido' }); } try { const token = authHeader.split(' ')[1]; req.usuario = jwt.verify(token, SECRET); next(); } catch (error) { res.status(401).json({ error: 'Token invalido' }); }};// Ruta protegidaapp.get('/api/perfil', autenticar, (req, res) => { res.json({ usuario: req.usuario });});El truco
El segundo argumento de app.get es el middleware. Express ejecutaautenticar antes del handler. Si llama a next()sigue; si responde, no.
req.usuario
Tras jwt.verify, guardamos el payload decodificado enreq.usuario. Cualquier ruta protegida ya sabe quién pide.
📚Material complementario▾
- InteractivoJWT.io — Decoder & docs
- DocsRFC 7519 — JSON Web Token
- Docsbcrypt — npm
- Docsjsonwebtoken — npm
- ArtículoOWASP — Authentication Cheat Sheet
Seguridad básica
Tres capas que tu API necesita antes de salir a producción. Ninguna sustituye a las otras: trabajan en conjunto para reducir la superficie de ataque.(El CORS ya quedó cubierto en la Clase 6.)
Variables de entorno
Sacar los secretos del código fuente con .env y dotenv.
Validación con Zod
Nunca confiar en req.body: definir un esquema y rechazar lo demás.
Rate limiting + Helmet
Frenar fuerza bruta y endurecer los headers HTTP con dos líneas.
Variables de entorno
NUNCA pongas secretos directamente en el código. Si llegan al repositorio, asume que están comprometidos.
# .env (fuera del repositorio)DATABASE_URL=postgresql://user:password@localhost:5432/midbJWT_SECRET=un-secreto-muy-largo-y-aleatorio-aquiPORT=3000// Cargar al inicio de tu apprequire('dotenv').config();const secret = process.env.JWT_SECRET;const dbUrl = process.env.DATABASE_URL;📊 .gitignore obligatorio
Añade .env a tu .gitignore antes del primer commit. Para compartir las variables sin el valor, mantén un.env.example con las claves vacías.
Validación de entrada con Zod
Nunca confíes en req.body. Defínelo con un esquema y rechaza cualquier cosa que no encaje.
const { z } = require('zod');const esquemaUsuario = z.object({ nombre: z.string().min(2).max(100), email: z.string().email(), edad: z.number().int().min(18).optional()});app.post('/api/usuarios', (req, res) => { const resultado = esquemaUsuario.safeParse(req.body); if (!resultado.success) { return res.status(400).json({ errores: resultado.error.errors }); } // resultado.data tiene los datos validados});Body JSON
// Esquema aplicadoz.object({ nombre: z.string().min(2).max(100), email: z.string().email(), edad: z.number().int().min(18).optional()})Resultado
✓ Datos válidos
Prueba a poner un email inválido, edad < 18 o un nombre vacío.
Rate limiting y headers seguros
Dos middlewares de una línea cada uno que cubren un puñado de vectores comunes.
const rateLimit = require('express-rate-limit');const helmet = require('helmet');// Headers seguros (CSP, X-Frame, etc.)app.use(helmet());// 100 peticiones / IP / 15 minutosapp.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100}));helmet
Configura headers como X-Content-Type-Options,Strict-Transport-Security y una CSP básica. Una línea cubre buena parte del top OWASP.
rate limiting
Frena fuerza bruta sobre login, abuso de la API y scraping. Aplica límites más estrictos en /auth/login.
Lo siguiente
HTTPS, sanitización de logs, CSRF (si usas cookies de sesión), backups y monitoreo. La seguridad es continua, no un checklist final.
📚Material complementario▾
Renderizado en la web
¿Dónde se construye el HTML que ve el usuario? Tres estrategias clásicas:SPA,SSR ySSG.
¿Por qué hablamos de esto en una clase de backend?
Es un tema clásicamente frontend, pero elSSR exige que el servidor genere HTML consultando la base de datos en cada petición y luegohidrate el cliente. Sin todo lo que acabamos de aprender (Sequelize, queries, auth) no se puede hacer SSR de verdad.
SPA
Single Page Application — el cliente lo construye todo en el navegador.
SSR
Server-Side Rendering — el servidor genera HTML por request e hidrata.
SSG
Static Site Generation — HTML pre-generado en build-time.
SPA — Single Page Application
El servidor entrega un HTML casi vacío y un bundle de JavaScript. El navegador pinta la UI y hace fetch a tu API para traer los datos. Es lo que construimos en las Clases 4 y 5 con React.
<!-- Lo que llega del servidor --><!DOCTYPE html><html> <body> <div id="root"></div> <script src="/app.js"></script> </body></html>// Después, en el navegador:fetch('/api/tareas') .then(r => r.json()) .then(tareas => renderUI(tareas))Fortalezas
Navegación instantánea entre vistas, sensación de app, perfecto para dashboards autenticados y herramientas internas. Servidor muy simple — solo expone JSON.
Debilidades
Primer paint lento (descargar JS, ejecutar, fetch). Mal SEO por defecto. Pantalla en blanco con JavaScript desactivado.
Quién la usa
React, Vue, Angular, Svelte (sin meta-framework). Vite + React es el caso de la Clase 5 — front y back desacoplados.
SSR — Server-Side Rendering
El servidor consulta la base de datos, genera el HTML ya con los datosy lo envía al navegador. Luego el JS se ejecuta y hidrata la página para hacerla interactiva.
// app/tareas/page.tsx (Next.js)import { Tarea } from '@/lib/db';export default async function TareasPage() { // ← se ejecuta en el SERVIDOR const tareas = await Tarea.findAll(); return ( <ul> {tareas.map(t => ( <li>{t.titulo}</li> ))} </ul> );}Hidratación
El navegador recibe el HTML ya con los datos pintados. Luego React/Svelte/Vue "ata" event listeners al DOM existente sin re-renderizar. El usuario ve contenido inmediato; en milisegundos, la página se vuelve interactiva.
Aquí entra la BD
El componente hace Tarea.findAll() en el servidor: cada request abre una consulta SQL. Sin lo que aprendiste hoy (BD + ORM + auth) no hay SSR real.
Quién lo usa
Next.js (App Router), Remix, SvelteKit, Nuxt. Server Components de React. E-commerce, dashboards con SEO, contenido personalizado.
SSG — Static Site Generation
El HTML se genera una vez durante el build, no en cada petición. El servidor solo sirve archivos estáticos desde un CDN. Ultra rápido, ultra barato.
# Build-time (una vez, en CI)npm run build→ Genera /dist: index.html blog/clase-1/index.html blog/clase-2/index.html docs/index.html# Runtime — solo sirve archivosGET /blog/clase-1 → 1ms (lectura de disco / CDN)Fortalezas
Carga casi instantánea (HTML ya pre-generado en el edge). Casi inmune a caídas de tu API. SEO perfecto. Cuesta céntimos servirlo en Cloudflare, Netlify o Vercel.
Debilidades
Datos pueden quedar desactualizados (hay que rebuild). Mal encaje cuando cada usuario ve cosas distintas (dashboards personalizados).
Quién lo usa
Astro, Next.js (static export), Hugo, Jekyll, Eleventy, Docusaurus. Esta misma web del curso es SSG: Vite build → Cloudflare Pages.
¿Cuándo elegir cada uno?
No hay un ganador absoluto. Cada estrategia gana en un eje concreto.
| Criterio | SPA | SSR | SSG |
|---|---|---|---|
| First paint | Lento | Rápido | Instantáneo |
| SEO | Limitado | Excelente | Excelente |
| Datos siempre frescos | Sí (por API) | Sí (por request) | No (build) |
| Coste de servidor | Bajo | Alto (BD por request) | Casi cero |
| Personalización por usuario | Trivial | Posible | Difícil |
Usa SPA si...
es una app interna o autenticada (dashboards, paneles admin) y el SEO no importa.
Usa SSR si...
necesitas SEO con datos dinámicos por usuario o por hora (e-commerce, feeds).
Usa SSG si...
el contenido cambia poco y es igual para todos (blogs, docs, landings).
📚Material complementario▾
Resumen de la Clase 7
| Concepto | Definición |
|---|---|
| SQL | Lenguaje para bases de datos relacionales |
| NoSQL | Bases de datos no relacionales (documentos, clave-valor) |
| ORM | Herramienta para interactuar con la BD usando código tipado |
| JWT | Token firmado para autenticación stateless |
| Hashing | Transformación de un solo sentido para proteger contraseñas |
| SPA | Single Page Application — el navegador construye la UI tras hacer fetch a la API |
| SSR | Server-Side Rendering — el servidor genera HTML consultando la BD por request |
| SSG | Static Site Generation — HTML pre-generado en build-time servido desde CDN |
| Hidratación | El cliente "ata" event listeners al HTML pre-renderizado para hacerlo interactivo |
📚Material complementario▾
Ejercicio 7.1: Modelar una base de datos
Define los modelos de Sequelize para una red social simple con Usuarios, Posts, Comentarios y Likes.
Ver solución (Sequelize)
const { Sequelize, DataTypes } = require('sequelize');const Usuario = sequelize.define('Usuario', { nombre: { type: DataTypes.STRING, allowNull: false }, email: { type: DataTypes.STRING, unique: true, allowNull: false }, bio: { type: DataTypes.TEXT }});const Post = sequelize.define('Post', { contenido: { type: DataTypes.TEXT, allowNull: false }});const Comentario = sequelize.define('Comentario', { contenido: { type: DataTypes.TEXT, allowNull: false }});const Like = sequelize.define('Like', {}, { indexes: [{ unique: true, fields: ["UsuarioId", "PostId"] }]});// RelacionesUsuario.hasMany(Post, { foreignKey: 'autorId' });Post.belongsTo(Usuario, { foreignKey: 'autorId' });Post.hasMany(Comentario); Comentario.belongsTo(Post);Usuario.hasMany(Comentario);Comentario.belongsTo(Usuario);Usuario.belongsToMany(Post, { through: Like });Post.belongsToMany(Usuario, { through: Like });Ejercicio 7.2: Implementar registro y login
Agrega autenticación a la API de tareas de la Clase 6. Cada usuario solo verá y editará sus tareas.
Endpoints a añadir
- •
POST /api/auth/registro— hashea, valida email único, devuelve201 - •
POST /api/auth/login— verifica credenciales, devuelve JWT conexpiresIn: '24h'
Middleware y protección
- • Middleware
autenticarque lee el headerAuthorization - • Protege todas las rutas de tareas
- • Filtra por
req.usuario.userIden cada consulta a Sequelize
Pista: en GET /api/tareas añadewhere: { usuarioId: req.usuario.userId }.
Ejercicio 7.3: Validación con Zod
Instala Zod, crea esquemas para registro y para creación de tarea, y construye un middleware reutilizable validar(esquema).
Ver pista 1
const validar = (esquema) => (req, res, next) => { const resultado = esquema.safeParse(req.body); if (!resultado.success) { return res.status(400).json({ error: 'Datos invalidos', detalles: resultado.error.errors }); } req.body = resultado.data; // datos validados y tipados next();};Úsalo así: app.post('/api/tareas', autenticar, validar(esquemaTarea), handler)
Body JSON
// Esquema aplicadoz.object({ nombre: z.string().min(2).max(100), email: z.string().email(), edad: z.number().int().min(18).optional()})Resultado
✓ Datos válidos
Prueba a poner un email inválido, edad < 18 o un nombre vacío.
Ejercicio 7.4: Auditoría de seguridad
Revisa este código y marca las líneas con problemas. Hay al menos 7. Tras comprobar verás el riesgo y la corrección de cada una.
Click en una línea para marcarla como sospechosa. Pulsa Comprobar cuando creas haberlas encontrado todas.
const DB_PASSWORD = "admin123";
const JWT_SECRET = "secreto";
app.post('/api/login', (req, res) => {
const usuario = usuarios.find(u =>
u.email === req.body.email && u.password === req.body.password
);
if (!usuario) return res.status(401).send('No autorizado');
const token = jwt.sign({ ...usuario }, JWT_SECRET);
res.json({ token, password: usuario.password });
});
app.post('/api/usuarios', (req, res) => {
usuarios.push(req.body);
res.json(req.body);
});
app.get('/api/admin/usuarios', (req, res) => res.json(usuarios));
Verde = acertaste · Amarillo = se te escapó · Gris = no era problema.
Línea 1
Contraseña hardcodeada en el código
Los secretos viajan al repositorio y quedan en el historial.
Fix: Mover a variable de entorno: process.env.DB_PASSWORD
Línea 2
Secret JWT débil y hardcodeado
Un secret corto se rompe por fuerza bruta y, además, está en el código.
Fix: process.env.JWT_SECRET con al menos 64 caracteres aleatorios.
Línea 5
Búsqueda lineal en array, sin BD
Vale para demos pero no escala; en producción usa la BD con índice por email.
Fix: await Usuario.findOne({ where: { email } })
Línea 6
Comparación de contraseña en texto plano
La contraseña se guarda y compara sin hash. Si se filtra la BD, todo queda expuesto.
Fix: Guardar bcrypt.hash() y comparar con bcrypt.compare(plain, hash).
Línea 9
Payload JWT con el objeto usuario completo
El JWT es legible (solo firmado, no cifrado). Estás exponiendo password y campos internos.
Fix: jwt.sign({ userId: usuario.id }, SECRET, { expiresIn: "24h" })
Línea 10
Devuelves la contraseña en la respuesta
Nada justifica que el cliente reciba la contraseña, ni siquiera hasheada.
Fix: res.json({ token }) — y nada más.
Línea 14
Registro sin validación ni hashing
Confías ciegamente en req.body; permite inyectar campos como { rol: "admin" } y la contraseña queda en plano.
Fix: Validar con Zod, hashear con bcrypt y guardar solo campos esperados.
Línea 18
Endpoint admin sin auth ni autorización
Cualquiera puede listar todos los usuarios. Faltan middleware de auth y comprobación de rol.
Fix: app.get("/api/admin/usuarios", autenticar, soloAdmin, (req, res) => { ... })
Proyecto: API con BD y autenticación
Evoluciona la API de tareas de la Clase 6 conbase de datos persistente (SQLite + Sequelize) y sistema completo de autenticación con JWT.
Stack
- • Express + Node.js (de la Clase 6)
- • SQLite con Sequelize
- • bcrypt + jsonwebtoken
- • Zod para validación
- • dotenv para secretos
- • helmet + express-rate-limit
Funcionalidades
- • Registro con contraseña hasheada
- • Login que retorna JWT (24h)
- • CRUD de tareas por usuario
- • Validación de entrada con Zod
- • Rutas protegidas con middleware
- • Variables de entorno (.env)
Estructura sugerida
Cada responsabilidad en su lugar: modelos, rutas, middleware y configuración.
mi-api-tareas/├── .env├── .env.example├── .gitignore├── package.json├── dev.db└── src/ ├── server.js ├── models/ │ ├── index.js (sequelize + asociaciones) │ ├── Usuario.js │ └── Tarea.js ├── routes/ │ ├── auth.js │ └── tareas.js ├── middleware/ │ ├── autenticar.js │ └── validar.js └── schemas/ ├── usuario.js └── tarea.jsmodels/
Un archivo por modelo (Usuario.js, Tarea.js) eindex.js que crea la instancia de Sequelize, importa los modelos y registra las asociaciones.
routes/
Un router Express por dominio. auth.js tiene/registro y /login; tareas.js el CRUD.
middleware/
autenticar.js verifica el JWT. validar.js es la fábrica de middlewares de Zod del Ejercicio 7.3.
schemas/
Los esquemas Zod, reutilizables desde varias rutas.
Criterios de evaluación
Checklist
- ☑ Base de datos funcional con Sequelize
- ☑ Registro con contraseña hasheada (bcrypt)
- ☑ Login que retorna JWT válido
- ☑ Rutas protegidas con middleware
- ☑ Cada usuario solo accede a sus datos
- ☑ Validación de entrada con Zod
- ☑ Variables de entorno usadas correctamente
- ☑ No hay secretos en el código fuente
Bonus
- ⭐
helmetyexpress-rate-limitactivos - ⭐ Refresh tokens además del access token
- ⭐ Roles de usuario (admin vs normal)
- ⭐ Colección Postman o archivo
.http - ⭐ Tests con Vitest + Supertest
- ⭐ Despliegue en Render, Railway o Fly.io
- ⭐ Pipeline CI que ejecuta migrate y tests
Tu backend ya es de producción
Bases de datos persistentes, autenticación con JWT y seguridad básica son la diferencia entre un prototipo y un servicio real. En la próxima clase damos un paso atrás para hablar dearquitectura y patrones de diseño.