🔎 Buscar

🔧 Backend — fundamentos transversales

HTTP y diseño de APIs, autenticación y autorización, bases de datos y ORMs, colas y workers, caching, testing, seguridad y despliegue. El contenido completo que aplica a cualquier lenguaje backend.

Backend📖 Contenido

Backend — fundamentos transversales

Un ingeniero backend profesional razona en términos de protocolos, garantías, estados y fallos, no de sintaxis. El lenguaje es una herramienta; estos conceptos son la carrera — y aplican igual a Go, Python, TypeScript o PHP.

Las wikis del sitio profundizan cada tema: HTTP, REST APIs, SQL, Transacciones, Índices, TDD, OWASP, Autenticación. Léelas en paralelo.

HTTP: el protocolo del backend

El backend es HTTP: recibe peticiones y devuelve respuestas. Dominar el protocolo por encima del framework te da ventaja en todo.

Estructura de una petición y una respuesta

PETICIÓN:
POST /api/usuarios HTTP/1.1
Host: api.misitio.com
Content-Type: application/json
Authorization: Bearer <token>

{"nombre": "Ana", "email": "ana@mail.com"}

RESPUESTA:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/usuarios/42

{"id": 42, "nombre": "Ana"}

Verbos y semántica

Verbo Uso Idempotente
GET Leer
POST Crear / acción no idempotente No
PUT Reemplazar todo el recurso
PATCH Actualizar parcialmente No
DELETE Borrar

💡 Idempotencia: repetir la operación da el mismo resultado. DELETE /api/usuarios/42 dos veces → la segunda también “funciona” (404 o 204, pero no crea efectos nuevos). Es la propiedad que hace seguro reintentar.

Códigos de estado que debes saber de memoria

2xx éxito:    200 OK, 201 Created, 204 No Content
3xx redirec.: 301 (permanente), 304 (Not Modified, caché)
4xx cliente:  400 Bad Request, 401 Unauthorized, 403 Forbidden,
              404 Not Found, 409 Conflict, 422 Unprocessable
5xx servidor: 500 Internal, 502 Bad Gateway, 503 Unavailable, 504 Timeout

⚠️ Error común: devolver 200 para errores de negocio («responde con {error:...}»). Es un anti-patrón: rompe la semántica, el monitoreo y los clientes. Usa los códigos de verdad.

Cabeceras clave

Cabecera Para qué
Content-Type Qué formato lleva el body (application/json…)
Authorization Credenciales (Bearer token, Basic)
Cache-Control Política de caché del cliente
CORS Quién puede llamarte desde el navegador
Location Dónde está el recurso creado (con 201)
Set-Cookie Establecer una cookie

HTTP/2 y HTTP/3

  • HTTP/1.1: una petición por conexión (con keep-alive reutiliza, pero en serie).
  • HTTP/2: multiplexa muchas peticiones por una conexión, comprime cabeceras, prioriza. Hoy el estándar.
  • HTTP/3: sobre QUIC/UDP, sin bloqueo head-of-line, mejor en redes malas. El futuro.

REST y diseño de APIs

REST es un estilo de arquitectura: modelas el sistema como recursos (nombres) operados con verbos HTTP (acciones).

Principios

  1. Recursos con nombres (sustantivos, en plural): /usuarios, /pedidos, NO /getUsuario.
  2. Verbos = acciones HTTP: GET /usuarios/42, POST /pedidos, DELETE /pedidos/7.
  3. Estado en el servidor, representaciones en el cuerpo (JSON).
  4. Stateless: cada petición lleva todo lo que necesita (auth, contexto). No hay sesión en el servidor.
  5. Códigos de estado correctos.
  6. HATEOAS (opcional): la respuesta enlaza a las acciones posibles (hoy poco usado).

Buenas prácticas

GET    /api/usuarios?page=2&limit=50       paginación
GET    /api/usuarios/42                    un recurso
POST   /api/usuarios                       crear
PUT    /api/usuarios/42                    reemplazar
PATCH  /api/usuarios/42                    actualizar parcial
DELETE /api/usuarios/42                    borrar

