← Proyectos/api-rest-jwt
Building💻 Full Stack

API REST con Autenticación JWT

API Node.js + TypeScript + Express con JWT, RBAC, rate limiting, OpenAPI docs y deploy en ECS Fargate.

Node.jsTypeScriptExpressJWTPostgreSQLECS FargateDocker

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 anterior

Implementación

1

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.

2

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.

3

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.

4

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.

5

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

6

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

Node.js 20 LTSExpress 4TypeScript 5

Autenticación & Seguridad

jsonwebtoken (RS256)bcrypthelmetcorsexpress-rate-limit

Validación & Documentación

Zodswagger-jsdocswagger-ui-express

Base de datos

PostgreSQL 16node-postgres (pg)pg-pool

Caché & Sessions

Redis 7ioredisrate-limit-redis

Infraestructura

Docker (multi-stage)ECS FargateTerraformGitHub Actions

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

TypeScript — JWT Middleware (authenticate + authorize)typescript
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();
  };
}
TypeScript — Validación con Zod + middleware genéricotypescript
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" });
  }
);