Skip to content

🎯 Meta del capítulo: entender qué pasa realmente cuando escribes una URL y pulsas Enter, y qué papel juega el "backend" en todo eso. Sin esto, todo lo demás es memorizar sintaxis.

🧭 ¿Vienes de cero? Si aún no te manejas con la terminal, empieza por la Parte 0: Terminal y Linux. Todo lo que construyas en este libro acabará corriendo en un Linux al que te conectarás por SSH.


0.1 · ¿Qué es el backend?

Una aplicación web tiene (casi siempre) dos mitades:

┌─────────────────────┐         petición (request)        ┌─────────────────────┐
│      FRONTEND        │  ───────────────────────────────► │      BACKEND         │
│  (lo que ves)        │                                    │  (lo que NO ves)     │
│                      │                                    │                      │
│  HTML, CSS, JS       │  ◄─────────────────────────────── │  Servidor + BD       │
│  Navegador / App     │         respuesta (response)       │  Lógica de negocio   │
└─────────────────────┘                                    └─────────────────────┘


                                                            ┌──────────────────┐
                                                            │  BASE DE DATOS   │
                                                            │  (PostgreSQL…)   │
                                                            └──────────────────┘
  • Frontend: lo que se ejecuta en el dispositivo del usuario (navegador, móvil). Se ocupa de mostrar y recoger información.
  • Backend: el programa que corre en un servidor (un ordenador encendido 24/7 en algún lado). Se ocupa de la lógica, la seguridad y de hablar con la base de datos.

🧠 Analogía del restaurante:

  • El frontend es el comedor y la carta: lo que el cliente ve y toca.
  • El camarero es la API: lleva y trae pedidos.
  • La cocina es el backend: donde se prepara todo de verdad.
  • La despensa es la base de datos: donde se guardan los ingredientes.

El cliente nunca entra a la cocina. Solo pide y recibe. Eso es una API.


0.2 · Cliente-Servidor: el modelo de todo

Internet funciona con el modelo petición/respuesta:

  1. El cliente (tu navegador) hace una petición a una dirección.
  2. El servidor la recibe, la procesa y devuelve una respuesta.
  3. La conexión se cierra. (En HTTP clásico cada petición es independiente → stateless).

Stateless significa que el servidor no "recuerda" nada entre una petición y la siguiente. Si necesitas recordar quién eres (por ejemplo, que ya iniciaste sesión), hay que enviar una "prueba" en cada petición: una cookie de sesión o un token (lo vemos más abajo).


0.3 · HTTP: el idioma de la web

HTTP (HyperText Transfer Protocol) es el "idioma" en que cliente y servidor se hablan. Una petición HTTP tiene esta forma:

http
POST /api/usuarios HTTP/1.1          ← método + ruta + versión
Host: miapp.com                       ← a qué servidor
Content-Type: application/json        ← qué formato envío
Authorization: Bearer eyJhbGc...      ← mi credencial
                                      ← (línea en blanco separa cabeceras del cuerpo)
{ "nombre": "Ana", "email": "a@x.com" }   ← cuerpo (body)

Y la respuesta:

http
HTTP/1.1 201 Created                  ← código de estado
Content-Type: application/json
                                      
{ "id": 42, "nombre": "Ana" }         ← cuerpo de la respuesta

Métodos HTTP (verbos)

Cada método dice qué quieres hacer:

MétodoSignificadoEjemplo¿Cambia datos?
GETLeer / obtenerVer un usuario❌ No (seguro)
POSTCrearRegistrar usuario✅ Sí
PUTReemplazar completoEditar todo el perfil✅ Sí
PATCHModificar parcialCambiar solo el email✅ Sí
DELETEBorrarEliminar usuario✅ Sí

💡 Tip: GET debe ser idempotente y seguro: llamarlo mil veces no cambia nada. Nunca uses GET para borrar o crear ("GET /borrar?id=5" es un anti-patrón clásico y peligroso).

Códigos de estado (los debes memorizar)

El número que devuelve el servidor te dice cómo fue todo:

RangoSignificadoLos importantes
2xx✅ Éxito200 OK, 201 Created, 204 No Content
3xx↪️ Redirección301 Moved Permanently, 304 Not Modified
4xx⚠️ Error del cliente (tú te equivocaste)400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable, 429 Too Many Requests
5xx🔥 Error del servidor (el backend falló)500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable

🧠 Regla mental: 4xx = "culpa tuya (cliente)", 5xx = "culpa mía (servidor)". El 401 es "no sé quién eres" (no autenticado); el 403 es "sé quién eres, pero no puedes" (no autorizado). Confundir estos dos es error de principiante.


0.4 · REST: cómo diseñar una buena API

REST es un estilo (no una tecnología) para diseñar APIs de forma ordenada y predecible. La idea: todo es un recurso identificado por una URL, y usas los verbos HTTP para actuar sobre él.

Recurso: usuarios

GET    /usuarios        → lista de usuarios
GET    /usuarios/42     → el usuario 42
POST   /usuarios        → crear un usuario
PUT    /usuarios/42     → reemplazar el usuario 42
PATCH  /usuarios/42     → modificar parcialmente el 42
DELETE /usuarios/42     → borrar el 42

Anidado:
GET    /usuarios/42/pedidos     → los pedidos del usuario 42

Reglas de oro de REST:

  1. Usa sustantivos en plural, no verbos: /usuarios ✅, no /obtenerUsuarios ❌.
  2. El verbo lo pone HTTP, no la URL.
  3. Devuelve el código de estado correcto (no todo 200).
  4. Usa JSON para el cuerpo.
  5. coherente: si un endpoint devuelve { "data": [...] }, que todos lo hagan igual.

💡 Tip de diseño: versiona tu API desde el día 1: /api/v1/usuarios. Cuando cambies algo que rompa a los clientes, creas /api/v2/ sin romper a nadie.


0.5 · JSON: el formato universal

JSON (JavaScript Object Notation) es cómo viajan los datos entre frontend y backend. Es texto plano, legible por humanos y máquinas:

json
{
  "id": 42,
  "nombre": "Ana López",
  "activo": true,
  "edad": 29,
  "roles": ["admin", "editor"],
  "direccion": {
    "ciudad": "Lima",
    "pais": "PE"
  },
  "telefono": null
}

Tipos que soporta: texto ("..."), número, booleano (true/false), null, array ([...]) y objeto ({...}). No hay fechas ni comentarios: las fechas se envían como texto en formato ISO 8601"2026-07-09T14:30:00Z".


0.6 · Autenticación vs Autorización

Dos conceptos que se confunden siempre:

  • Autenticación (authentication, "authN") = ¿quién eres? → login.
  • Autorización (authorization, "authZ") = ¿qué puedes hacer? → permisos.

Primero te autenticas (demuestras tu identidad), luego el sistema te autoriza (o no) a hacer cosas.

Las dos formas de recordar quién eres

1. Sesiones + cookies (clásico, el que usa Laravel por defecto):

1. Login → el servidor crea una "sesión" y guarda un ID en su memoria/BD.
2. Envía al navegador una cookie con ese ID de sesión.
3. El navegador manda la cookie en cada petición → el servidor sabe quién eres.

2. Tokens JWT (moderno, para APIs y móviles):

1. Login → el servidor genera un JWT (token firmado) y te lo da.
2. Guardas el token (en el cliente).
3. Lo mandas en cada petición: Authorization: Bearer <token>
4. El servidor verifica la firma → confía en el contenido SIN consultar BD.

Un JWT tiene 3 partes separadas por puntos: cabecera.datos.firma

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBbmEifQ.dBjftJeZ4CVP...
└──── cabecera ────┘ └──────── datos (payload) ────────┘ └─ firma ─┘

⚠️ Ojo de seguridad: el payload de un JWT no está cifrado, solo firmado. Cualquiera puede leerlo (es Base64). Nunca metas contraseñas ni datos secretos dentro. La firma solo garantiza que nadie lo modificó.

🔗 La seguridad de APIs (JWT mal implementados, ataques, etc.) la profundizas en tu Manual de Ciberseguridad — Purple Team, capítulo de Web Hacking. Aquí vemos el lado "constructor"; allí, el lado "atacante/defensor".