Versionado: /api/v1/usuarios  (o cabecera)
Filtros:    ?estado=activo&desde=2026-01-01
  • Paginación: siempre en listas. Modelos: limit/offset (simple), cursor (escalable, evita saltos).
  • Validación: el backend nunca confía en el cliente; valida todo.
  • Documentación: OpenAPI (Swagger) — el contrato de la API, genera clientes y tests.
  • Errores consistentes: mismo formato siempre.
{
  "error": {
    "code": "USUARIO_NO_ENCONTRADO",
    "message": "No existe un usuario con id 42",
    "status": 404
  }
}

Autenticación y autorización

Autenticación = ¿quién eres? Autorización = ¿qué puedes hacer? Son cosas distintas.

Sesiones vs tokens

Sesión (cookie) Token (JWT)
Estado En el servidor En el propio token (stateless)
Escalado Hay que compartir sesiones Cualquier servidor valida el token
Invalidar Fácil (borrar sesión) Difícil (esperar expiración)
Uso típico Apps web clásicas APIs, SPA, móviles

JWT (JSON Web Token)

Un JWT es un token firmado con 3 partes: header.payload.signature.

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiIsInJvbGUiOiJhZG1pbiJ9.tjVA9rKuQkhZ...
└─── header ───┘ └──── payload ──────┘ └──── firma (firma HMAC/RSA) ────┘
import jwt

token = jwt.encode(
    {"sub": "42", "role": "admin", "exp": 1735689600},
    SECRETO,              # la clave secreta NUNCA en el cliente
    algorithm="HS256",
)
# El servidor verifica la firma en cada petición: si el token no es
# válido o está firmado con otra clave, la verificación falla.

⚠️ Reglas de oro del JWT: guarda el secreto solo en el servidor (variables de entorno), fija exp corto, usa sub para el usuario, nunca pongas datos sensibles en el payload (está firmado pero no cifrado: cualquiera puede leerlo). JWT sobre cookies httpOnly es el estándar moderno de SPAs.

OAuth 2.0 y OIDC

OAuth 2.0 permite delegar acceso («login con Google»): el usuario autoriza a una app a acceder a sus datos sin darle la contraseña. OIDC (OpenID Connect) añade la capa de identidad (¿quién es?).

Usuario ──▶ App (login con Google)
             │  "¿me das acceso?" (OAuth authorization flow)

          Google (authorization server) ──▶ emite access token


App usa el token para llamar a la API de Google

Almacenar contraseñas: hash, nunca texto plano

import bcrypt

hash_pwd = bcrypt.hashpw(b"secreta123", bcrypt.gensalt())
# bcrypt/argon2: lentos A PROPÓSITO (hacen fuerza bruta inviable)
if bcrypt.checkpw(b"secreta123", hash_pwd):
    print("ok")

⚠️ Nunca md5/sha1 para contraseñas (instantáneos de romper). Usa bcrypt, argon2 o scrypt (lentos por diseño, con salt automático).

Autorización: RBAC

RBAC (Role-Based Access Control): roles → permisos.

PERMISOS = {
    "admin":  {"usuarios": "crud", "pedidos": "crud"},
    "editor": {"usuarios": "r",    "pedidos": "crud"},
    "lector": {"usuarios": "r",    "pedidos": "r"},
}

def puede(rol, recurso, accion):
    return accion in PERMISOS.get(rol, {}).get(recurso, "")

Bases de datos desde el backend

La teoría está en Fundamentos: BBDD. Aquí, lo que cambia al usarla desde un backend:

ORM vs SQL directo

ORM (SQLAlchemy, Prisma, GORM, Eloquent) SQL directo / query builder
Productividad Alta (objetos ↔ tablas) Media
Control del SQL Bajo Total
Migraciones Suelen traerlas Manuales
Riesgo N+1, queries ineficientes ocultas SQL manual, más seguro en rendimiento

💡 Usa ORM para el 90% y SQL crudo para el 10% crítico (queries complejas, reports). Y siempre conoce qué SQL genera tu ORM (log de queries activado).

