API REST con Autenticación JWT
API Node.js + TypeScript + Express con JWT, RBAC, rate limiting, OpenAPI docs y deploy en ECS Fargate.
Problema & Solución
Problema
Construir una API REST production-ready con autenticación stateless (JWT), control de acceso basado en roles (RBAC), protección contra abuso (rate limiting), validación exhaustiva de inputs, y documentación automática. El deploy debe ser reproducible con Docker y la infraestructura gestionada con Terraform.
Solución
API construida con Express + TypeScript usando una arquitectura en capas (routes → middleware → controllers → services → repositories). JWT para autenticación stateless con refresh token rotation. RBAC con roles definidos en base de datos. Rate limiting por IP y por usuario con Redis. Validación con Zod en cada endpoint. Documentación OpenAPI generada automáticamente. Containerizada con Docker multi-stage y desplegada en ECS Fargate.
Diagrama de Arquitectura
Cliente (React / React Native / Postman)
│ HTTPS
▼
┌──────────────────────────────────────────────────────┐
│ Express App (TypeScript) │
│ │
│ Request Pipeline: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 1. Helmet (security headers) │ │
│ │ 2. CORS (allow-list de orígenes) │ │
│ │ 3. Rate Limiter (100 req/15min por IP, Redis) │ │
│ │ 4. Body Parser (JSON, max 1MB) │ │
│ │ 5. Morgan (request logging) │ │
│ │ 6. authenticate() middleware (JWT verify) │ │
│ │ 7. authorize() middleware (RBAC check) │ │
│ │ 8. validate() middleware (Zod schema) │ │
│ │ 9. Route Handler (Controller) │ │
│ │ 10. Error Handler global │ │
│ └─────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────┘
│
┌─────────────────┼──────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌────────────────┐
│ PostgreSQL │ │ Redis │ │ Secrets Mgr │
│ (usuarios, │ │ (rate limit │ │ (JWT secret, │
│ roles, │ │ + sessions)│ │ DB password) │
│ recursos) │ └─────────────┘ └────────────────┘
└─────────────┘
Auth Flow (JWT + Refresh Token):
POST /auth/login ──▶ valida credenciales ──▶ genera:
• accessToken (JWT, 15 min, firmado con RS256)
• refreshToken (opaque, 7 días, almacenado en Redis)
POST /auth/refresh ──▶ valida refreshToken en Redis ──▶ rota:
• nuevo accessToken (15 min)
• nuevo refreshToken (7 días) + invalida el anteriorImplementación
Arquitectura en capas (Layered Architecture)
La aplicación se divide en: Routes (define endpoints y aplica middleware), Controllers (maneja el request/response, delega lógica), Services (lógica de negocio pura, testeable), Repositories (acceso a datos, abstrae la DB). Esta separación permite testear cada capa de forma independiente con mocks.
Autenticación JWT con RS256 y Refresh Token Rotation
Se usa RS256 (asimétrico) en lugar de HS256: la clave privada firma los tokens (solo el servidor), la clave pública verifica (puede distribuirse a otros servicios). Access token: 15 minutos, contiene userId y roles. Refresh token: opaque UUID aleatorio, 7 días, almacenado en Redis con el userId asociado. Refresh token rotation: cada vez que se usa, se invalida y genera uno nuevo.
Control de acceso basado en roles (RBAC)
Los roles (admin, user, viewer) se almacenan en PostgreSQL y se incluyen en el JWT payload al login. El middleware authorize('admin', 'user') verifica que el rol del token esté en la lista permitida. Para permisos más granulares (ej: solo el dueño del recurso puede editar), se hace una verificación adicional en el Service layer comparando req.user.id con el ownerId del recurso.
Validación con Zod en cada endpoint
Cada endpoint define un schema Zod para body, params y query. El middleware validate(schema) ejecuta schema.parse() y retorna 400 con los errores de validación si falla. Zod garantiza type safety en runtime, complementando TypeScript que solo opera en compile time.
Rate Limiting con Redis
express-rate-limit con Redis store (rate-limit-redis). Límites: 100 requests/15min por IP (global), 5 intentos de login/15min por IP (anti-brute-force), 1000 requests/hora por usuario autenticado. Redis persiste los contadores entre restarts del servidor y entre múltiples instancias (crítico en ECS con múltiples tasks).
Documentación OpenAPI con Swagger UI
swagger-jsdoc genera la especificación OpenAPI 3.0 desde JSDoc comments en las routes. swagger-ui-express sirve el UI interactivo en /api/docs. Cada endpoint documenta: descripción, parámetros, request body schema, response schemas (200, 400, 401, 403, 404, 500), y ejemplos.
Tech Stack
Runtime & Framework
Autenticación & Seguridad
Validación & Documentación
Base de datos
Caché & Sessions
Infraestructura
Decisiones Técnicas
Express vs Fastify vs NestJS
Elegido
Express + TypeScript
Alternativas
- —Fastify — mayor throughput, decoradores nativos, pero ecosistema más pequeño
- —NestJS — estructura empresarial, inyección de dependencias, más complejo
Razón
Express es el estándar de facto con el ecosistema más amplio. Fastify tiene mejor performance (~2x) pero el overhead de Express es irrelevante en la mayoría de casos reales. NestJS añade demasiado opinionamiento y curva de aprendizaje para una API REST estándar.
JWT RS256 vs HS256
Elegido
RS256 (RSA)
Alternativas
- —HS256 — más simple, suficiente para monolitos o cuando solo hay un servicio
- —PASETO — más moderno, evita errores comunes de JWT, menos soporte de librerías
Razón
RS256 usa par de claves asimétricas: la clave privada firma (solo el auth service), la clave pública verifica (cualquier microservicio). Permite que otros servicios validen tokens sin acceder a la clave privada. HS256 requiere compartir el secreto con todos los servicios que validan tokens, lo que aumenta la superficie de ataque.
Snippets de Código
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import fs from "fs";
const PUBLIC_KEY = fs.readFileSync("./keys/public.pem", "utf-8");
export interface AuthRequest extends Request {
user?: { id: string; email: string; role: string };
}
// Verifica el JWT y adjunta el payload a req.user
export function authenticate(
req: AuthRequest,
res: Response,
next: NextFunction
): void {
const header = req.headers.authorization;
if (!header?.startsWith("Bearer ")) {
res.status(401).json({ error: "Token de autenticación requerido" });
return;
}
const token = header.slice(7);
try {
req.user = jwt.verify(token, PUBLIC_KEY, { algorithms: ["RS256"] }) as AuthRequest["user"];
next();
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
res.status(401).json({ error: "Token expirado", code: "TOKEN_EXPIRED" });
} else {
res.status(401).json({ error: "Token inválido" });
}
}
}
// Verifica que el rol del usuario esté en la lista permitida
export function authorize(...roles: string[]) {
return (req: AuthRequest, res: Response, next: NextFunction): void => {
if (!req.user) {
res.status(401).json({ error: "No autenticado" });
return;
}
if (!roles.includes(req.user.role)) {
res.status(403).json({
error: "Permisos insuficientes",
required: roles,
current: req.user.role,
});
return;
}
next();
};
}import { Request, Response, NextFunction } from "express";
import { ZodSchema, ZodError } from "zod";
// Middleware genérico que valida body, params o query contra un schema Zod
export function validate<T>(
schema: ZodSchema<T>,
target: "body" | "params" | "query" = "body"
) {
return (req: Request, res: Response, next: NextFunction): void => {
const result = schema.safeParse(req[target]);
if (!result.success) {
const errors = (result.error as ZodError).errors.map((e) => ({
field: e.path.join("."),
message: e.message,
}));
res.status(400).json({ error: "Validación fallida", details: errors });
return;
}
req[target] = result.data;
next();
};
}
// --- Uso en routes ---
import { z } from "zod";
import { Router } from "express";
const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8).max(72),
role: z.enum(["user", "admin"]).default("user"),
});
const router = Router();
router.post(
"/users",
authenticate,
authorize("admin"),
validate(CreateUserSchema),
async (req, res) => {
// req.body está tipado y validado
const { email, password, role } = req.body;
// ...
res.status(201).json({ message: "Usuario creado" });
}
);