0.7 · Anatomía de una petición completa (el viaje)

Veamos TODO lo que pasa cuando el frontend pide GET /api/v1/usuarios/42:

1. NAVEGADOR       → resuelve el dominio (DNS: miapp.com → 203.0.113.10)
2. RED             → abre conexión TCP + TLS (candado 🔒 HTTPS)
3. NGINX           → recibe la petición (reverse proxy, cap. 13)
                     y la pasa al backend
4. BACKEND (app)   → 
   a. Routing        ¿qué función maneja "GET /usuarios/42"?
   b. Middleware     ¿está autenticado? ¿tiene permiso? ¿rate limit?
   c. Controlador    lógica: pide a la BD el usuario 42
   d. Base de datos  SELECT * FROM usuarios WHERE id = 42
   e. Serialización  convierte la fila en JSON
5. RESPUESTA       → 200 OK + { "id": 42, ... } vuelve por el mismo camino
6. NAVEGADOR       → recibe el JSON y lo pinta en pantalla

Cada uno de esos pasos es un capítulo de este libro. El "routing", "middleware" y "controlador" los aprenderás en los capítulos de frameworks. La base de datos, en los caps. 01-02. Nginx y TLS, en el 13.


0.8 · Variables de entorno y configuración

Nunca escribas contraseñas ni claves directamente en el código. Se usan variables de entorno, normalmente en un archivo .env:

bash
# .env  (NUNCA se sube a git — va en .gitignore)
DB_HOST=localhost
DB_PORT=5432
DB_NAME=miapp
DB_USER=admin
DB_PASSWORD=un_secreto_muy_largo
JWT_SECRET=otra_clave_secreta
APP_ENV=production

⚠️ Regla sagrada: el archivo .env jamás se sube al repositorio. Añádelo a .gitignore el primer día. En su lugar sube un .env.example con las claves pero sin los valores reales. El 90% de las filtraciones de credenciales en GitHub son por olvidar esto.


0.9 · Paginación, filtrado y límites de uso

Un endpoint que devuelve "todos los usuarios" es una bomba de tiempo: el día que haya 2 millones, esa respuesta tumba el servidor y satura la red. Toda lista en una API real necesita paginación:

http
GET /api/v1/usuarios?page=2&per_page=20&sort=-creado_en&estado=activo
ParámetroPara qué
page / per_pagePaginación clásica por número de página (fácil de entender, ideal para UIs con "página 1, 2, 3…")
cursorPaginación por cursor (un puntero opaco a "la última fila vista") — más eficiente y estable en listas que cambian mientras el usuario navega
sort=-creado_enOrden (el - indica descendente); evita hardcodear el orden en el backend
estado=activoFiltro por campo — documenta qué campos son filtrables, no aceptes cualquier cosa
json
{
  "data": [ { "id": 42, "nombre": "Ana" } ],
  "meta": { "page": 2, "per_page": 20, "total": 187, "total_pages": 10 }
}

⚠️ Paginación por page/offset tiene un problema a gran escala: OFFSET 100000 obliga a la base de datos a recorrer y descartar 100 000 filas antes de devolver la página. Para listas muy grandes o que cambian mucho, la paginación por cursor (WHERE id > :ultimo_id ORDER BY id LIMIT 20) es mucho más rápida porque usa el índice directamente, sin descartar nada.

Rate limiting — proteger tu API de abuso (accidental o malicioso): limitar cuántas peticiones acepta un cliente en una ventana de tiempo.

http
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1721764800

💡 Devuelve siempre las cabeceras X-RateLimit-*. Un cliente bien hecho las lee y espera antes de reintentar, en vez de martillar tu API a ciegas hasta que le des un 429. El apéndice B (Redis) muestra cómo implementarlo con un contador que expira.


0.10 · CORS: por qué el navegador bloquea tus peticiones

Tarde o temprano tu frontend (localhost:3000) intentará llamar a tu backend (localhost:8000) y verás en la consola: blocked by CORS policy. Es el error de principiante más común al conectar frontend y backend.