El error N+1 (el clásico)

# MAL (N+1): 1 query para la lista + N queries (una por cada pedido)
usuarios = db.query(Usuario).all()
for u in usuarios:
    print(u.pedidos)   # ← dispara 1 query POR usuario

# BIEN: 1 query con JOIN / eager loading
usuarios = db.query(Usuario).options(joinedload(Usuario.pedidos)).all()

⚠️ Con 100 usuarios: 101 queries vs 1. Se nota brutalmente. Activa el log de queries de tu ORM y lo verás al instante.

Migraciones

Las migraciones versionan el esquema de la BBDD como tu código: cada cambio es un archivo ordenado, aplicable y reversible.

# Ejemplo conceptual de migración
class CreateUsuarios(Migration):
    def up():
        create_table("usuarios", id=PK, nombre=String, email=Unique)
    def down():
        drop_table("usuarios")

💡 Reglas: nunca modifiques el esquema a mano en producción; siempre por migración. Las migraciones se aplican en orden y se versionan con el código.

Conexiones y pooling

Cada petición no abre una conexión nueva (carísimo): los pools reutilizan conexiones.

[App] ──▶ [Pool de conexiones (10)] ──▶ [PostgreSQL]
   pide  ───────────────▶  usa una libre
   devuelve ───────────▶  queda libre para otra

⚠️ Fuga de conexiones (leak): si olvidas cerrar la conexión en una ruta, el pool se agota y toda la app se bloquea («too many connections»). Los frameworks modernos lo gestionan solos; sé consciente al usar clientes raw.

Colas y workers: procesamiento asíncrono

No todo puede hacerse en la petición HTTP (mandar 10.000 emails, procesar un vídeo, generar un reporte). Ahí entran las colas: encolas la tarea, un worker la procesa en segundo plano.

[API] ──▶ [Cola (Redis/RabbitMQ/SQS)] ──▶ [Worker] ──▶ [BBDD]
  responde al instante          procesa en background
# Encolar (rápido, la petición responde ya)
cola.enqueue("procesar_video", video_id=123)

# Worker: procesa en su propio proceso
def procesar_video(video_id):
    renderizar(video_id)
    notificar(video_id)
Concepto Qué es
Cola Buffer de tareas pendientes (Redis, RabbitMQ, SQS, Kafka)
Worker Proceso que saca y procesa tareas
Retry Reintentar tareas fallidas (backoff exponencial)
DLQ (dead-letter queue) Cola donde van las tareas que fallan demasiado
Idempotencia El worker debe tolerar que la tarea se procese 2 veces

💡 Regla de oro de las colas: las tareas deben ser idempotentes (procesarlas dos veces = resultado igual). Los retries las re-ejecutan; si no, duplicas emails/pagos.

Caching en backend

La teoría de caché está en System Design. En backend, el día a día:

# Cache-aside con Redis
def obtener_usuario(uid):
    cacheado = redis.get(f"user:{uid}")
    if cacheado:
        return json.loads(cacheado)        # hit: devuelve la copia
    usuario = db.query(Usuario).get(uid)   # miss: lee de BBDD
    redis.set(f"user:{uid}", json.dumps(usuario), ex=300)  # llena + TTL
    return usuario
Patrón Cómo Cuándo
Cache-aside App lee caché → miss → BBDD → llena El estándar
Read-through La caché carga sola Contenido
Write-through Escribe a ambos Consistencia
Write-back Escribe solo caché, vuelca luego Alta escritura, riesgo

⚠️ Invalidación: TTL corto para datos que cambian, invalidación manual al escribir para lo crítico. La caché es un acelerador, no una fuente de verdad.

Testing de backend

La teoría completa en TDD. El mapa:

Nivel Qué prueba Velocidad Ejemplo
Unit Una función aislada ms suma(2,2) == 4
Integration Varias piezas juntas (ruta + BBDD) s crear usuario vía API
E2E Flujo completo real min registro → login → pedido
Contract La API cumple su contrato s OpenAPI/validación
# Test unitario (pytest)
def test_suma():
    assert suma(2, 2) == 4

