Fragua Tech

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 tabla
CREATE 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ásico
INSERT INTO usuarios (nombre, email, edad)
VALUES ('Ana', 'ana@email.com', 25);
SELECT nombre, email FROM usuarios
WHERE edad > 20;
UPDATE usuarios SET edad = 26
WHERE 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.

CriterioSQLNoSQL
Datos altamente relacionadosNo ideal
Esquema estableNo necesario
Datos flexiblesRígido
Consultas complejasExcelenteLimitadas

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 relacionadas
CREATE 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 JOIN
SELECT u.nombre, d.ciudad
FROM usuarios u
JOIN direcciones d
  ON d.usuario_id = u.id
WHERE u.email = 'ana@email.com';
 
-- Resultado
 nombre | ciudad 
--------+--------
 Ana    | 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 driver
npm install sequelize sqlite3
# Para PostgreSQL en producción:
npm install sequelize pg pg-hstore
# CLI opcional para migraciones:
npm install -D sequelize-cli
npx sequelize-cli init

Los 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.js
const { 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 tareas
Usuario.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 tareas
const { sequelize, Usuario, Tarea } = require('./db');
await sequelize.sync();  // dev
app.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 filtros
app.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      ^firma

El 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.

Cliente

POST /api/auth/login

Envía { email, password } al servidor.

Servidor

bcrypt.compare(password, hash)

Verifica que la contraseña coincide con el hash guardado.

Servidor

jwt.sign({ userId }, SECRET)

Firma y emite un token con expiración (ej. 24h).

Cliente

Guarda el token

Lo guarda en memoria o cookie httpOnly y se lo lleva a cada petición.

Cliente

GET /api/perfil

Authorization: Bearer <token>

Servidor

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.

// Registro
const 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 });
});
// Login
app.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ón
const 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 protegida
app.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
Capítulo 5

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.)

01

Variables de entorno

Sacar los secretos del código fuente con .env y dotenv.

02

Validación con Zod

Nunca confiar en req.body: definir un esquema y rechazar lo demás.

03

Rate limiting + Helmet

Frenar fuerza bruta y endurecer los headers HTTP con dos líneas.

Seguridad básica · 01

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/midb
JWT_SECRET=un-secreto-muy-largo-y-aleatorio-aqui
PORT=3000
// Cargar al inicio de tu app
require('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.

Seguridad básica · 02

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 aplicado
z.object({
  nombre: z.string().min(2).max(100),
  email: z.string().email(),
  edad: z.number().int().min(18).optional()
})

Resultado

Pulsa «Validar» para comprobar el body.

Prueba a poner un email inválido, edad < 18 o un nombre vacío.

Seguridad básica · 03

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 minutos
app.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
Capítulo 6

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.

01

SPA

Single Page Application — el cliente lo construye todo en el navegador.

02

SSR

Server-Side Rendering — el servidor genera HTML por request e hidrata.

03

SSG

Static Site Generation — HTML pre-generado en build-time.

Renderizado · 01

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.

Renderizado · 02

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.

Renderizado · 03

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 archivos
GET /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.

Renderizado · 04

¿Cuándo elegir cada uno?

No hay un ganador absoluto. Cada estrategia gana en un eje concreto.

CriterioSPASSRSSG
First paintLentoRápidoInstantáneo
SEOLimitadoExcelenteExcelente
Datos siempre frescosSí (por API)Sí (por request)No (build)
Coste de servidorBajoAlto (BD por request)Casi cero
Personalización por usuarioTrivialPosibleDifí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

ConceptoDefinición
SQLLenguaje para bases de datos relacionales
NoSQLBases de datos no relacionales (documentos, clave-valor)
ORMHerramienta para interactuar con la BD usando código tipado
JWTToken firmado para autenticación stateless
HashingTransformación de un solo sentido para proteger contraseñas
SPASingle Page Application — el navegador construye la UI tras hacer fetch a la API
SSRServer-Side Rendering — el servidor genera HTML consultando la BD por request
SSGStatic Site Generation — HTML pre-generado en build-time servido desde CDN
HidrataciónEl cliente "ata" event listeners al HTML pre-renderizado para hacerlo interactivo
Material complementario
Básico

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"] }]
});
// Relaciones
Usuario.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 });
Intermedio

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, devuelve 201
  • POST /api/auth/login — verifica credenciales, devuelve JWT con expiresIn: '24h'

Middleware y protección

  • • Middleware autenticar que lee el header Authorization
  • • Protege todas las rutas de tareas
  • • Filtra por req.usuario.userId en cada consulta a Sequelize

Pista: en GET /api/tareas añadewhere: { usuarioId: req.usuario.userId }.

Intermedio

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 aplicado
z.object({
  nombre: z.string().min(2).max(100),
  email: z.string().email(),
  edad: z.number().int().min(18).optional()
})

Resultado

Pulsa «Validar» para comprobar el body.

Prueba a poner un email inválido, edad < 18 o un nombre vacío.

Avanzado

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));
        
🔒

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.js

models/

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

  • helmet y express-rate-limit activos
  • ⭐ 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.

Fragua Tech — Clase 7