🎯 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:
- El cliente (tu navegador) hace una petición a una dirección.
- El servidor la recibe, la procesa y devuelve una respuesta.
- 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:
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/1.1 201 Created ← código de estado
Content-Type: application/json
{ "id": 42, "nombre": "Ana" } ← cuerpo de la respuestaMétodos HTTP (verbos)
Cada método dice qué quieres hacer:
| Método | Significado | Ejemplo | ¿Cambia datos? |
|---|---|---|---|
GET | Leer / obtener | Ver un usuario | ❌ No (seguro) |
POST | Crear | Registrar usuario | ✅ Sí |
PUT | Reemplazar completo | Editar todo el perfil | ✅ Sí |
PATCH | Modificar parcial | Cambiar solo el email | ✅ Sí |
DELETE | Borrar | Eliminar usuario | ✅ Sí |
💡 Tip:
GETdebe ser idempotente y seguro: llamarlo mil veces no cambia nada. Nunca usesGETpara 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:
| Rango | Significado | Los importantes |
|---|---|---|
| 2xx | ✅ Éxito | 200 OK, 201 Created, 204 No Content |
| 3xx | ↪️ Redirección | 301 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
401es "no sé quién eres" (no autenticado); el403es "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 42Reglas de oro de REST:
- Usa sustantivos en plural, no verbos:
/usuarios✅, no/obtenerUsuarios❌. - El verbo lo pone HTTP, no la URL.
- Devuelve el código de estado correcto (no todo
200). - Usa JSON para el cuerpo.
- Sé 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:
{
"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 pantallaCada 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:
# .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
.envjamás se sube al repositorio. Añádelo a.gitignoreel primer día. En su lugar sube un.env.examplecon 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:
GET /api/v1/usuarios?page=2&per_page=20&sort=-creado_en&estado=activo| Parámetro | Para qué |
|---|---|
page / per_page | Paginación clásica por número de página (fácil de entender, ideal para UIs con "página 1, 2, 3…") |
cursor | Paginació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_en | Orden (el - indica descendente); evita hardcodear el orden en el backend |
estado=activo | Filtro por campo — documenta qué campos son filtrables, no aceptes cualquier cosa |
{
"data": [ { "id": 42, "nombre": "Ana" } ],
"meta": { "page": 2, "per_page": 20, "total": 187, "total_pages": 10 }
}⚠️ Paginación por
page/offsettiene un problema a gran escala:OFFSET 100000obliga 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/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 un429. 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:
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: truePara 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 conAccess-Control-Allow-Credentials: truepor 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:
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 usarloETag 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:
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 nadaClaves 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:
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érmino | En una frase |
|---|---|
| API | La "puerta" del backend por la que entran y salen datos |
| Endpoint | Una URL concreta de la API (ej. /usuarios/42) |
| Request / Response | Petición y respuesta HTTP |
| Payload / Body | El cuerpo de datos de una petición (normalmente JSON) |
| Middleware | Código que se ejecuta entre la petición y el controlador (auth, logs…) |
| CRUD | Create, Read, Update, Delete: las 4 operaciones básicas sobre datos |
| Stateless | El servidor no recuerda peticiones anteriores |
| ORM | Herramienta 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:
- Listar artículos, ver uno, crear, editar y borrar.
- Ver los comentarios de un artículo y añadir un comentario.
- 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 restoGuá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/registroyPOST /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), no200. - Piensa en los errores también: ¿qué código devuelve crear un artículo con el título vacío? (pista:
422, no400— 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.