# Test de API (FastAPI TestClient / httpx)
def test_crear_usuario(client):
    r = client.post("/api/usuarios", json={"nombre": "Ana"})
    assert r.status_code == 201
    assert r.json()["id"] is not None

💡 Mocks vs real: unit tests con mocks (rápidos, aislados); integration tests con BBDD real (temporal o de test). No mockees el SELECT: tu test dejará de validar el SQL real.

Seguridad: OWASP Top 10

Los 10 riesgos que todo backend debe mitigar (detalle en wiki: OWASP):

Riesgo Mitigación
Inyección SQL Prepared statements / ORM (nunca concatenar strings)
Broken auth bcrypt/argon2, sesiones seguras, MFA
XSS Escapar salida, CSP, no usar innerHTML con datos
CSRF Tokens CSRF en formularios, SameSite cookies
SSRF Validar URLs de salida, bloqueo de rangos internos
Security misconfiguration Sin defaults débiles, headers seguros, sin info en errores
Exposición de datos Logs sin datos sensibles, permisos correctos
# Inyección SQL: NUNCA hacer esto
query = f"SELECT * FROM usuarios WHERE email = '{email}'"   # ⚠️
# Atacante: email = "x' OR '1'='1"  → devuelve TODOS los usuarios

# SIEMPRE: parámetros
query = "SELECT * FROM usuarios WHERE email = ?"
cursor.execute(query, (email,))

💡 Regla de oro: nunca confíes en el cliente. Valida entradas, parametriza SQL, hashea contraseñas, no loguees datos sensibles, y pon headers de seguridad (X-Frame-Options, CSP, HSTS).

Despliegue: 12-factor app

La metodología canónica para apps desplegables (en español en 12factor.net). Lo esencial:

  1. Config por entorno (variables de entorno, no en el código).
  2. Procesos stateless (cualquier instancia puede atender cualquier petición).
  3. Backing services (BBDD, Redis, colas) como servicios adjuntos reemplazables.
  4. Logs como streams (stdout, no archivos por app).
  5. Paridad dev/prod (lo que funciona en local, funciona en prod).
  6. Arranque rápido y apagado limpio.
import os
DATABASE_URL = os.getenv("DATABASE_URL", "postgres://localhost/app")
SECRET_KEY   = os.getenv("SECRET_KEY")   # ⚠️ nunca en el repo

Terminología que debes dominar

Término En una frase
Endpoint Una ruta+verbo de tu API
Middleware Código que corre antes/después de cada ruta (auth, logging)
DTO Objeto de transferencia de datos (lo que cruza la red)
Serialización Convertir objeto ↔ JSON
Idempotencia Reintentar da el mismo resultado
Rate limiting Limitar peticiones por usuario/IP
Pool de conexiones Reutilizar conexiones a BBDD
N+1 N queries extra por descuido del ORM
Migración Cambio versionado del esquema
Worker Proceso que procesa tareas de una cola
DLQ Cola de tareas que fallaron demasiado
Cache invalidation Marcar datos cacheados como viejos
AuthN / AuthZ Autenticación / autorización
JWT Token firmado y stateless
CORS Política de acceso desde el navegador

Práctica propuesta

  1. Construye una API REST CRUD en tu lenguaje (ver las rutas de Go, Python, TypeScript o PHP) con validación, paginación y códigos de estado correctos.
  2. Añade auth con JWT (login + endpoint protegido) y hashea contraseñas con bcrypt.
  3. Reproduce el error N+1 con tu ORM y arréglalo. Mide la diferencia en nº de queries.
  4. Crea una tarea de cola (envío de email simulado) con Redis: encolar, procesar, reintentar al fallar.
  5. Cachea una lectura caliente con Redis (cache-aside) y mide la mejora.
  6. Escribe tests (unit + integration) para tu API. Rompe algo a propósito y verifica que el test lo pilla.
  7. Documenta tu API con OpenAPI y generaa el cliente.

Para profundizar

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.