CORS (Cross-Origin Resource Sharing) es una protección del navegador, no del servidor. Por defecto, un script cargado desde el origen A (https://miapp.com) no puede leer respuestas de un origen B distinto (https://api.miapp.com) — protege al usuario de que una web maliciosa lea datos de otra web donde tiene sesión iniciada (tu banco, por ejemplo).

¿Mismo origen? = mismo esquema + mismo dominio + mismo puerto

https://miapp.com:443       vs   https://miapp.com:443/api      → MISMO origen ✅
https://miapp.com           vs   http://miapp.com                → distinto (esquema) ❌
https://miapp.com           vs   https://api.miapp.com           → distinto (subdominio) ❌
http://localhost:3000       vs   http://localhost:8000           → distinto (puerto) ❌

El servidor decide, con cabeceras, qué orígenes puede leer el navegador:

http
Access-Control-Allow-Origin: https://miapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true

Para peticiones "no simples" (con PUT/DELETE, cabeceras custom o Content-Type: application/json en algunos casos), el navegador manda antes una petición preflight automática con método OPTIONS preguntando "¿me dejas hacer esto?" — tu servidor debe responder a ese OPTIONS con las cabeceras de arriba, sin que tu código de negocio se entere.

🧠 CORS no es un problema, es que está funcionando. Si ves el error en desarrollo, el arreglo NO es deshabilitar CORS a lo loco (Access-Control-Allow-Origin: * con credenciales, por ejemplo, es una vulnerabilidad real) — es decirle al backend, de forma explícita, qué orígenes son de fiar. Todos los frameworks del libro tienen middleware de CORS configurable en una línea (cap. 03-08).

⚠️ Access-Control-Allow-Origin: * + cookies no funciona (y no debe funcionar). El comodín * es incompatible con Access-Control-Allow-Credentials: true por especificación: si tu API necesita cookies de sesión, tienes que listar orígenes explícitos, nunca el comodín.


0.11 · Caché HTTP e idempotencia (lo que evita trabajo repetido)

Dos cabeceras que aparecen en casi cualquier API real y que casi nadie te explica bien:

Cache-Control le dice al navegador/CDN cuánto puede reutilizar una respuesta sin volver a preguntar al servidor:

http
Cache-Control: public, max-age=3600        ← cachéalo 1 hora, cualquiera puede guardarlo
Cache-Control: private, no-store           ← nunca lo guardes (datos sensibles)
Cache-Control: no-cache                     ← guárdalo, pero revalida antes de usarlo

ETag es una "huella digital" del recurso. El cliente la guarda y, en la siguiente petición, pregunta "¿sigue siendo esta versión?" con If-None-Match. Si nada cambió, el servidor responde 304 Not Modified sin cuerpo — ahorra ancho de banda sin sacrificar frescura:

http
GET /api/v1/productos/42
If-None-Match: "a1b2c3"

HTTP/1.1 304 Not Modified        ← el cliente ya tiene la versión correcta, no reenvía nada

Claves de idempotencia (idempotency keys): un POST no es idempotente por naturaleza (llamarlo dos veces crea dos recursos) — pero cuando ese POST cobra dinero (pagos, apéndice P), necesitas que reintentar por un timeout de red no duplique el cargo. El cliente genera un UUID único por operación y lo manda en una cabecera; el servidor recuerda qué UUIDs ya procesó y devuelve el mismo resultado sin repetir el efecto:

http
POST /api/v1/pagos
Idempotency-Key: 8f14e45f-ceea-4c9e-8e7c-9dd10ec6c1f0

🔗 Volverás a ver esta idea exacta como "idempotencia de webhooks" en el apéndice D.5 y en los pagos con Stripe (apéndice P) — es el mismo problema (una operación que se puede repetir por error de red) resuelto con la misma herramienta (una clave única + memoria de qué ya se hizo).


0.12 · Glosario rápido de este capítulo

TérminoEn una frase
APILa "puerta" del backend por la que entran y salen datos
EndpointUna URL concreta de la API (ej. /usuarios/42)
Request / ResponsePetición y respuesta HTTP
Payload / BodyEl cuerpo de datos de una petición (normalmente JSON)
MiddlewareCódigo que se ejecuta entre la petición y el controlador (auth, logs…)
CRUDCreate, Read, Update, Delete: las 4 operaciones básicas sobre datos
StatelessEl servidor no recuerda peticiones anteriores
ORMHerramienta que traduce objetos de tu código ↔ tablas de la BD

✅ Ejercicio del capítulo

Sin escribir código todavía, diseña en papel la API REST de un blog. Debe permitir:

  1. Listar artículos, ver uno, crear, editar y borrar.
  2. Ver los comentarios de un artículo y añadir un comentario.
  3. Registro y login de usuarios.

Para cada operación escribe: método HTTP + ruta + código de estado esperado. Ejemplo:

Listar artículos      → GET  /api/v1/articulos           → 200
Crear artículo        → POST /api/v1/articulos           → 201
Comentarios de un art.→ GET  /api/v1/articulos/5/comentarios → 200
...completa el resto

Guárdalo. Cuando llegues al capítulo de Laravel, implementarás exactamente este diseño.

💡 Pistas de la solución (abre solo si te atascas)
  • Los "comentarios de un artículo" son un recurso anidado: /articulos/{id}/comentarios, no un recurso suelto /comentarios?articuloId=5. Anida cuando el hijo no tiene sentido sin el padre.
  • Registro y login no son REST puro (no hay un recurso "sesión" que se lista o edita), pero se modelan como POST /auth/registro y POST /auth/login — crean un recurso (la cuenta, o el token de sesión).
  • El borrado de un artículo devuelve 204 No Content (éxito, sin cuerpo que devolver), no 200.
  • Piensa en los errores también: ¿qué código devuelve crear un artículo con el título vacío? (pista: 422, no 400 — repásalo en 0.3 si dudas).

🧠 Autoevaluación

Porque GET debe ser seguro (no cambia estado) e idempotente (llamarlo mil veces da el mismo resultado). Los navegadores, proxies y crawlers asumen que un GET es inofensivo y pueden repetirlo o precargarlo sin avisar — un GET /borrar?id=5 puede borrar datos sin que el usuario lo pidiera dos veces.

401 Unauthorized significa "no sé quién eres" — falta autenticación o es inválida. 403 Forbidden significa "sé exactamente quién eres, pero no tienes permiso para esto" — es un fallo de autorización. Confundirlos es el error de principiante más común del capítulo.

Porque cuando de verdad haga falta (un cambio que rompe a los clientes existentes), ya tendrás consumidores en producción que no puedes migrar de la noche a la mañana. Empezar en v1 te da un sitio limpio donde crear v2 sin romper nada, en vez de tener que reescribir rutas retroactivamente.

Porque contiene secretos reales (contraseñas, claves JWT) que cualquiera con acceso al repo podría leer — incluido un repo público por error. Se sube un .env.example con las claves pero sin los valores, documentando qué variables necesita el proyecto sin exponer ninguna.

Ambas son válidas en la práctica, pero anidar bajo el recurso padre (/usuarios/42/pedidos) comunica mejor la relación de pertenencia cuando el hijo no tiene sentido fuera de ese contexto. Un query param (/pedidos?usuarioId=42) encaja más cuando "pedidos" es también un recurso de primer nivel consultable por sí mismo (GET /pedidos).

Porque OFFSET 100000 obliga a la base de datos a recorrer y descartar cien mil filas antes de devolver la página siguiente — el coste crece con la página. Un cursor (WHERE id > :ultimo_id LIMIT 20) usa el índice para saltar directo al punto donde se quedó, sin descartar nada, así que la página 2 y la página 10 000 cuestan lo mismo.

No es un bug de tu API: CORS es una protección que aplica el navegador, no el servidor — por eso curl (que no es un navegador) nunca la sufre. El servidor tiene que declarar explícitamente, con la cabecera Access-Control-Allow-Origin, qué orígenes tienen permiso para leer sus respuestas desde JavaScript.

Porque un timeout de red no te dice si el cargo se procesó o no — el cliente no sabe si debe reintentar. Con una clave de idempotencia única por operación, el servidor recuerda qué peticiones ya procesó y devuelve el mismo resultado sin repetir el efecto, aunque el mismo POST llegue dos veces por un reintento automático.


Siguiente: 01-bases-de-datos-sql.md — donde viven los datos.